diff --git a/.github/ISSUE_TEMPLATE/python-api-v1-0-0-feedback.md b/.github/ISSUE_TEMPLATE/python-api-v1-0-0-feedback.md
deleted file mode 100644
index 35c70deb..00000000
--- a/.github/ISSUE_TEMPLATE/python-api-v1-0-0-feedback.md
+++ /dev/null
@@ -1,10 +0,0 @@
----
-name: Python API v1.0.0 Feedback
-about: Feedback for the new Python API
-title: "[API v1.0.0 Feedback] description of feedback"
-labels: python api feedback
-assignees: ACscooter
-
----
-
-
diff --git a/.gitignore b/.gitignore
index e3dfb396..39dca0bf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -124,4 +124,14 @@ datacommons.RCheck
*tar.gz
## VSCode
-.vscode/
\ No newline at end of file
+.vscode/
+
+## JetBrains
+.idea/
+
+# Gemini
+GEMINI.md
+.gemini/
+
+# Temp files
+tmp/
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 396e9ddc..f4890aee 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,113 @@
# Changelog
+## 2.1.6
+
+**Date** - 01/28/2026
+
+**Release Tag** - [v2.1.6](https://github.com/datacommonsorg/api-python/releases/tag/v2.1.6)
+
+**Release Status** - Current head of branch [`master`](https://github.com/datacommonsorg/api-python/tree/master)
+
+This is a patch update that adds support for resolving indicators (StatisticalVariables and Topics) from natural language queries via the `resolve` endpoint. It introduces a new `fetch_indicators` method and updates the `Candidate` model to include match scores and types.
+
+## 2.1.5
+
+**Date** - 01/12/2026
+
+**Release Tag** - [v2.1.5](https://github.com/datacommonsorg/api-python/releases/tag/v2.1.5)
+
+**Release Status** - Current head of branch [`master`](https://github.com/datacommonsorg/api-python/tree/master)
+
+This is a patch update that adds support for per-request API key overrides using a context manager.
+
+## 2.1.4
+
+**Date** - 10/31/2025
+
+**Release Tag** - [v2.1.4](https://github.com/datacommonsorg/api-python/releases/tag/v2.1.4)
+
+**Release Status** - Current head of branch [`master`](https://github.com/datacommonsorg/api-python/tree/master)
+
+This is a patch update that allows the client to use an API key, if specified, for instance validation when a URL is provided during initialization. This is useful for validating against non-production environments or custom Data Commons instances that require authentication.
+
+## 2.1.3
+
+**Date** - 10/21/2025
+
+**Release Tag** - [py2.1.3](https://github.com/datacommonsorg/api-python/releases/tag/py2.1.3)
+
+**Release Status** - Current head of branch [`master`](https://github.com/datacommonsorg/api-python/tree/master)
+
+This is a minor fix that removes text validation on the `surface_header_value` parameter so that additional Data Commons surfaces can be added without re-releasing the package. This is only used by Data Commons to track usage across its platforms, and does not change the behavior of the client.
+
+## 2.1.2
+
+**Date** - 10/16/2025
+
+**Release Tag** - [py2.1.2](https://github.com/datacommonsorg/api-python/releases/tag/py2.1.2)
+
+**Release Status** - Current head of branch [`master`](https://github.com/datacommonsorg/api-python/tree/master)
+
+This update adds an optional `surface_header_value` parameter to the Data Commons Client that is used internally by Data Commons
+to track usage across its platforms. Other Data Commons services make calls to the Python client and pass in this parameter, but it
+is not intended for public use and does not affect the behavior of the client.
+
+## 2.1.1
+
+**Date** - 06/10/2025
+
+**Release Tag** - [py2.1.1](https://github.com/datacommonsorg/api-python/releases/tag/py2.1.1)
+
+**Release Status** - Current head of branch [`master`](https://github.com/datacommonsorg/api-python/tree/master)
+
+This is an under-the-hood update to use `pydantic` for data models.
+
+## 2.1.0
+
+**Date** - 05/08/2025
+
+**Release Tag** - [py2.1.0](https://github.com/datacommonsorg/api-python/releases/tag/py2.1.0)
+
+**Release Status** - Current head of branch [`master`](https://github.com/datacommonsorg/api-python/tree/master)
+
+Bugs fixed:
+
+- Remove auto-flattening for unpack_arcs
+- Fix unpack_arcs when multiple arcs are in the node response
+
+Other improvements:
+
+- Clarify parent_entity requirements for observations_dataframe
+- Updated some tutorials and notebooks to use the v2 client
+- Fix install command and refine documentation
+- Handle empty/malformed REST API node responses
+- Make renamed methods backwards compatible
+
+New features:
+
+- Add helpers to extract data from NodeResponse arcs
+- Add convenient ways to fetch parents and children of given entities
+
+## 2.0.0
+
+**Date** - 04/08/2025
+
+**Release Tag** - [py2.0.0](https://github.com/datacommonsorg/api-python/releases/tag/py2.0.0)
+
+**Release Status** - Current head of branch [`master`](https://github.com/datacommonsorg/api-python/tree/master)
+
+Initial v2 of the Data Commons API Python client library.
+
+## 1.4.4
+
+**Date** - 01/12/2026
+
+**Release Tag** - [py1.4.4](https://github.com/datacommonsorg/api-python/releases/tag/py1.4.4)
+
+**Release Status** - Current head of branch [`master`](https://github.com/datacommonsorg/api-python/tree/master)
+
+Added deprecation notice to legacy `datacommons` package.
+
## 1.4.3
**Date** - 11/10/2020
@@ -22,7 +130,7 @@ Bugs fixed in new release
New features added to the Python API
-- Added batching to `get_stat_all` to handle querying for many StatisticalVariables across many Places.
+- Added batching to `get_stat_all` to handle querying for many StatisticalVariables across many Places.
## 1.4.1
@@ -34,9 +142,9 @@ New features added to the Python API
New features added to the Python API
-- `get_stat_value`: returns a single value for the specified Place and StatisticalVariable.
-- `get_stat_series`: returns a single time series dict for the specified Place and StatisticalVariable.
-- `get_stat_all`: returns a nested dictionary of all possible time series for each Place and StatisticalVariable pair.
+- `get_stat_value`: returns a single value for the specified Place and StatisticalVariable.
+- `get_stat_series`: returns a single time series dict for the specified Place and StatisticalVariable.
+- `get_stat_all`: returns a nested dictionary of all possible time series for each Place and StatisticalVariable pair.
## 1.3.0
@@ -48,12 +156,12 @@ New features added to the Python API
New features added to the Python API
-- Option to use the API without providing API key.
-- New options to `get_stats`: `measurement_method`, `unit`, and `obs_period` for finer-grain control over returned statistics.
+- Option to use the API without providing API key.
+- New options to `get_stats`: `measurement_method`, `unit`, and `obs_period` for finer-grain control over returned statistics.
Bugs fixed in new release
-- Elegantly handle sparse responses from `query`.
+- Elegantly handle sparse responses from `query`.
## 1.2.0
@@ -65,12 +173,11 @@ Bugs fixed in new release
New features added to the Python API
-- Add get_stats API to get observations given a StatisticalVariable and place dcids.
+- Add get_stats API to get observations given a StatisticalVariable and place dcids.
Bugs fixed in new release
-- Check Null and empty data in REST API response field to avoid KeyError.
-
+- Check Null and empty data in REST API response field to avoid KeyError.
## 1.1.0
@@ -82,11 +189,11 @@ Bugs fixed in new release
New features added to the Python API
-- Handle and ignore NaN in API argument.
+- Handle and ignore NaN in API argument.
Bugs fixed in new release
-- Various small fix.
+- Various small fix.
## 1.0.9
@@ -98,7 +205,7 @@ Bugs fixed in new release
New features added to the Python API
-- Use six package for urllib.
+- Use six package for urllib.
## 1.0.7
@@ -110,7 +217,7 @@ New features added to the Python API
New features added to the Python API
-- Support python 2.7.
+- Support python 2.7.
## 1.0.6
@@ -122,9 +229,7 @@ New features added to the Python API
New features added to the Python API
-- Add a new API for getting related places.
-
-
+- Add a new API for getting related places.
## 1.0.5
@@ -136,9 +241,8 @@ New features added to the Python API
New features added to the Python API
-- Remove the dependency on Pandas and Numpy in package dependency.
-- Replace requests with urllib.
-
+- Remove the dependency on Pandas and Numpy in package dependency.
+- Replace requests with urllib.
## 1.0.2
@@ -150,8 +254,7 @@ New features added to the Python API
New features added to the Python API
-- Remove the dependency on Pandas.
-
+- Remove the dependency on Pandas.
## 1.0.1
@@ -163,14 +266,14 @@ New features added to the Python API
New features added to the Python API
-- Added two new functions `get_pop_obs` and `get_place_obs`
-- SPARQL query is now supported as a function `query` instead of a class.
-- Added documentation on how to provision an API key and provide it to the API
+- Added two new functions `get_pop_obs` and `get_place_obs`
+- SPARQL query is now supported as a function `query` instead of a class.
+- Added documentation on how to provision an API key and provide it to the API
Bugs fixed in new release
-- Fixed various typos and formatting issues in the documentation.
-- If the index of the `pandas.Series` passed into functions such as `get_populations` and `get_observations` was not contiguous, then the assignment step would not properly align the values returned by calling the function. This is because the `pandas.Series` returned by the function would have a different index than the given series. This is fixed by assigning the index of the returned series to that of the given series.
+- Fixed various typos and formatting issues in the documentation.
+- If the index of the `pandas.Series` passed into functions such as `get_populations` and `get_observations` was not contiguous, then the assignment step would not properly align the values returned by calling the function. This is because the `pandas.Series` returned by the function would have a different index than the given series. This is fixed by assigning the index of the returned series to that of the given series.
## 1.0.0
@@ -180,15 +283,15 @@ Bugs fixed in new release
New release of the Python API.
-- New functions in the API built on top of the [Data Commons REST API](https://github.com/datacommonsorg/mixer).
- - `get_property_labels`
- - `get_property_values`
- - `get_triples`
- - `get_populations`
- - `get_observations`
- - `get_places_in`
-- New tests and examples checked into `datacommons/test` and `datacommons/examples`
-- Full documentation released on [readthedocs](https://datacommons.readthedocs.io/en/latest/)
+- New functions in the API built on top of the [Data Commons REST API](https://github.com/datacommonsorg/mixer).
+ - `get_property_labels`
+ - `get_property_values`
+ - `get_triples`
+ - `get_populations`
+ - `get_observations`
+ - `get_places_in`
+- New tests and examples checked into `datacommons/test` and `datacommons/examples`
+- Full documentation released on [readthedocs](https://datacommons.readthedocs.io/en/latest/)
## 0.4.3
@@ -200,7 +303,7 @@ New release of the Python API.
Patch release that fixes bugs in `datacommons.Client`.
-- Functions `get_cities` and `get_states` now provides `typeOf` constraints in their datalog queries.
+- Functions `get_cities` and `get_states` now provides `typeOf` constraints in their datalog queries.
## 0.x
diff --git a/Missing_Data_Imputation_Tutorial.ipynb b/Missing_Data_Imputation_Tutorial.ipynb
deleted file mode 100644
index a0fbdab9..00000000
--- a/Missing_Data_Imputation_Tutorial.ipynb
+++ /dev/null
@@ -1,639 +0,0 @@
-{
- "nbformat": 4,
- "nbformat_minor": 0,
- "metadata": {
- "colab": {
- "name": "Missing Data Imputation Tutorial.ipynb",
- "provenance": [],
- "collapsed_sections": [],
- "include_colab_link": true
- },
- "kernelspec": {
- "name": "python3",
- "display_name": "Python 3"
- }
- },
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {
- "id": "view-in-github",
- "colab_type": "text"
- },
- "source": [
- ""
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "id": "m-Jrb-eMOvMP",
- "colab_type": "text"
- },
- "source": [
- "# Time Series Holes: A Case Study Using Data Commons\n",
- "\n",
- "In this notebook, we will examine types of missing values in time series and methods of imputing those values. Missing values might happen during data collection, or may be intentionally introduced during processing. Data scientists often impute missing values before using time series for analysis and machine learning.\n",
- "\n",
- "First, we will survey random statistical variables from Data Commons and visualize their time series for a few counties. This will give us an understanding of the types of time series holes that exist.\n",
- "\n",
- "Then, we will impute missing values using Pandas imputation methods that the [scipy.interpolate](https://docs.scipy.org/doc/scipy/reference/tutorial/interpolate.html) package provides.\n",
- "\n",
- "\n",
- "**To use this notebook, click on 'Runtime' and select 'Run all'**."
- ]
- },
- {
- "cell_type": "code",
- "metadata": {
- "id": "b-Wt5rWaAtSQ",
- "colab_type": "code",
- "cellView": "form",
- "colab": {}
- },
- "source": [
- "#@title Technical details: sampling function\n",
- "#\n",
- "#\n",
- "#@markdown This section contains the functions that we use to retrieve data from Data Commons. We use \n",
- "#@markdown [**`build_time_series_dataframe`**](https://docs.datacommons.org/api/pandas/time_series.html) \n",
- "#@markdown to access the data for each sampled statistical variable. \n",
- "#\n",
- "#@markdown **To reveal the hidden code double click on this section.**\n",
- "#\n",
- "\n",
- "\n",
- "\"\"\"\n",
- "The function input are the total sampled statistical variables and a boolean flag for \n",
- "plot/not plot the sampled statistical variables. To avoid value errors for statistical variables \n",
- "with no data for our selected state we use try/except. \n",
- "\"\"\"\n",
- "def select_random_statvars(sample_size, plot_flag, log_p):\n",
- " prov = pd.DataFrame(dc.get_property_values([\"StatisticalVariable\"], \"typeOf\", out =False, limit =500))\n",
- "\n",
- " chosen_idx = np.random.choice(len(prov.index), replace=False, size=sample_size)\n",
- " df_trimmed = prov.iloc[chosen_idx]\n",
- " allstatvars = set(df_trimmed['StatisticalVariable'])\n",
- " pltstats =[]\n",
- "\n",
- " # Comment the following line if you want to include all statistical variables:\n",
- " allstatvars = [x for x in allstatvars if not (x.startswith(\"dc/\"))]\n",
- "\n",
- " geoids = (dc.get_places_in([_PLACE], \"County\"))[_PLACE]\n",
- "\n",
- " \"\"\"\n",
- " For each statistical variable, a column represents a county (e.g. counties contained in California). \n",
- " We select time series with at least four data points, at least four complete counties, and at least \n",
- " four missing values. You can change the threshold by setting another value to *threshold*.\n",
- " \"\"\"\n",
- " for statvar in allstatvars:\n",
- " try:\n",
- " data = dc.build_time_series_dataframe(geoids, statvar)\n",
- "\n",
- " # Get all state names and store it in a column \"name\"\n",
- " # Get the first name, if there are multiple for a state\n",
- " data.insert(0, 'name', data.index.map(dc.get_property_values(data.index, 'name')).str[0])\n",
- " data.set_index('name', inplace=True)\n",
- " data = data.transpose()\n",
- "\n",
- " data.dropna(axis=0, how='all', inplace=True)\n",
- "\n",
- " notNaColumns = data.columns[data.notna().all()].tolist()\n",
- " naColumns = data.columns[data.isnull().any()].tolist()\n",
- " \n",
- " threshold = 4\n",
- " if (len(naColumns) > threshold and len(notNaColumns)> threshold and data.size> threshold):\n",
- " if (plot_flag):\n",
- " plt = data[naColumns].plot(style=['bo-', 'o-', 'ro-'], figsize=(22, 10));\n",
- " plt.set_ylabel(statvar, fontsize=14)\n",
- " plt.set_xlabel('Date', fontsize=14)\n",
- " pltstats.append(statvar)\n",
- " except ValueError as e:\n",
- " if (log_p):\n",
- " print(statvar, ': No data for', _PLACE)\n",
- " \n",
- " return pltstats"
- ],
- "execution_count": null,
- "outputs": []
- },
- {
- "cell_type": "code",
- "metadata": {
- "id": "20oMQF58TCZO",
- "colab_type": "code",
- "cellView": "form",
- "colab": {}
- },
- "source": [
- "#@title Technical details: validation\n",
- "#\n",
- "#\n",
- "#@markdown In this section, we explain the implementation details of the cross-validation function for technical readers. \n",
- "#\n",
- "#@markdown **To reveal the hidden code double click on this section.**\n",
- "#\n",
- "#\n",
- "\n",
- "\"\"\"\n",
- "The *artificial_score_generator* retrieves data using *build_time_series_dataframe* for each statistical variable.\n",
- "It calls the *multiple_rounds_score_validation* to compute a matrix of R2 scores for each method and statistical \n",
- "variable. \n",
- "\"\"\"\n",
- "\n",
- "def artificial_score_generator(rand_st):\n",
- " if (len(rand_st) < 1):\n",
- " print(\"run select_random_statvars again\")\n",
- " return\n",
- "\n",
- " geoids = (dc.get_places_in([_PLACE], \"County\"))[_PLACE]\n",
- " all_scores = pd.DataFrame()\n",
- " i = -1\n",
- " for sv in rand_st:\n",
- " i += 1\n",
- " data = dc.build_time_series_dataframe(geoids, sv).transpose()\n",
- " data.reset_index(level=0, inplace=True)\n",
- " data.dropna(axis=1, how='all', inplace=True)\n",
- " data['index'] = pd.to_datetime(data['index'])\n",
- " data.set_index('index', inplace=True)\n",
- "\n",
- " current_scores = multiple_rounds_cross_validation(data, 20, sv)\n",
- " # Cap negative values at -100.\n",
- " current_scores.iloc[:, 1:2] = current_scores.iloc[:, 1:2].apply(lambda x: [y if y > -100 else -101 for y in x])\n",
- " if i > 0:\n",
- " all_scores = all_scores.merge(current_scores, on='method', how='left')\n",
- " else:\n",
- " all_scores = current_scores\n",
- " \n",
- " return all_scores\n",
- "\n",
- "\"\"\"\n",
- "*multiple_rounds_score_validation* function calls *one_round_cross_validation()* for \n",
- "(rounds =) 20 times. In each round, the *one_round_cross_validation()* removes data points \n",
- "*independently and uniformly at random* and calls *_FillwithX()* to impute the SAME set of missing \n",
- "data points with each method from the set *_METHODS* that we defined earlier. \n",
- "To generate a normalized result, we report the average of calculated scores.\n",
- "\"\"\"\n",
- "\n",
- "def multiple_rounds_cross_validation(df, rounds, stvar):\n",
- " notNaColumns = df.columns[df.notna().all()].tolist()\n",
- " multiple_rsquared_values = one_round_cross_validation(df, stvar)\n",
- " cols = multiple_rsquared_values.columns.difference(['method'])\n",
- " for round in range(rounds-1):\n",
- " multiple_rsquared_values[cols] = multiple_rsquared_values[cols].add(one_round_cross_validation(df, stvar)[cols])\n",
- " \n",
- " multiple_rsquared_values[cols] = multiple_rsquared_values[cols]/rounds\n",
- " ave = 'Average of %s rounds for %s'%(rounds, stvar)\n",
- " multiple_rsquared_values[ave]=multiple_rsquared_values.mean(axis=1,skipna=True)\n",
- " multiple_rsquared_values.sort_values(by=ave, inplace = True, ascending=False)\n",
- " return multiple_rsquared_values[['method',ave]].round(3) \n",
- "\n",
- "def one_round_cross_validation(df, sv):\n",
- " rsquared_df = pd.DataFrame({'method': pd.Series(_METHODS)})\n",
- " notNaColumns = df.columns[df.notna().all()].tolist()\n",
- " df = df[notNaColumns]\n",
- "\n",
- " # Make a copy of each column and randomly delete values from original columns:\n",
- " for col in notNaColumns:\n",
- " df[col+\"_ref\"] = df[col].copy()\n",
- " df[col] = df[col].sample(frac=0.8)\n",
- "\n",
- " for col in notNaColumns:\n",
- " _FillwithX(df, col, sv)\n",
- " result = []\n",
- " # scoring the result and see which is better\n",
- " for m in _METHODS:\n",
- " r2 = -100\n",
- " try:\n",
- " r2 = r2_score(df[col+'_ref'], df[\"Fill\"+m+\"_\"+col])\n",
- " except ValueError as e:\n",
- " warnings.warn(\"r2 returns large negative value for %s :\" %m)\n",
- " result += [(m, r2)]\n",
- " \n",
- " rsquared_df[\"r2_\"+col] = pd.DataFrame(result, columns=['Method', 'R_squared'])['R_squared'].copy()\n",
- " rsquared_df[\"r2_\"+col] = pd.to_numeric(rsquared_df[\"r2_\"+col])\n",
- " return rsquared_df \n",
- "\n",
- "\n",
- "def _FillwithX(df, geo, sv):\n",
- " for meth in _METHODS:\n",
- " method = \"Fill\"+meth+\"_\"+geo\n",
- " try:\n",
- " if (meth == 'Mean'):\n",
- " df[method] = df[geo].fillna(df[geo].mean())\n",
- " elif (meth == 'Median'):\n",
- " df[method] = df[geo].fillna(df[geo].median()).bfill()\n",
- " elif (meth == 'Spline_d1'):\n",
- " df[method] = df[geo].interpolate(method='spline', order=1).bfill()\n",
- " elif (meth == 'Spline_d2'):\n",
- " df[method] = df[geo].interpolate(method='spline', order=2).bfill()\n",
- " elif (meth == 'Spline_d3'):\n",
- " df[method] = df[geo].interpolate(method='spline', order=3).bfill()\n",
- " elif (meth == 'Spline_d4'):\n",
- " df[method] = df[geo].interpolate(method='spline', order=4).bfill()\n",
- " elif (meth == 'Spline_d5'):\n",
- " df[method] = df[geo].interpolate(method='spline', order=5).bfill()\n",
- " elif (meth == 'InterpolateLinear'):\n",
- " df[method] = df[geo].interpolate(method='linear').bfill()\n",
- " elif (meth == 'Time'):\n",
- " df[method] = df[geo].interpolate(method='time').bfill()\n",
- " elif (meth == 'InterpolateAkima'):\n",
- " df[method] = df[geo].interpolate(method='akima', order=2).bfill()\n",
- " elif (meth == 'InterpolateSLinear'):\n",
- " df[method] = df[geo].interpolate(method='slinear').bfill()\n",
- " elif (meth == 'InterpolatePoly5'):\n",
- " df[method] = df[geo].interpolate(method='polynomial', order=2).bfill()\n",
- " elif (meth == 'InterpolatePoly7'):\n",
- " df[method] = df[geo].interpolate(method='polynomial', order=7).bfill()\n",
- " elif (meth == 'Krogh'):\n",
- " df[method] = df[geo].interpolate(method='krogh').bfill()\n",
- " elif (meth == 'Pchip'):\n",
- " df[method] = df[geo].interpolate(method='pchip').bfill()\n",
- " except:\n",
- " print(meth, \"imputation raised error for \", sv, \" filled missing value with zero instead!\")\n",
- " df[method] = df[geo].fillna(0, inplace=True) \n",
- " return df"
- ],
- "execution_count": null,
- "outputs": []
- },
- {
- "cell_type": "code",
- "metadata": {
- "id": "iVcpgbNr_m5l",
- "colab_type": "code",
- "colab": {
- "base_uri": "https://localhost:8080/",
- "height": 35
- },
- "outputId": "40cde294-a9ce-48f2-f16f-e23f5db21aa4"
- },
- "source": [
- "# from google.colab import auth\n",
- "# auth.authenticate_user()\n",
- "\n",
- "# Import the required libraries\n",
- "import pandas as pd\n",
- "import random\n",
- "\n",
- "!pip install datacommons --upgrade --quiet\n",
- "import datacommons_pandas as dc\n",
- "\n",
- "# Import other required libraries\n",
- "import matplotlib.pyplot as plt\n",
- "plt.rcParams.update({'font.size': 22})\n",
- "import matplotlib.patches as mpatches\n",
- "\n",
- "# Import a scoring metric to compare methods\n",
- "from sklearn.metrics import r2_score\n",
- "# %matplotlib inline\n",
- "\n",
- "import numpy as np\n",
- "\n",
- "import warnings\n",
- "\n",
- "states = pd.DataFrame(dc.get_places_in(['country/USA'], 'State')['country/USA'])\n",
- "states.insert(0, 'name', states[0].map(dc.get_property_values(states[0], 'name')).str[0])"
- ],
- "execution_count": null,
- "outputs": [
- {
- "output_type": "stream",
- "text": [
- "\u001b[?25l\r\u001b[K |████▎ | 10kB 11.5MB/s eta 0:00:01\r\u001b[K |████████▋ | 20kB 1.7MB/s eta 0:00:01\r\u001b[K |█████████████ | 30kB 2.3MB/s eta 0:00:01\r\u001b[K |█████████████████▎ | 40kB 2.6MB/s eta 0:00:01\r\u001b[K |█████████████████████▋ | 51kB 2.0MB/s eta 0:00:01\r\u001b[K |██████████████████████████ | 61kB 2.2MB/s eta 0:00:01\r\u001b[K |██████████████████████████████▎ | 71kB 2.5MB/s eta 0:00:01\r\u001b[K |████████████████████████████████| 81kB 2.2MB/s \n",
- "\u001b[?25h"
- ],
- "name": "stdout"
- }
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "id": "9rRpybHcPAWh",
- "colab_type": "text"
- },
- "source": [
- "# Part 1: Sample random statistical variables\n",
- "\n",
- "In the following block, we will query Data Commons for the time series of 300 statistical variables for your selected state (defaults to California). Then, we will explore the datasets for all counties inside the state and plot the times series that are available for at least 4 counties. In **`select_random_statvars(sample_size, plot_flag, print_flag)`**, you can alter the plot_flag and print_flag to hide the plots and print logs respectively. The third option will let you see the sampled statistical variables with no data within the selected place. \n",
- "\n",
- "**Note:** For visual purposes, we include dots for a maximum of 3 lines. We use plain lines if there are more than 3 counties to plot. "
- ]
- },
- {
- "cell_type": "code",
- "metadata": {
- "id": "YaJPrlmKOVfN",
- "colab_type": "code",
- "cellView": "both",
- "colab": {
- "base_uri": "https://localhost:8080/",
- "height": 1000
- },
- "outputId": "910e7e91-d826-4775-a7f1-a73f1de565f7"
- },
- "source": [
- "#@title Select a state to plot { run: \"auto\" }\n",
- "state_name = \"California\" #@param [\"Missouri\", \"Arkansas\", \"Arizona\", \"Ohio\", \"Connecticut\", \"Vermont\", \"Illinois\", \"South Dakota\", \"Iowa\", \"Oklahoma\", \"Kansas\", \"Washington\", \"Oregon\", \"Hawaii\", \"Minnesota\", \"Idaho\", \"Alaska\", \"Colorado\", \"Delaware\", \"Alabama\", \"North Dakota\", \"Michigan\", \"California\", \"Indiana\", \"Kentucky\", \"Nebraska\", \"Louisiana\", \"New Jersey\", \"Rhode Island\", \"Utah\", \"Nevada\", \"South Carolina\", \"Wisconsin\", \"New York\", \"North Carolina\", \"New Hampshire\", \"Georgia\", \"Pennsylvania\", \"West Virginia\", \"Maine\", \"Mississippi\", \"Montana\", \"Tennessee\", \"New Mexico\", \"Massachusetts\", \"Wyoming\", \"Maryland\", \"Florida\", \"Texas\", \"Virginia\"]\n",
- "_PLACE = states.loc[states['name'] == state_name].values[0][1]\n",
- "\n",
- "# Set the second value to True if you want to plot data for the sampled statistical variable:\n",
- "random_stats = select_random_statvars(300, plot_flag=True, log_p=False)\n",
- "print(\"Number of valid samples: \" , len(random_stats), random_stats)"
- ],
- "execution_count": null,
- "outputs": [
- {
- "output_type": "stream",
- "text": [
- "Number of valid samples: 4 ['Count_Death_85Years_EndocrineNutritionalMetabolicDiseases_Female', 'Count_Death_55To64Years_Male_White', 'RetailDrugDistribution_DrugDistribution_PoppyStrawConcentrate', 'Count_Death_MentalBehaviouralDisorders_White']\n"
- ],
- "name": "stdout"
- },
- {
- "output_type": "display_data",
- "data": {
- "image/png": "iVBORw0KGgoAAAANSUhEUgAABQ8AAAJ6CAYAAACG8bRTAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjIsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+WH4yJAAAgAElEQVR4nOzdd1zWVf/H8deXIUOGgltRMHPnCE3NSVbiTEutNBUbdpum1Z2p3VlmdWdad+t2ZJk4y3E707Ql5sjcpknDElMcCQIuRMb5/YHwA7kQ0AsvwPfz8bgecJ3zPed8vl980NWHMyxjDCIiIiIiIiIiIiJXcnJ0ACIiIiIiIiIiIlI0KXkoIiIiIiIiIiIiNil5KCIiIiIiIiIiIjYpeSgiIiIiIiIiIiI2KXkoIiIiIiIiIiIiNrk4OgAREREpHDt37izr5OQ0yNnZub8xphxgOTomEZFiwFiWFZOamjovLS1tdnBwcLyjAxIREXEkyxjj6BhERETEznbu3FnKxcVlsZ+f323lypU75+7ufsmylDsUEcmLMYaLFy+WOnXqlHdcXNxPKSkpfYKDgy85Oi4RERFH0bJlERGRkulhHx+f26pVq3baw8NDiUMRkXyyLAsPD49LAQEBsT4+PrcBDzk6JhEREUdS8lBERKQEcnV17eLn56ekoYjINbIsCz8/v0uurq5dHB2LiIiIIyl5KCIiUjI1Kl269AVHByEiUpyVLl06EWjk6DhEREQcSclDERGREsgY4+Hs7Jzm6DhERIozZ2fnVGOMp6PjEBERcSQlD0VEREooLVkWEbk++j0qIiKi5KGIiIiIiIiIiIjkQslDERERERERERERsUnJQxEREREREREREbFJyUMRERERERERERGxSclDERERERERERERsUnJQxEREREREREREbFJyUMRERERERERERGxSclDEREREcGyrGDLsoIBPv7447JNmjSp6+np2bR06dJNW7VqVXvdunVettp99913pZ988slqDRs2rOfv79/Y1dX19goVKjQKDQ2t+e2335a21ea5556rYllW8HPPPVfljz/+cH3ggQcCy5cv38jDw6Np/fr1682aNatsxrVfffVV6fbt29cqU6ZMEw8Pj6YtWrSovWHDBs/c7uPEiRPOI0aMqFK7du36np6eTTP6fPXVVyskJSVZ1/ucRERERG42ljHG0TGIiIiIne3duzeqcePGMY6OQ4qPjMThyJEjj3/44YeVb7/99nMVKlRIjoyM9Dh06JC7q6urWbNmza933333+azt7rzzztrbtm3zvuWWWxKrVq16qVSpUml//vmn+++//+7h7OzMjBkz/nz00UfjsrZ57rnnqrz77ruV77///tiIiAhfT0/P1MaNG184fvy4665du7wsy2L69Ol/uru7m8cee6xm3bp1E2vUqJF04MABj0OHDrl7eHikbd269UCjRo2Ssva7bds2j27dut166tQp14oVKybXq1fvQlpaGnv37vVKSEhwbtWq1dnvvvvud3d3d30Alnzbu3dvucaNGwc6Og4RERFHUfJQRESkBFLyUAoqI3no6+ubumrVqt/atm17ASA1NZVHHnmkxueff16uVatWZ7Zs2fJ71nZLlizxadGixYWAgICUrOULFizwHTRo0C2lS5dOO3LkyE/e3t5pGXUZyUOAwYMH/z1jxowjLi4uALz11lvlx4wZU71ixYrJiYmJTu++++7hjORjamoq9913X83Vq1eX7dOnT8yiRYsOZ/R57tw5q27dug2jo6NLjR07NvrVV1894erqCsDJkyede/XqVfOHH37wefbZZ4//5z//OVYYz1BKJiUPRUTkZqdlyyIiIiKS6YUXXojOSBwCODs7M2nSpGiAnTt3el+59Ld3795nrkwcAvTr1y+hc+fOcQkJCc6rV6/2tjVWlSpVLk2dOvVoRuIQ4J///OepMmXKpJw8edK1Xbt2CVlnLTo7OzN27NjjAFu2bPHJ2teUKVPKRUdHl+rSpUvcv//978zEIUDFihVTFyxYEOXi4mJmzZpVPi0tDRERERHJH5e8LxERERGRm8UDDzyQcGVZQEBAio+PT+qZM2ecT5486Vy9evVsycLjx4+7LF682Hf//v0eCQkJzikpKRbAr7/+6nH5q5utse68886zVy4hdnFxoWrVqpfi4+NdOnXqdObKNg0bNkwCOHXqlGvW8nXr1vkC9O7dO+7KNgCBgYHJNWrUSPrjjz/c9+/f73blkmcRERERsU3JQxERERHJVKtWrUu2yr28vFLPnDnjnJiYmG3lyuTJk8u9/PLLARcvXsx1RcuZM2ecbZVXrVrV5lienp5pANWrV89R7+vrmwZw6dKlbDMg//rrLzeARx99tOajjz6aWygAnDhxwkXJQxEREZH8UfJQRERERDI5O9vM89m0YcMGz9GjR9dwdnY248aNO/rAAw/EBwUFJXt5eaU5OTkxfPjwqlOmTKlkjLF5yrGT09V30MmrPqvU1FQAOnTokODn55djGXVW5cuXT813xyIiIiI3OSUPRUREROSaLFy4sKwxhsGDB/89YcKEk1fW//nnnzaXKxeGypUrX4qKinJ/8sknTz300EM5ll6LiIiIyLXRgSkiIiIick3i4uJcAAICAnIsLz527JjLpk2bfHK2Khz33nvvGYDFixeXvVFjioiIiNwMlDwUERERkWtSp06diwCff/65f0JCQubnyri4OKdHHnkk8OzZs/lfA32dnn322VOVKlW6tHTpUv/nnnuuytmzZ3N8zv3ll19KTZ061e9GxSQiIiJSEih5KCIiIiLXZNiwYTGVKlW6dODAAc+goKDb7r333lvuueeeW4KCghrt27fPs0+fPjE3KhZfX9+0FStWHKxcufKld999t3JAQECjFi1a1O7Ro0fQXXfdVatGjRoN69Wrd9tHH31U4UbFJCIiIlISKHkoIiIiItekfPnyqTt27Ih8+OGHYzw9PdMiIiJ89+3bVzo0NDRux44dkdWqVUu+kfHccccdifv27TswZsyY6Bo1alw8cOCA59q1a8v+/PPPnmXLlk0ZOXLk8RkzZhy+kTGJiIiIFHeWMcbRMYiIiIid7d27N6px48Y3bNaXiEhJtXfv3nKNGzcOdHQcIiIijqKZhyIiIiIiIiIiImKTkociIiIiIiIiIiJik5KHIiIiIiIiIiIiYpOShyIiIiIiIiIiImKTkociIiIiIiIiIiJik4ujA7jRypUrZwIDAx0dhoiISKGaNGkSBw4cqOHoOEREirvY2FiaNWtmHB2HiIhIYdq5c2eMMaa8rbqbLnkYGBjIjh07HB2GiIhIoYqMjKRevXqODkNEpNizLEv//yAiIiWeZVmHc6vTsmURERERERERERGxSclDERERERERERERsUnJQxEREREREREREbFJyUMRERERERERERGxSclDERERERERERERsUnJQxEREREREREREbFJyUMRERERERERERGxSclDERERuekEBgZiWVa2l5OTE76+vrRo0YL//Oc/JCUlOTrMGyopKYkZM2bQrVs3qlWrhru7O6VLl+bWW2+lX79+LFu2jLS0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9iz+gwc7OpQSI8/koWVZpy3LKpelaARw0BizzRhzrvBCExEREZGSLiIiAsuy6NChwzX3kZyczPDhw2ncuDEffvghMTExtG7dmj59+nD77bezf/9+xo8fT+3atVm2bJn9gr9BMpLYeSW7RUREbnYmJYXTc+biERyMR6NGjg6nxMjPsuUyZE8yvggsAuILJSIRERERkQLo378/ixcvpnLlykyfPp0ePXpkq09JSWHZsmW8+OKLmn0oIiJSgp395huSo6OpMGa0o0MpUfK952EWlt2jEBERERG5Bp988gmLFy+mTJkybN68maCgoBzXuLi40KdPHzp37szvv//ugChFRESksBljiJ01C9fq1fG+6y5Hh1OiFGjPQxEREZFczZ8PgYHg5JT+df58R0dUaMLCwq66F2Ju+wJmLT969ChhYWFUrlwZT09Pbr/9dpYsWZJ57ebNm+nSpQv+/v54enoSEhLC9u3brxpXSkoKEydOpF69eri7u1OxYkUGDRrEX3/9lWubn3/+mYEDBxIQEICbmxvlypWjS5cufPnll/l+HhmMMcycOZPbb78dDw8PypUrR8+ePfnpp59sXt+hQwdCQkIA2LBhA5ZlZb7ys4zZGMO///1vAF5++WWbicOsvLy8aNq0aY7y1atX07lzZ8qVK0epUqUICAhg0KBBREZG2uwnI8bcZOypGRUVlWv5119/TceOHfH19cXT05OWLVuycuXKbNdnLOnesGEDACEhIdmeUUREBHPnzsWyLEJDQ3ONZ9++fViWRdWqVUlJScn1OhERkeIscfduLu79Cb9BA7GcnR0dTomS35mH/7AsK2N/QxfgMcuyYrNeYIz5j10jExERkeJj/nwYMgQuXEh/f/hw+nuA/v0dF1cRFRUVRXBwMF5eXrRv356jR4+yefNm+vbty4IFC3Bzc+PBBx+kSZMm3HPPPezdu5eIiAhCQkLYtWsXtWvXttnvgw8+yBdffEGHDh1o3LgxW7ZsYc6cOaxdu5bvv/+eOnXqZLt+5cqV9O3bl6SkJBo0aEDbtm05evQo69at48svv+Sll17itddey/d9DRs2jGnTpuHs7Ez79u2pUKEC27Zto0WLFgy2sWl5aGgo7u7urFu3jooVK2ZLgNWtWzfP8X766ScOHTqEZVkMHDgw33FmNXbsWCZOnIiTkxNt2rShatWq/PTTT8yZM4dF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3MeL0UaZamdkffHgg/CPf8B//gNl+VyrQna5yJszB9OwK9HuNBOgFiQF76F1u5qVIW5r1PNRlzjG/32Y5w8e49+dWtdYWftTZj79rCbaGBrHya5WK3/W669XqnjffXejDOunCjIzYd06RRV6rhJ4zTVIAQGkPfEkqXffTZvp09EE10JtG3WB4ijc8CkMuB/MZekFBj0Eu75X8rUOebR+jffjE42uPJQkqbckSRtObMCJONCpp+0/QTywGMiSJOl3SZLmSJK0DEgF/lnW5hkhxPJG+xB+mgxXehFFa9Iw9YtC3zawQcbQhIQQeO21FC5ahGfXb8Q7EpnuvZJPVvmu4GhKDBeEETq+K+6sUrI/34XX1kzykgUEwtDH4eBvkLq+qa1p8eh0MH8+tAp1IbanoekShi7G3NRmnXMs2pFOtt3JvRclAEqlVGP3MBz785Ad5/dyfoRFj16jJq2gFFluPsVTMm1O1CqJMIvurG3uuecefvj6a+675RZatW7N33//zfz58/nrr78wm83cddddrF69mnffrVjoKTg4mC1btjB58mTMZjNLlixhy5YtXH/99Wzbtq3S8OG6oNfrWbZsGVdddRWHDx/mm2++Yfr06axatQpQCpysWrWKZ555hujoaH777TdWr17NsGHD2Lp1K7169fJpHOGV8RY4kbRqVGbFETxmzBgOHz7M+++/z6BBg0hMTOSHH37g999/R5IkJk2axMqVK5k7d26NP1e7du1Yu3YtY8aMoaCggCVLllBYWMj777/P7LnzKM+AI59MD+D1Kv/fsWMHs2bNKt/mfPM18+Z+w8J537J93UoirQF4hSCjsJR9x+0czLSTaXPgcHvPyN02aNAgDh48yKeffsqll15KamoqP/30E0uXLqWoqIjbbruNxYsXs27dugr92rdvz/bt27nv0ccwm0wsW7qElStX0q1bNz766CN+/fVX9JUo8T744APeffddEhISWLNmDWvWrGHAgAFs2bLF579VTfjxxx8ZN24ceXl5zJ07l+nTp7NkyZIz2gmvl1FDhgDQtX17BrVujbewEJXJhK51awI6d0YXF4cmJMTvOKxHsrKUhcCbb4aePWve3+10kLJzG+37DayZM2vbbNCZ4aPl1Q5uHhCNpFVhX31m5fBGJzxcCb2YPRuyq889bFv8C8KhRR19HcIjczjqIHv3rUaWa552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\n",
- "text/plain": [
- "
"
- ]
- },
- "metadata": {
- "tags": [],
- "needs_background": "light"
- }
- }
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "id": "BtHUpG0bS1bq",
- "colab_type": "text"
- },
- "source": [
- "# Part 2: Comparing imputation methods\n",
- "\n",
- "We will compute approximate values for missing values in the time series. This is called [imputation](https://en.wikipedia.org/wiki/Imputation_(statistics)). We can calculate the imputation accuracy by comparing the imputed values with real values. Although in practice we don't hold the original values, we can determine the accuracy by artificial missing points. \n",
- "\n",
- "When sampling in Part 1, we made sure to only keep statistical variables for which some counties had time series holes, and some did not.\n",
- "\n",
- "In this step, we're going to utilize the counties with no missing values. We will artificially remove some of the dates and use the imputation methods to fill them. Then, we can compute the accuracy of each imputation method by comparing with the original (removed) values. Note that we want to compare the $R^2$ scores of different imputation methods and it's crucial to use \n",
- "the same set of missing points for all of methods. \n",
- "\n",
- "In this notebook, the $R^2$ score $\\in [-\\infty , 1]$. Higher $R^2$ scores denote higher accuracy imputation. \n",
- "\n",
- "\n",
- "You can learn more about $R^2$ score in [Coefficient of determination](https://en.wikipedia.org/wiki/Coefficient_of_determination). In short:\n",
- "\n",
- "$$ R^2 = 1- \\frac{\\sum{(y_i - f_i)^2}}{{\\sum{(y_i - mean)^2}}} = 1- \\frac{\\sum{(err)^2}}{{\\sum{(y_i - mean)^2}}} $$\n",
- "\n",
- "where $y_i$s are the actual values and $f_i$ are the imputed ones. $mean = \\frac{1}{n} \\sum{y_i} $."
- ]
- },
- {
- "cell_type": "code",
- "metadata": {
- "id": "wi1jY4lzQ129",
- "colab_type": "code",
- "colab": {
- "base_uri": "https://localhost:8080/",
- "height": 445
- },
- "outputId": "03a0953f-4540-45f9-d9c5-93ffed8f550e"
- },
- "source": [
- "_METHODS = [\"Mean\", \"Median\", \"Spline_d1\", \"Spline_d2\", \"Spline_d3\", \"Spline_d4\", \n",
- " \"Time\", \"Spline_d5\", \"InterpolateLinear\", \"Pchip\", \"Krogh\", \"InterpolateAkima\"]\n",
- "\n",
- "import warnings; warnings.simplefilter('ignore')\n",
- "score = artificial_score_generator(random_stats)\n",
- "pd.set_option('display.float_format', lambda x: '%.2f' % x)\n",
- "score"
- ],
- "execution_count": null,
- "outputs": [
- {
- "output_type": "execute_result",
- "data": {
- "text/html": [
- "
\n",
- "\n",
- "
\n",
- " \n",
- "
\n",
- "
\n",
- "
method
\n",
- "
Average of 20 rounds for Count_Death_85Years_EndocrineNutritionalMetabolicDiseases_Female
\n",
- "
Average of 20 rounds for Count_Death_55To64Years_Male_White
\n",
- "
Average of 20 rounds for RetailDrugDistribution_DrugDistribution_PoppyStrawConcentrate
\n",
- "
Average of 20 rounds for Count_Death_MentalBehaviouralDisorders_White
\n",
- "
\n",
- " \n",
- " \n",
- "
\n",
- "
0
\n",
- "
Time
\n",
- "
0.89
\n",
- "
0.88
\n",
- "
0.67
\n",
- "
0.94
\n",
- "
\n",
- "
\n",
- "
1
\n",
- "
InterpolateLinear
\n",
- "
0.89
\n",
- "
0.88
\n",
- "
0.67
\n",
- "
0.94
\n",
- "
\n",
- "
\n",
- "
2
\n",
- "
Spline_d1
\n",
- "
0.86
\n",
- "
0.87
\n",
- "
0.74
\n",
- "
0.92
\n",
- "
\n",
- "
\n",
- "
3
\n",
- "
Mean
\n",
- "
0.76
\n",
- "
0.77
\n",
- "
0.79
\n",
- "
0.77
\n",
- "
\n",
- "
\n",
- "
4
\n",
- "
Median
\n",
- "
0.75
\n",
- "
0.75
\n",
- "
0.78
\n",
- "
0.75
\n",
- "
\n",
- "
\n",
- "
5
\n",
- "
Pchip
\n",
- "
0.74
\n",
- "
0.79
\n",
- "
-0.37
\n",
- "
0.88
\n",
- "
\n",
- "
\n",
- "
6
\n",
- "
Spline_d2
\n",
- "
-0.35
\n",
- "
0.13
\n",
- "
0.71
\n",
- "
0.50
\n",
- "
\n",
- "
\n",
- "
7
\n",
- "
InterpolateAkima
\n",
- "
-23.53
\n",
- "
-20.34
\n",
- "
-22.56
\n",
- "
-19.25
\n",
- "
\n",
- "
\n",
- "
8
\n",
- "
Spline_d5
\n",
- "
-101.00
\n",
- "
-101.00
\n",
- "
-101.00
\n",
- "
-101.00
\n",
- "
\n",
- "
\n",
- "
9
\n",
- "
Spline_d3
\n",
- "
-101.00
\n",
- "
-101.00
\n",
- "
-0.04
\n",
- "
-101.00
\n",
- "
\n",
- "
\n",
- "
10
\n",
- "
Krogh
\n",
- "
-101.00
\n",
- "
-101.00
\n",
- "
-101.00
\n",
- "
-101.00
\n",
- "
\n",
- "
\n",
- "
11
\n",
- "
Spline_d4
\n",
- "
-101.00
\n",
- "
-101.00
\n",
- "
-11.02
\n",
- "
-101.00
\n",
- "
\n",
- " \n",
- "
\n",
- "
"
- ],
- "text/plain": [
- " method ... Average of 20 rounds for Count_Death_MentalBehaviouralDisorders_White\n",
- "0 Time ... 0.94 \n",
- "1 InterpolateLinear ... 0.94 \n",
- "2 Spline_d1 ... 0.92 \n",
- "3 Mean ... 0.77 \n",
- "4 Median ... 0.75 \n",
- "5 Pchip ... 0.88 \n",
- "6 Spline_d2 ... 0.50 \n",
- "7 InterpolateAkima ... -19.25 \n",
- "8 Spline_d5 ... -101.00 \n",
- "9 Spline_d3 ... -101.00 \n",
- "10 Krogh ... -101.00 \n",
- "11 Spline_d4 ... -101.00 \n",
- "\n",
- "[12 rows x 5 columns]"
- ]
- },
- "metadata": {
- "tags": []
- },
- "execution_count": 5
- }
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "id": "xvZASllBtHUH",
- "colab_type": "text"
- },
- "source": [
- "You can sort the above **`score`** table by different statistical variables. The current table is sorted by $R^2$ values of the first variable. "
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "id": "yvJSW-YF-J4K",
- "colab_type": "text"
- },
- "source": [
- "# Conclusion\n",
- "\n",
- "In this notebook, we used Data Commons to survey the missing value patterns across different time series. In our experiments, a time series might have random disjoint missing values, a missing interval (consecutive dates), or even multiple missing intervals. \n",
- "\n",
- "\n",
- "We used cross-validation to evaluate multiple imputation methods. The method with the highest $R^2$ score may differ by time series. Although the best method might change across runs, in our experiments, we observed that in most cases **time**, **interpolate linear**, **mean**, and **median** perform well. Note that the linear method \"ignores the index and treat the values as equally spaced.\" ([pandas.Series.interpolate](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.interpolate.html)) thus, for equally spaced data series time and linear will have the same result. \n",
- "\n",
- "While performing imputation, we recommend to avoid any method with a negative score for a time series."
- ]
- }
- ]
-}
\ No newline at end of file
diff --git a/README.md b/README.md
index 018bbb5c..08d1f9d1 100644
--- a/README.md
+++ b/README.md
@@ -2,123 +2,19 @@
This is a Python library for accessing data in the Data Commons Graph.
-> See also: [Data Commons Pandas API](datacommons_pandas/README.md).
-
-
-To get started, install this package from pip.
-
- pip install datacommons
-
-Once the package is installed, import `datacommons`.
-
- import datacommons as dc
-
-If you would like to provide an API key, follow the steps in
-[Setting up access to the Data Commons API](https://docs.datacommons.org/api/setup.html),
-add the following line to your code:
-
- dc.set_api_key('YOUR-API-KEY')
-
-Data Commons *does not charge* users, but uses the API key for
-understanding API usage.
-
-For more detail on getting started with the API, please visit our
-[API Overview](http://docs.datacommons.org/api/).
-
-When you are ready to use the API, you can refer to `datacommons/examples` for
-examples on how to use this package to perform various tasks. More tutorials and
-documentation can be found on our [tutorials page](https://datacommons.org/colab)!
+See the `datacommons-client` [README](datacommons_client/README.md) for details on installation and usage.
## About Data Commons
[Data Commons](https://datacommons.org/) is an open knowledge repository that
provides a unified view across multiple public data sets and statistics. You can
view what [datasets](https://datacommons.org/datasets) are currently ingested
-and browse the graph using our [browser](https://browser.datacommons.org/).
+and browse the graph using our [browser](https://datacommons.org/browser).
## License
Apache 2.0
-## Development
-
-The Python API currently supports `python>=2.7`.
-
-To test, run:
-
-```
-$ ./run_tests_local.sh
-```
-
-To debug the continuous integration tests, run:
-
-```
-$ cloud-build-local --config=cloudbuild.yaml --dryrun=false .
-```
-
-Both commands will run the same set of tests.
-
-To run the examples:
-
-```
-$ python -m datacommons.examples.XXX
-```
-
-where XXX is the module you want to run.
-
-## Release
-
-Note: Always release `datacommons_pandas` when `datacommons` is released.
-
-**If this is your first time releasing to PyPI**, please review the PyPI guide
-starting from the
-[setup section](https://packaging.python.org/tutorials/packaging-projects/#creating-setup-py).
-
-### Release to Test PyPI
-
-1. In [setup_datacommons.py](setup_datacommons.py) and
- [setup_datacommons_pandas.py](setup_datacommons_pandas.py):
- - Append "-USERNAME" to the package "NAME". For example,
- `NAME = 'foo_package-janedoe123'`.
- - Increment the "VERSION" codes to something that has not been used in your
- test project. This will not affect the production PyPI versioning.
-1. Build the dists:
- ```bash
- rm dist/*
- python3 -m pip install --user --upgrade setuptools wheel
- python3 setup_datacommons.py sdist bdist_wheel
- python3 setup_datacommons_pandas.py sdist bdist_wheel
- ```
-1. Release the dists to TestPyPI:
- ```bash
- python3 -m pip install --user --upgrade twine
- python3 -m twine upload --repository testpypi dist/*
- ```
-
-### Release to Production PyPI
-1. In [setup_datacommons.py](setup_datacommons.py) and
- [setup_datacommons_pandas.py](setup_datacommons_pandas.py):
- - Revert the package name to `datacommons` and `datacommons_pandas`
- - Update and double check "VERSION"
-1. Update [CHANGELOG.md](CHANGELOG.md) and
- [datacommons_pandas/CHANGELOG.md](datacommons_pandas/CHANGELOG.md)
-1. Build the dists:
- ```bash
- rm dist/*
- python3 -m pip install --user --upgrade setuptools wheel
- python3 setup_datacommons.py sdist bdist_wheel
- python3 setup_datacommons_pandas.py sdist bdist_wheel
- ```
-1. Release the dists to PyPI:
- ```bash
- python3 -m pip install --user --upgrade twine
- twine upload dist/*
- ```
-
## Support
-For general questions or issues about the API, please open an issue on our
-[issues](https://github.com/google/datacommons/issues) page. For all other
-questions, please send an email to `support@datacommons.org`.
-
-**Note** - This is not an officially supported Google product.
+For questions, please send an email to `support@datacommons.org`.
diff --git a/cloudbuild.yaml b/cloudbuild.yaml
index 1d09ed16..40b3fe0e 100644
--- a/cloudbuild.yaml
+++ b/cloudbuild.yaml
@@ -1,8 +1,7 @@
steps:
-
-- id: api_python
- name: python:3.7-slim
- entrypoint: /bin/sh
- args:
- - -c
- - 'pip3 install -r requirements.txt && python3 -m pytest'
+ - id: api_python
+ name: python:3.10-slim
+ entrypoint: /bin/bash
+ args:
+ - -c
+ - "./run_test.sh -s && hatch run test:all"
diff --git a/datacommons/README.md b/datacommons/README.md
new file mode 100644
index 00000000..218c00d4
--- /dev/null
+++ b/datacommons/README.md
@@ -0,0 +1 @@
+**DEPRECATED: This library has been deprecated. Please migrate to the [datacommons_client](https://pypi.org/project/datacommons-client/) library. For help on translating your requests, see the [Migration guide](https://docs.datacommons.org/api/python/v2/migration.html).**
diff --git a/datacommons/__init__.py b/datacommons/__init__.py
deleted file mode 100644
index 93d61ab4..00000000
--- a/datacommons/__init__.py
+++ /dev/null
@@ -1,34 +0,0 @@
-# Copyright 2017 Google Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-################################## IMPORTANT #################################
-# All user-facing functions in this package must be symlinked to the #
-# datacommons_pandas pkg. This is so that users do not need to import both #
-# libraries for pd support. Please keep the below imports in sync with the #
-# __init__.py in the datacommons_pandas/ dir, and add a symlink when #
-# creating a new file. #
-# TODO: https://github.com/datacommonsorg/api-python/issues/149 #
-##############################################################################
-
-# Data Commons SPARQL query support
-from datacommons.query import query
-
-# Data Commons Python API
-from datacommons.core import get_property_labels, get_property_values, get_triples
-from datacommons.places import get_places_in, get_related_places, get_stats
-from datacommons.populations import get_populations, get_observations, get_pop_obs, get_place_obs
-from datacommons.stat_vars import get_stat_value, get_stat_series, get_stat_all
-
-# Other utilities
-from datacommons.utils import set_api_key
diff --git a/datacommons/core.py b/datacommons/core.py
deleted file mode 100644
index 75060388..00000000
--- a/datacommons/core.py
+++ /dev/null
@@ -1,258 +0,0 @@
-# Copyright 2017 Google Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-""" Data Commons Python API Core.
-
-Provides primitive operations for working with collections of nodes. For a
-collection of nodes identified by their dcids, this submodule implements the
-following:
-
-- Getting all property labels
-- Getting all property values
-- Getting all triples
-"""
-
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
-from collections import defaultdict
-
-import datacommons.utils as utils
-
-# ----------------------------- WRAPPER FUNCTIONS -----------------------------
-
-
-def get_property_labels(dcids, out=True):
- """ Returns the labels of properties defined for the given :code:`dcids`.
-
- Args:
- dcids (:obj:`iterable` of :obj:`str`): A list of nodes identified by their
- dcids.
- out (:obj:`bool`, optional): Whether or not the property points away from
- the given list of nodes.
-
- Returns:
- A :obj:`dict` mapping dcids to lists of property labels. If `out` is `True`,
- then property labels correspond to edges directed away from given nodes.
- Otherwise, they correspond to edges directed towards the given nodes.
-
- Raises:
- ValueError: If the payload returned by the Data Commons REST API is
- malformed.
-
- Examples:
- To get all outgoing property labels for
- `California `_ and
- `Colorado `_, we can write
- the following.
-
- >>> get_property_labels(['geoId/06', 'geoId/08'])
- {
- "geoId/06": [
- "containedInPlace",
- "geoId",
- "kmlCoordinates",
- "name",
- "provenance",
- "typeOf"
- ],
- "geoId/08",: [
- "containedInPlace",
- "geoId",
- "kmlCoordinates",
- "name",
- "provenance",
- "typeOf"
- ]
- }
-
- We can also get incoming property labels by setting `out=False`.
-
- >>> get_property_labels(['geoId/06', 'geoId/08'], out=False)
- {
- "geoId/06": [
- "addressRegion",
- "containedInPlace",
- "location",
- "overlapsWith"
- ],
- "geoId/08",: [
- "addressRegion",
- "containedInPlace",
- "location",
- "overlapsWith"
- ]
- }
- """
- # Generate the GetProperty query and send the request
- dcids = filter(lambda v: v==v, dcids) # Filter out NaN values
- dcids = list(dcids)
- url = utils._API_ROOT + utils._API_ENDPOINTS['get_property_labels']
- payload = utils._send_request(url, req_json={'dcids': dcids})
-
- # Return the results based on the orientation
- results = {}
- for dcid in dcids:
- if out:
- results[dcid] = payload[dcid]['outLabels']
- else:
- results[dcid] = payload[dcid]['inLabels']
- return results
-
-
-def get_property_values(dcids,
- prop,
- out=True,
- value_type=None,
- limit=utils._MAX_LIMIT):
- """ Returns property values of given :code:`dcids` along the given property.
-
- Args:
- dcids (:obj:`iterable` of :obj:`str`): dcids to get property values for.
- prop (:obj:`str`): The property to get property values for.
- out (:obj:`bool`, optional): A flag that indicates the property is directed
- away from the given nodes when set to true.
- value_type (:obj:`str`, optional): A type to filter returned property values
- by.
- limit (:obj:`int`, optional): The maximum number of property values returned
- aggregated over all given nodes.
-
- Returns:
- Returned property values are formatted as a :obj:`dict` from a given dcid
- to a list of its property values.
-
- Raises:
- ValueError: If the payload returned by the Data Commons REST API is
- malformed.
-
- Examples:
- We would like to get the `name` of a list of states specified by their dcid:
- `geoId/06 `_,
- `geoId/21 `_, and
- `geoId/24 `_
-
- First, let's try specifying the :code:`dcids` as a :obj:`list` of
- :obj:`str`.
-
- >>> get_property_values(["geoId/06", "geoId/21", "geoId/24"], "name")
- {
- "geoId/06": ["California"],
- "geoId/21": ["Kentucky"],
- "geoId/24": ["Maryland"],
- }
- """
- # Convert the dcids field and format the request to GetPropertyValue
- dcids = filter(lambda v: v==v, dcids) # Filter out NaN values
- dcids = list(dcids)
- if out:
- direction = 'out'
- else:
- direction = 'in'
-
- req_json = {
- 'dcids': dcids,
- 'property': prop,
- 'limit': limit,
- 'direction': direction
- }
- if value_type:
- req_json['value_type'] = value_type
-
- # Send the request
- url = utils._API_ROOT + utils._API_ENDPOINTS['get_property_values']
- payload = utils._send_request(url, req_json=req_json)
-
- # Create the result format for when dcids is provided as a list.
- unique_results = defaultdict(set)
- for dcid in dcids:
- # Get the list of nodes based on the direction given.
- nodes = []
- if out:
- if dcid in payload and 'out' in payload[dcid]:
- nodes = payload[dcid]['out']
- else:
- if dcid in payload and 'in' in payload[dcid]:
- nodes = payload[dcid]['in']
-
- # Add nodes to unique_results if it is not empty
- for node in nodes:
- if 'dcid' in node:
- unique_results[dcid].add(node['dcid'])
- elif 'value' in node:
- unique_results[dcid].add(node['value'])
-
- # Make sure each dcid is in the results dict, and convert all sets to lists.
- results = {dcid: sorted(list(unique_results[dcid])) for dcid in dcids}
-
- return results
-
-
-def get_triples(dcids, limit=utils._MAX_LIMIT):
- """ Returns all triples associated with the given :code:`dcids`.
-
- A knowledge graph can be described as a collection of `triples` which are
- 3-tuples that take the form `(s, p, o)`. Here `s` and `o` are nodes in the
- graph called the *subject* and *object* respectively while `p` is the property
- label of a directed edge from `s` to `o` (sometimes also called the
- *predicate*).
-
- Args:
- dcids (:obj:`iterable` of :obj:`str`): A list of dcids to get triples for.
- limit (:obj:`int`, optional): The maximum total number of triples to get.
-
- Returns:
- A :obj:`dict` mapping dcids to a :obj:`list` of triples `(s, p, o)` where
- `s`, `p`, and `o` are instances of :obj:`str` and either the subject
- or object is the mapped dcid.
-
- Raises:
- ValueError: If the payload returned by the Data Commons REST API is
- malformed.
-
- Examples:
- We would like to get five triples associated with
- `California `_
-
- >>> get_triples(["geoId/06"], limit=5)
- {
- "geoId/06": [
- ("geoId/06", "name", "California"),
- ("geoId/06", "typeOf", "State"),
- ("geoId/06", "geoId", "06"),
- ("geoId/0687056", "containedInPlace", "geoId/06"),
- ("geoId/0686440", "containedInPlace", "geoId/06")
- ]
- }
- """
- # Generate the GetTriple query and send the request.
- dcids = filter(lambda v: v==v, dcids) # Filter out NaN values
- dcids = list(dcids)
- url = utils._API_ROOT + utils._API_ENDPOINTS['get_triples']
- payload = utils._send_request(url, req_json={'dcids': dcids, 'limit': limit})
-
- # Create a map from dcid to list of triples.
- results = defaultdict(list)
- for dcid in dcids:
- # Make sure each dcid is mapped to an empty list.
- results[dcid]
-
- # Add triples as appropriate
- for t in payload[dcid]:
- if 'objectId' in t:
- results[dcid].append(
- (t['subjectId'], t['predicate'], t['objectId']))
- elif 'objectValue' in t:
- results[dcid].append(
- (t['subjectId'], t['predicate'], t['objectValue']))
- return dict(results)
diff --git a/datacommons/examples/__init__.py b/datacommons/examples/__init__.py
deleted file mode 100644
index 7c07b241..00000000
--- a/datacommons/examples/__init__.py
+++ /dev/null
@@ -1,13 +0,0 @@
-# Copyright 2017 Google Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
diff --git a/datacommons/examples/core.py b/datacommons/examples/core.py
deleted file mode 100644
index 6eb4aa10..00000000
--- a/datacommons/examples/core.py
+++ /dev/null
@@ -1,56 +0,0 @@
-# Copyright 2017 Google Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-""" Data Commons Python API examples.
-
-Basic demo for get_property_labels, get_property_values, and get_triples.
-"""
-
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
-import datacommons as dc
-
-def main():
- # Set the dcid to be that of Santa Clara County.
- dcids = ['geoId/06085', 'dc/p/zsb968m3v1f97']
-
- # Print all incoming and outgoing properties from Santa Clara County.
- print('Property Labels for Santa Clara County')
- in_labels = dc.get_property_labels(dcids)
- out_labels = dc.get_property_labels(dcids, out=False)
- print('> Printing properties for {}'.format(dcids))
- print('> Incoming properties: {}'.format(in_labels))
- print('> Outgoing properties: {}'.format(out_labels))
-
- # Print all property values for "containedInPlace" for Santa Clara County.
- print('Property Values for "containedInPlace" of Santa Clara County')
- prop_vals = dc.get_property_values(
- dcids, 'containedInPlace', out=False, value_type='City')
- print('> Cities contained in {}'.format(dcids))
- for dcid in dcids:
- for city_dcid in prop_vals[dcid]:
- print(' - {}'.format(city_dcid))
-
- # Print the first 10 triples associated with Santa Clara County
- print('Triples for Santa Clara County')
- triples = dc.get_triples(dcids)
- for dcid in dcids:
- print('> Triples for {}'.format(dcid))
- for s, p, o in triples[dcid][:5]:
- print(' - ("{}", {}, "{}")'.format(s, p, o))
-
-
-if __name__ == '__main__':
- main()
diff --git a/datacommons/examples/places.py b/datacommons/examples/places.py
deleted file mode 100644
index ee9c71ec..00000000
--- a/datacommons/examples/places.py
+++ /dev/null
@@ -1,73 +0,0 @@
-# Copyright 2017 Google Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-""" Data Commons Python API examples.
-
-Basic demo for get_places_in
-"""
-
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
-import datacommons as dc
-
-
-def main():
- # Create a list of dcids for Santa Clara and Montgomery County.
- sc, mc = 'geoId/06085', 'geoId/24031'
- dcids = [sc, mc]
-
- # Get all CensusTracts in these two counties.
- print('Get Census Tracts')
- tracts = dc.get_places_in(dcids, 'CensusTract')
- if sc in tracts:
- print('> 10 CensusTracts in Santa Clara County')
- for dcid in tracts[sc][:10]:
- print(' - {}'.format(dcid))
- if mc in tracts:
- print('> 10 CensusTracts in Montgomery County')
- for dcid in tracts[mc][:10]:
- print(' - {}'.format(dcid))
-
- # Get place stats.
- print('Get place stats -- all')
- stats = dc.get_stats(['geoId/05', 'geoId/06', 'dc/madDcid'], 'dc/0hyp6tkn18vcb', obs_dates='all')
- print(stats)
-
- print('Get place stats -- latest')
- stats = dc.get_stats(['geoId/05', 'geoId/06', 'dc/madDcid'], 'dc/0hyp6tkn18vcb')
- print(stats)
-
- print('Get place stats -- 2014')
- stats = dc.get_stats(['geoId/05', 'geoId/06', 'dc/madDcid'], 'dc/0hyp6tkn18vcb', obs_dates=['2014'])
- print(stats)
-
- print('Get place stats -- 2014 badly formatted')
- stats = dc.get_stats(['geoId/05', 'geoId/06', 'dc/madDcid'], 'dc/0hyp6tkn18vcb', obs_dates='2014')
- print(stats)
-
- print('Get place stats -- 2015-2016')
- stats = dc.get_stats(['geoId/05', 'geoId/06', 'dc/madDcid'], 'dc/0hyp6tkn18vcb', obs_dates=['2015', '2016'])
- print(stats)
-
- # Get related places.
-# TODO(*): Fix the related places example.
-# print('Get related places')
-# related_places = dc.get_related_places(['geoId/06085'], 'Person', 'count',
-# 'CensusACS5yrSurvey', "measuredValue", {"gender": "Female"})
-# print(related_places)
-
-
-if __name__ == '__main__':
- main()
diff --git a/datacommons/examples/populations.py b/datacommons/examples/populations.py
deleted file mode 100644
index 3804dbf3..00000000
--- a/datacommons/examples/populations.py
+++ /dev/null
@@ -1,56 +0,0 @@
-# Copyright 2017 Google Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-""" Data Commons Python API examples.
-
-Basic demo for get_populations and get_observations.
-"""
-
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
-import datacommons as dc
-import pprint
-import json
-
-
-def main():
- # Create a list of dcids for California, Kentucky, and Maryland
- ca, ky, md = 'geoId/06', 'geoId/21', 'geoId/24'
- dcids = [ca, ky, md]
-
- # Get the population of all employed individuals in the above states.
- print('Get Populations for All Employed Individuals')
- employed = dc.get_populations(dcids, 'Person', constraining_properties={
- 'employment': 'BLS_Employed'})
- print(json.dumps(employed, indent=2))
-
- # Get the count for all male / females for the above states in 2016
- print('Get Population Counts for Employed Individuals in Maryland')
- pop_dcids = [employed[md]]
- obs = dc.get_observations(pop_dcids,
- 'count',
- 'measuredValue',
- '2018-12',
- observation_period='P1M',
- measurement_method='BLSSeasonallyAdjusted')
- print(json.dumps(obs, indent=2))
-
- # Get all population and observation data of Mountain View.
- print('Get Mountain View population and observation')
- popobs = dc.get_pop_obs("geoId/0649670")
- pprint.pprint(popobs)
-
-if __name__ == '__main__':
- main()
diff --git a/datacommons/examples/query.py b/datacommons/examples/query.py
deleted file mode 100644
index 8be1bfe4..00000000
--- a/datacommons/examples/query.py
+++ /dev/null
@@ -1,46 +0,0 @@
-# Copyright 2017 Google Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-""" Data Commons Python API examples.
-
-Example on how to use the Client API SPARQL query wrapper.
-"""
-
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
-import datacommons as dc
-
-
-def main():
- # Create a SPARQL query querying for the name of some states
- query = ('''
-SELECT ?name ?dcid
-WHERE {
- ?a typeOf Place .
- ?a name ?name .
- ?a dcid ("geoId/06" "geoId/21" "geoId/24") .
- ?a dcid ?dcid
-}
-''')
- print('> Issuing query.\n{}'.format(query))
-
- # Iterate through all the rows in the results.
- print('> Printing results.\n')
- for row in dc.query(query_string=query):
- print(' {}'.format(row))
-
-
-if __name__ == '__main__':
- main()
diff --git a/datacommons/examples/stat_vars.py b/datacommons/examples/stat_vars.py
deleted file mode 100644
index 51218b98..00000000
--- a/datacommons/examples/stat_vars.py
+++ /dev/null
@@ -1,232 +0,0 @@
-# Copyright 2020 Google Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-"""Basic examples for StatisticalVariable-based Data Commons API functions."""
-
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
-import datacommons as dc
-import pprint
-
-
-def main():
- param_sets = [
- {
- 'place': 'geoId/06085',
- 'stat_var': "Count_Person",
- },
- {
- 'place': 'geoId/06085',
- 'stat_var': "Count_Person",
- 'date': '2018',
- },
- {
- 'place': 'geoId/06085',
- 'stat_var': "Count_Person",
- 'date': '2018',
- 'measurement_method': 'CensusACS5yrSurvey',
- },
- {
- 'place': 'geoId/06085',
- 'stat_var': 'UnemploymentRate_Person',
- },
- {
- 'place': 'geoId/06085',
- 'stat_var': 'UnemploymentRate_Person',
- 'observation_period': 'P1Y',
- },
- {
- 'place': 'geoId/06085',
- 'stat_var': 'UnemploymentRate_Person',
- 'observation_period': 'P1Y',
- 'measurement_method': 'BLSSeasonallyUnadjusted',
- },
- {
- 'place':
- 'nuts/HU22',
- 'stat_var':
- 'Amount_EconomicActivity_GrossDomesticProduction_Nominal',
- },
- {
- 'place':
- 'nuts/HU22',
- 'stat_var':
- 'Amount_EconomicActivity_GrossDomesticProduction_Nominal',
- 'observation_period':
- 'P1Y',
- 'unit':
- 'PurchasingPowerStandard'
- },
- ]
-
- def call_str(pvs):
- """Helper function to print the minimal call string."""
- s = "'{}', '{}'".format(pvs.get('place'), pvs.get('stat_var'))
- if pvs.get('measurement_method'):
- s += ", measurement_method='{}'".format(
- pvs.get('measurement_method'))
- if pvs.get('observation_period'):
- s += ", observation_period='{}'".format(
- pvs.get('observation_period'))
- if pvs.get('unit'):
- s += ", unit='{}'".format(pvs.get('unit'))
- if pvs.get('scaling_factor'):
- s += ", scaling_factor={}".format(pvs.get('scaling_factor'))
- return s
-
- for pvs in param_sets:
- print('\nget_stat_value({})'.format(call_str(pvs)))
- print(
- '>>> ',
- dc.get_stat_value(pvs.get('place'),
- pvs.get('stat_var'),
- date=pvs.get('date'),
- measurement_method=pvs.get('measurement_method'),
- observation_period=pvs.get('observation_period'),
- unit=pvs.get('unit'),
- scaling_factor=pvs.get('scaling_factor')))
- for pvs in param_sets:
- pvs.pop('date', None)
- print('\nget_stat_series({})'.format(call_str(pvs)))
- print(
- '>>> ',
- dc.get_stat_series(pvs.get('place'),
- pvs.get('stat_var'),
- measurement_method=pvs.get('measurement_method'),
- observation_period=pvs.get('observation_period'),
- unit=pvs.get('unit'),
- scaling_factor=pvs.get('scaling_factor')))
-
- pp = pprint.PrettyPrinter(indent=4)
- print(
- '\nget_stat_all(["geoId/06085", "country/FRA"], ["Median_Age_Person", "Count_Person"])'
- )
- print('>>> ')
- pp.pprint(
- dc.get_stat_all(["geoId/06085", "country/FRA"],
- ["Median_Age_Person", "Count_Person"]))
-
- print(
- '\nget_stat_all(["badPlaceId", "country/FRA"], ["Median_Age_Person", "Count_Person"])'
- )
- print('>>> ')
- pp.pprint(
- dc.get_stat_all(["badPlaceId", "country/FRA"],
- ["Median_Age_Person", "Count_Person"]))
-
-
- print('\nWhen no data for get_stat_value')
- pp.pprint(dc.get_stat_value('foooo', 'barrrr'))
-
- print('\nWhen no data for get_stat_series')
- pp.pprint(dc.get_stat_series('foobarbar', 'barfoo'))
-
- print('\nSTRESS TEST FOR GET_STAT_ALL')
- try:
- dc.get_stat_all(
- dc.get_places_in(['country/USA'], 'County')['country/USA'], [
- 'Count_Person', 'LandAreaSqMeter',
- 'PopulationDensityPerSqMeter',
- 'Count_Person_BlackOrAfricanAmericanAlone',
- 'PercentBlackOrAfricanAmericanAlone', 'Count_Person_Female',
- 'Count_Person_Male',
- 'Count_Person_AmericanIndianAndAlaskaNativeAlone',
- 'Count_Person_AmericanIndianAndAlaskaNativeAloneOrInCombinationWithOneOrMoreOtherRaces',
- 'Count_Person_AmericanIndianOrAlaskaNativeAlone',
- 'Count_Person_AsianAlone',
- 'Count_Person_AsianAloneOrInCombinationWithOneOrMoreOtherRaces',
- 'Count_Person_BlackOrAfricanAmericanAloneOrInCombinationWithOneOrMoreOtherRaces',
- 'Count_Person_HispanicOrLatino',
- 'Count_Person_NativeHawaiianAndOtherPacificIslanderAlone',
- 'Count_Person_NativeHawaiianAndOtherPacificIslanderAloneOrInCombinationWithOneOrMoreOtherRaces',
- 'Count_Person_NativeHawaiianOrOtherPacificIslanderAlone',
- 'Count_Person_SomeOtherRaceAlone',
- 'Count_Person_SomeOtherRaceAloneOrInCombinationWithOneOrMoreOtherRaces',
- 'Count_Person_TwoOrMoreRaces', 'Count_Person_WhiteAlone',
- 'Count_Person_WhiteAloneNotHispanicOrLatino',
- 'Count_Person_WhiteAloneOrInCombinationWithOneOrMoreOtherRaces',
- 'Count_Person_Upto5Years', 'Count_Person_Upto18Years',
- 'Count_Person_65OrMoreYears', 'Count_Person_75OrMoreYears',
- 'Count_Person_ForeignBorn',
- 'Count_Person_USCitizenByNaturalization',
- 'Count_Person_NotAUSCitizen', 'Count_Person_Nonveteran',
- 'Count_Person_Veteran', 'Count_Person_NotWorkedFullTime',
- 'Count_Person_WorkedFullTime', 'Count_Person_Employed',
- 'Count_Person_Unemployed', 'Count_Person_InLaborForce',
- 'Count_Person_IncomeOf10000To14999USDollar',
- 'Count_Person_IncomeOf15000To24999USDollar',
- 'Count_Person_IncomeOf25000To34999USDollar',
- 'Count_Person_IncomeOf35000To49999USDollar',
- 'Count_Person_IncomeOf50000To64999USDollar',
- 'Count_Person_IncomeOf65000To74999USDollar',
- 'Count_Person_IncomeOf75000OrMoreUSDollar',
- 'Count_Person_IncomeOfUpto9999USDollar',
- 'Count_Person_EnrolledInSchool',
- 'Count_Person_NotEnrolledInSchool',
- 'Count_Person_EnrolledInCollegeUndergraduateYears',
- 'Count_Person_EnrolledInGrade1ToGrade4',
- 'Count_Person_EnrolledInGrade5ToGrade8',
- 'Count_Person_EnrolledInGrade9ToGrade12',
- 'Count_Person_EnrolledInKindergarten',
- 'Count_Person_EnrolledInNurserySchoolPreschool',
- 'Count_Person_GraduateOrProfessionalSchool',
- 'Count_Person_EducationalAttainment10ThGrade',
- 'Count_Person_EducationalAttainment11ThGrade',
- 'Count_Person_EducationalAttainment12ThGradeNoDiploma',
- 'Count_Person_EducationalAttainment1StGrade',
- 'Count_Person_EducationalAttainment2NdGrade',
- 'Count_Person_EducationalAttainment3RdGrade',
- 'Count_Person_EducationalAttainment4ThGrade',
- 'Count_Person_EducationalAttainment5ThGrade',
- 'Count_Person_EducationalAttainment6ThGrade',
- 'Count_Person_EducationalAttainment7ThGrade',
- 'Count_Person_EducationalAttainment8ThGrade',
- 'Count_Person_EducationalAttainment9ThGrade',
- 'Count_Person_EducationalAttainmentAssociatesDegree',
- 'Count_Person_EducationalAttainmentBachelorsDegree',
- 'Count_Person_EducationalAttainmentBachelorsDegreeOrHigher',
- 'Count_Person_EducationalAttainmentDoctorateDegree',
- 'Count_Person_EducationalAttainmentGedOrAlternativeCredential',
- 'Count_Person_EducationalAttainmentKindergarten',
- 'Count_Person_EducationalAttainmentMastersDegree',
- 'Count_Person_EducationalAttainmentNoSchoolingCompleted',
- 'Count_Person_EducationalAttainmentNurserySchool',
- 'Count_Person_EducationalAttainmentPrimarySchool',
- 'Count_Person_EducationalAttainmentProfessionalSchoolDegree',
- 'Count_Person_EducationalAttainmentRegularHighSchoolDiploma',
- 'Count_Person_EducationalAttainmentSomeCollege1OrMoreYearsNoDegree',
- 'Count_Person_EducationalAttainmentSomeCollegeLessThan1Year',
- 'Count_Person_Divorced', 'Count_Person_MarriedAndNotSeparated',
- 'Count_Person_NeverMarried', 'Count_Person_Separated',
- 'Count_Person_Widowed', 'Count_Person_NowMarried',
- 'Count_Person_AbovePovertyLevelInThePast12Months',
- 'Count_Person_BelowPovertyLevelInThePast12Months',
- 'Percent_Person_20OrMoreYears_WithDiabetes',
- 'Percent_Person_20OrMoreYears_Obesity',
- 'Percent_Person_20OrMoreYears_PhysicalInactivity',
- 'Percent_Person_Upto64Years_NoHealthInsurance',
- 'Median_Age_Person', 'Median_Income_Person', 'Count_Death',
- 'Count_Death_CertainInfectiousParasiticDiseases',
- 'Count_Death_DiseasesOfBloodAndBloodFormingOrgansAndImmuneDisorders',
- 'Count_Death_DiseasesOfTheRespiratorySystem'
- ])
- except ValueError:
- print('Stress test for get_stat_all FAILED!')
- else:
- print('Stress test for get_stat_all succeeded.')
-
-
-if __name__ == '__main__':
- main()
diff --git a/datacommons/places.py b/datacommons/places.py
deleted file mode 100644
index b04badc5..00000000
--- a/datacommons/places.py
+++ /dev/null
@@ -1,250 +0,0 @@
-# Copyright 2017 Google Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-""" Data Commons Python API Places Module.
-
-Provides convenience functions for working with Places in the Data Commons
-Graph. This submodule implements the ability to access :obj:`Place`'s
-within a collection of nodes identified by dcid.
-"""
-
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
-import datacommons.utils as utils
-
-
-def get_places_in(dcids, place_type):
- """ Returns :obj:`Place`s contained in :code:`dcids` of type
- :code:`place_type`.
-
- Args:
- dcids (:obj:`iterable` of :obj:`str`): Dcids to get contained in places.
- place_type (:obj:`str`): The type of places contained in the given dcids to
- filter by.
-
- Returns:
- The returned :obj:`Place`'s are formatted as a :obj:`dict` from a given
- dcid to a list of places identified by dcids of the given `place_type`.
-
- Raises:
- ValueError: If the payload returned by the Data Commons REST API is
- malformed.
-
- Examples:
- We would like to get all Counties contained in
- `California `_. Specifying
- the :code:`dcids` as a :obj:`list` result in the following.
-
- >>> get_places_in(["geoId/06"], "County")
- {
- 'geoId/06': [
- 'geoId/06041',
- 'geoId/06089',
- 'geoId/06015',
- 'geoId/06023',
- 'geoId/06067',
- ...
- # and 53 more
- ]
- }
- """
- dcids = filter(lambda v: v==v, dcids) # Filter out NaN values
- dcids = list(dcids)
- url = utils._API_ROOT + utils._API_ENDPOINTS['get_places_in']
- payload = utils._send_request(url, req_json = {
- 'dcids': dcids,
- 'place_type': place_type,
- })
-
- # Create the results and format it appropriately
- result = utils._format_expand_payload(payload, 'place', must_exist=dcids)
- return result
-
-def get_stats(dcids, stats_var, obs_dates='latest', measurement_method=None,
- unit=None, obs_period=None):
- """ Returns :obj:`TimeSeries` for :code:`dcids` \
- based on the :code:`stats_var`.
-
- Args:
- dcids (:obj:`iterable` of :obj:`str`): Dcids of places to query for.
- stats_var (:obj:`str`): The dcid of the :obj:StatisticalVariable.
- obs_dates (:obj:`str` or :obj:`iterable` of :obj:`str`):
- Which observation to return.
- Can be 'latest', 'all', or an iterable of dates in 'YYYY-MM-DD' format.
- measurement_method (:obj:`str`): Optional, the dcid of the preferred
- `measurementMethod` value.
- unit (:obj:`str`): Optional, the dcid of the preferred `unit` value.
- obs_period (:obj:`str`): Optional, the dcid of the preferred
- `observationPeriod` value.
- Returns:
- A :obj:`dict` mapping the :obj:`Place` identified by the given :code:`dcid`
- to its place name and the :obj:`TimeSeries` associated with the
- :obj:`StatisticalVariable` identified by the given :code:`stats_var`
- and filtered by :code:`obs_dates` and optional args.
- See example below for more detail about how the returned :obj:`dict` is
- structured.
-
- Raises:
- ValueError: If the payload returned by the Data Commons REST API is
- malformed.
-
- Examples:
- We would like to get the :obj:`TimeSeries` of the number of males
- at least 25 years old that attended 12th grade but did not receive
- a high school diploma
- (`dc/0hyp6tkn18vcb `_)
- in `Arkansas `_
- and `California `_.
-
- >>> get_stats(["geoId/05", "geoId/06"], "dc/0hyp6tkn18vcb")
- {
- 'geoId/05': {
- 'place_name': 'Arkansas'
- 'data': {
- '2011':18136,
- '2012':17279,
- '2013':17459,
- '2014':16966,
- '2015':17173,
- '2016':17041,
- '2017':17783,
- '2018':18003
- },
- },
- 'geoId/05': {
- 'place_name': 'California'
- 'data': {
- '2011':316667,
- '2012':324116,
- '2013':331853,
- '2014':342818,
- '2015':348979,
- '2016':354806,
- '2017':360645,
- '2018':366331
- },
- },
- }
- """
- dcids = filter(lambda v: v==v, dcids) # Filter out NaN values
- dcids = list(dcids)
- url = utils._API_ROOT + utils._API_ENDPOINTS['get_stats']
- batches = -(-len(dcids) // utils._QUERY_BATCH_SIZE) # Ceil to get # of batches.
- res = {}
- for i in range(batches):
- req_json = {
- 'place': dcids[i * utils._QUERY_BATCH_SIZE:(i+1) * utils._QUERY_BATCH_SIZE],
- 'stats_var': stats_var,
- }
- if measurement_method:
- req_json['measurement_method'] = measurement_method
- if unit:
- req_json['unit'] = unit
- if obs_period:
- req_json['observation_period'] = obs_period
- payload = utils._send_request(url, req_json)
- if obs_dates == 'all':
- res.update(payload)
- elif obs_dates == 'latest':
- for geo, stats in payload.items():
- if not stats:
- continue
- time_series = stats.get('data')
- if not time_series: continue
- max_date = max(time_series)
- max_date_stat = time_series[max_date]
- time_series.clear()
- time_series[max_date] = max_date_stat
- res[geo] = stats
- elif obs_dates:
- obs_dates = set(obs_dates)
- for geo, stats in payload.items():
- if not stats:
- continue
- time_series = stats.get('data')
- if not time_series: continue
- for date in list(time_series):
- if date not in obs_dates:
- time_series.pop(date)
- res[geo] = stats
- return res
-
-
-def get_related_places(dcids, population_type, measured_property,
- measurement_method, stat_type, constraining_properties={},
- within_place='', per_capita=False, same_place_type=False):
- """ Returns :obj:`Place`s related to :code:`dcids` for the given constraints.
-
- Args:
- dcids (:obj:`iterable` of :obj:`str`): Dcids to get related places.
- population_type (:obj:`str`): The type of statistical population.
- measured_property (:obj:`str`): The measured property.
- measurement_method(:obj:`str`): The measurement method for the observation.
- stat_type (:obj:`str`): The statistical type for the observation.
- constraining_properties (:obj:`map` from :obj:`str` to :obj:`str`, optional):
- A map from constraining property to the value that the
- :obj:`StatisticalPopulation` should be constrained by.
- within_place(:obj:`str`): Optional, the DCID of the place that all the
- related places are contained in.
- per_capita(:obj:`bool`): Optional, whether to take into account
- `PerCapita` when compute the relatedness.
- same_place_type(:obj:`bool`): Optional, whether to require all the
- related places under the same place type.
-
- Returns:
- The returned :obj:`Place`'s are formatted as a :obj:`dict` from a given
- dcid to a list of related places for the given constraints.
-
- Raises:
- ValueError: If the payload returned by the Data Commons REST API is
- malformed.
-
- Examples:
- We would like to get all related places of
- `Santa Clara county `
- Specifying the :code:`dcids` as a :obj:`list` result in the following.
-
- >>> get_related_places(["geoId/06"], "Person", {
- "age": "Years21To64",
- "gender": "Female"
- }, "count", "CenusACS5yrSurvey", "measuredValue")
- {
- 'geoId/06085': [
- 'geoId/06041',
- 'geoId/06089',
- 'geoId/06015',
- 'geoId/06023',
- ]
- }
- """
- dcids = filter(lambda v: v==v, dcids) # Filter out NaN values
- dcids = list(dcids)
- url = utils._API_ROOT + utils._API_ENDPOINTS['get_related_places']
- pvs = []
- for p in constraining_properties:
- pvs.append({'property': p, 'value': constraining_properties[p]})
- req_json = {
- 'dcids': dcids,
- 'populationType': population_type,
- 'pvs': pvs,
- 'measuredProperty': measured_property,
- 'statType': '', # TODO: Set to stat_type when having it in BT data.
- 'measurementMethod': measurement_method,
- 'withinPlace': within_place,
- 'perCapita': per_capita,
- 'samePlaceType': same_place_type,
- }
- payload = utils._send_request(url, req_json=req_json)
- return payload
diff --git a/datacommons/populations.py b/datacommons/populations.py
deleted file mode 100644
index 2b1bd42c..00000000
--- a/datacommons/populations.py
+++ /dev/null
@@ -1,418 +0,0 @@
-# Copyright 2017 Google Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-""" Data Commons Python API Populations Module.
-
-Provides convenience functions for accessing :obj:`StatisticalPopulation`'s and
-:obj:`Observation`'s in the Data Commons Graph. Implements the
-following:
-
-- Get :obj:`StatisticalPopulation`'s located at a given collection of nodes.
-- Get :obj:`Observation`'s observing a collection of
- :obj:`StatisticalPopulation`'s
-"""
-
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
-import datacommons.utils as utils
-
-
-def _flatten_results(result, default_value=None):
- """ Formats results to map to a single value or default value if empty. """
- for k in list(result):
- v = result[k]
- if len(v) > 1:
- raise ValueError(
- 'Expected one result, but more returned for "{}": {}'.format(k, v))
- if len(v) == 1:
- result[k] = v[0]
- else:
- if default_value is not None:
- result[k] = default_value
- else:
- del result[k]
- return result
-
-
-def get_populations(dcids, population_type, constraining_properties={}):
- """ Returns :obj:`StatisticalPopulation`'s located at the given :code:`dcids`.
-
- Args:
- dcids (:obj:`iterable` of :obj:`str`): Dcids
- identifying :obj:`Place`'s of populations to query for. These dcids are
- treated as the property value associated with returned :obj:`Population`'s
- by the property
- `location `_
- population_type (:obj:`str`): The population type of the
- :obj:`StatisticalPopulation`
- constraining_properties (:obj:`map` from :obj:`str` to :obj:`str`, optional):
- A map from constraining property to the value that the
- :obj:`StatisticalPopulation` should be constrained by.
-
- Returns:
- The returned :obj:`StatisticalPopulation` are formatted as a :obj:`dict` from a given
- dcid to the unique :obj:`StatisticalPopulation` located at the dcid as
- specified by the `population_type` and `constraining_properties` *if such
- exists*. A given dcid will *NOT* be a member of the :obj:`dict` if such
- a population does not exist.
-
- Raises:
- ValueError: If the payload returned by the Data Commons REST API is
- malformed.
-
- Examples:
- We would like to get
-
- - The `population of employed persons in California `_
- - The `population of employed persons in Kentucky `_
- - The `population of employed persons in Maryland `_.
-
- These populations are specified as having a
- `population_type` as :obj:`Person` and the `constraining_properties`
- as `employment `_
- = BLS_Employed
-
- With a :obj:`list` of dcids for our states, we can get the populations we
- want as follows.
-
- >>> dcids = ["geoId/06", "geoId/21", "geoId/24"]
- >>> pvs = {'employment': 'BLS_Employed'}
- >>> dc.get_populations(dcids, 'Person', constraining_properties=pvs)
- {
- "geoId/06": "dc/p/x6t44d8jd95rd",
- "geoId/21": "dc/p/fs929fynprzs",
- "geoId/24": "dc/p/lr52m1yr46r44"
- }
- """
- # Convert the dcids field and format the request to GetPopulations
- dcids = filter(lambda v: v==v, dcids) # Filter out NaN values
- dcids = list(dcids)
- pv = [{'property': k, 'value': v} for k, v in constraining_properties.items()]
- url = utils._API_ROOT + utils._API_ENDPOINTS['get_populations']
- payload = utils._send_request(url, req_json={
- 'dcids': dcids,
- 'population_type': population_type,
- 'pvs': pv,
- })
-
- # Create the results and format it appropriately
- result = utils._format_expand_payload(
- payload, 'population', must_exist=dcids)
-
- # Drop empty results while flattening
- return _flatten_results(result)
-
-
-def get_observations(dcids,
- measured_property,
- stats_type,
- observation_date,
- observation_period=None,
- measurement_method=None):
- """ Returns values of :obj:`Observation`'s observing the given :code:`dcids`.
-
- Args:
- dcids (:obj:`iterable` of :obj:`str`): Dcids
- identifying nodes that returning :obj:`Observation`'s observe. These dcids
- are treated as the property value associated with returned
- :obj:`Observation`'s by the property
- `observedNode `_
- measured_property (:obj:`str`): The measured property.
- stats_type (:obj:`str`): The statistical type for the observation.
- observation_date (:obj:`str`): The associated observation date in ISO8601
- format.
- observation_period (:obj:`str`, optional): An optional parameter specifying
- the observation period.
- measurement_method (:obj:`str`, optional): An optional parameter specifying
- the measurement method.
-
- Raises:
- ValueError: If the payload returned by the Data Commons REST API is
- malformed.
-
- Returns:
- When :code:`dcids` is an instance of :obj:`list`, the returned
- :obj:`Observation`'s are formatted as a :obj:`dict` from a given dcid to the
- unique :obj:`Observation` observing the dcid where the observation is
- specified by what is given in the other parameters *if such exists*. A given
- dcid will *NOT* be a member of the :obj:`dict` if such an observation does
- not exist.
-
- Examples:
- We would like to get the following for December, 2018:
-
- - The `total count of employed persons in California `_
- - The `total count of employed persons in Kentucky `_
- - The `total count of employed persons in Maryland `_.
-
- The observations we want are observations of the populations representing
- employed individuals in each state (to get these, see
- :any:module-datacommons.populations.get_populations). With a list of these
- population dcids, we can get the observations like so.
-
- >>> dcids = [
- ... "dc/p/x6t44d8jd95rd", # Employed individuals in California
- ... "dc/p/fs929fynprzs", # Employed individuals in Kentucky
- ... "dc/p/lr52m1yr46r44" # Employed individuals in Maryland
- ... ]
- >>> get_observations(dcids, 'count', 'measuredValue', '2018-12',
- ... observation_period='P1M',
- ... measurement_method='BLSSeasonallyAdjusted'
- ... )
- {
- "dc/p/x6t44d8jd95rd": 18704962.0,
- "dc/p/fs929fynprzs": 1973955.0,
- "dc/p/lr52m1yr46r44": 3075662.0
- }
- """
- dcids = filter(lambda v: v==v, dcids) # Filter out NaN values
- dcids = list(dcids)
- req_json = {
- 'dcids': dcids,
- 'measured_property': measured_property,
- 'stats_type': stats_type,
- 'observation_date': observation_date,
- }
- if observation_period:
- req_json['observation_period'] = observation_period
- if measurement_method:
- req_json['measurement_method'] = measurement_method
-
- # Issue the request to GetObservation
- url = utils._API_ROOT + utils._API_ENDPOINTS['get_observations']
- payload = utils._send_request(url, req_json=req_json)
-
- # Create the results and format it appropriately
- result = utils._format_expand_payload(
- payload, 'observation', must_exist=dcids)
-
- # Drop empty results by calling _flatten_results without default_value, then
- # coerce the type to float if possible.
- typed_results = {}
- for k, v in _flatten_results(result).items():
- try:
- typed_results[k] = float(v)
- except ValueError:
- typed_results[k] = v
- return typed_results
-
-
-def get_pop_obs(dcid):
- """ Returns all :obj:`StatisticalPopulation` and :obj:`Observation` \
- of a :obj:`Thing`.
-
- Args:
- dcid (:obj:`str`): Dcid of the thing.
-
- Returns:
- A :obj:`dict` of :obj:`StatisticalPopulation` and :obj:`Observation` that
- are associated to the thing identified by the given :code:`dcid`. The given
- dcid is linked to the returned :obj:`StatisticalPopulation`,
- which are the :obj:`observedNode` of the returned :obj:`Observation`.
- See example below for more detail about how the returned :obj:`dict` is
- structured.
-
- Raises:
- ValueError: If the payload returned by the Data Commons REST API is
- malformed.
-
- Examples:
- We would like to get all :obj:`StatisticalPopulation` and
- :obj:`Observations` of
- `Santa Clara `_.
-
- >>> get_pop_obs("geoId/06085")
- {
- 'name': 'Santa Clara',
- 'placeType': 'County',
- 'populations': {
- 'dc/p/zzlmxxtp1el87': {
- 'popType': 'Household',
- 'numConstraints': 3,
- 'propertyValues': {
- 'householderAge': 'Years45To64',
- 'householderRace': 'USC_AsianAlone',
- 'income': 'USDollar35000To39999'
- },
- 'observations': [
- {
- 'marginOfError': 274,
- 'measuredProp': 'count',
- 'measuredValue': 1352,
- 'measurementMethod': 'CensusACS5yrSurvey',
- 'observationDate': '2017'
- },
- {
- 'marginOfError': 226,
- 'measuredProp': 'count',
- 'measuredValue': 1388,
- 'measurementMethod': 'CensusACS5yrSurvey',
- 'observationDate': '2013'
- }
- ],
- },
- },
- 'observations': [
- {
- 'meanValue': 4.1583,
- 'measuredProp': 'particulateMatter25',
- 'measurementMethod': 'CDCHealthTracking',
- 'observationDate': '2014-04-04',
- 'observedNode': 'geoId/06085'
- },
- {
- 'meanValue': 9.4461,
- 'measuredProp': 'particulateMatter25',
- 'measurementMethod': 'CDCHealthTracking',
- 'observationDate': '2014-03-20',
- 'observedNode': 'geoId/06085'
- }
- ]
- }
-
- Notice that the return value is a multi-level :obj:`dict`. The top level
- contains the following keys.
-
- - :code:`name` and :code:`placeType` provides the name and type of the
- :obj:`Place` identified by the given :code:`dcid`.
- - :code:`populations` maps to a :obj:`dict` containing all
- :obj:`StatisticalPopulation` that have the given :code:`dcid` as its
- :obj:`location`.
- - :code:`observations` maps to a :obj:`list` containing all
- :obj:`Observation` that have the given :code:`dcid` as its
- :obj:`observedNode`.
-
- The :code:`populations` dictionary is keyed by the dcid of each
- :obj:`StatisticalPopulation`. The mapped dictionary contains the following
- keys.
-
- - :code:`popType` which gives the population type of the
- :obj:`StatisticalPopulation` identified by the key.
- - :code:`numConstraints` which gives the number of constraining properties
- defined for the identified :obj:`StatisticalPopulation`.
- - :code:`propertyValues` which gives a :obj:`dict` mapping a constraining
- property to its value for the identified :obj:`StatisticalPopulation`.
- - :code:`observations` which gives a list of all :obj:`Observation`'s that
- have the identified :obj:`StatisticalPopulation` as their
- :obj:`observedNode`.
-
- Each :obj:`Observation` is represented by a :code:`dict` that have the keys:
-
- - :code:`measuredProp`: The property measured by the :obj:`Observation`.
- - :code:`observationDate`: The date when the :obj:`Observation` was made.
- - :code:`observationPeriod` (optional): The period over which the
- :obj:`Observation` was made.
- - :code:`measurementMethod` (optional): A field providing additional
- information on how the :obj:`Observation` was collected.
- - Additional fields that denote values measured by the :obj:`Observation`.
- These may include the following: :code:`measuredValue`, :code:`meanValue`,
- :code:`medianValue`, :code:`maxValue`, :code:`minValue`, :code:`sumValue`,
- :code:`marginOfError`, :code:`stdError`, :code:`meanStdError`, and others.
- """
- url = utils._API_ROOT + utils._API_ENDPOINTS['get_pop_obs'] + '?dcid={}'.format(dcid)
- return utils._send_request(url, compress=True, post=False)
-
-def get_place_obs(
- place_type, observation_date, population_type, constraining_properties={}):
- """ Returns all :obj:`Observation`'s for all places given the place type,
- observation date and the :obj:`StatisticalPopulation` constraints.
-
- Args:
- place_type (:obj:`str`): The type of places to query
- :obj:`StatisticalPopulation`'s and :obj:`Observation`'s for.
- observation_date (:obj:`str`): The observation date in ISO-8601 format.
- population_type (:obj:`str`): The population type of the
- :obj:`StatisticalPopulation`
- constraining_properties (:obj:`map` from :obj:`str` to :obj:`str`, optional):
- A map from constraining property to the value that the
- :obj:`StatisticalPopulation` should be constrained by.
-
- Returns:
- A list of dictionaries, with each dictionary containng *all*
- :obj:`Observation`'s of a place that conform to the :obj:`StatisticalPopulation`
- constraints. See examples for more details on how the format of the
- return value is structured.
-
- Raises:
- ValueError: If the payload is malformed.
-
- Examples:
- We would like to get all :obj:`StatisticalPopulation` and
- :obj:`Observations` for all places of type :obj:`City` in year 2017 where
- the populations have a population type of :obj:`Person` is specified by the
- following constraining properties.
-
- - Persons should have `age `_
- with value `Years5To17 `_
- - Persons should have `placeOfBirth `_
- with value BornInOtherStateInTheUnitedStates.
-
- >>> props = {
- ... 'age': 'Years5To17',
- ... 'placeOfBirth': 'BornInOtherStateInTheUnitedStates'
- ... }
- >>> get_place_obs('City', '2017', Person', constraining_properties=props)
- [
- {
- 'name': 'Marcus Hook borough',
- 'place': 'geoId/4247344',
- 'populations': {
- 'dc/p/pq6frs32sfvk': {
- 'observations': [
- {
- 'marginOfError': 39,
- 'measuredProp': 'count',
- 'measuredValue': 67,
- 'type': 'Observation'
- },
- # More observations...
- ],
- }
- }
- },
- # Entries for more cities...
- ]
-
- The value returned by :code:`get_place_obs` is a :obj:`list` of
- :obj:`dict`'s. Each dictionary corresponds to a :obj:`StatisticalPopulation`
- matching the given :code:`population_type` and
- :code:`constraining_properties` for a single place of the given
- :code:`place_type`. The dictionary contains the following keys.
-
- - :code:`name`: The name of the place being described.
- - :code:`place`: The dcid associated with the place being described.
- - :code:`populations`: A :obj:`dict` mapping :code:`StatisticalPopulation`
- dcids to a a :obj:`dict` with a list of :code:`observations`.
-
- Each :obj:`Observation` is represented by a :obj:`dict` with the following
- keys.
- - :code:`measuredProp`: The property measured by the :obj:`Observation`.
- - :code:`measurementMethod` (optional): A field identifying how the
- :obj:`Observation` was made
- - Additional fields that denote values measured by the :obj:`Observation`.
- These may include the following: :code:`measuredValue`, :code:`meanValue`,
- :code:`medianValue`, :code:`maxValue`, :code:`minValue`, :code:`sumValue`,
- :code:`marginOfError`, :code:`stdError`, :code:`meanStdError`, and others.
- """
- # Create the json payload and send it to the REST API.
- pv = [{'property': k, 'value': v} for k, v in constraining_properties.items()]
- url = utils._API_ROOT + utils._API_ENDPOINTS['get_place_obs']
- payload = utils._send_request(url, req_json={
- 'place_type': place_type,
- 'observation_date': observation_date,
- 'population_type': population_type,
- 'pvs': pv,
- }, compress=True)
- return payload['places']
diff --git a/datacommons/query.py b/datacommons/query.py
deleted file mode 100644
index 94b8ab20..00000000
--- a/datacommons/query.py
+++ /dev/null
@@ -1,129 +0,0 @@
-# Copyright 2017 Google Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-""" Data Commons Python API Query Module.
-
-Implements functions for sending graph queries to the Data Commons Graph.
-"""
-
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
-from datacommons.utils import _API_ROOT, _API_ENDPOINTS, _ENV_VAR_API_KEY
-
-import json
-import os
-import six.moves.urllib.error
-import six.moves.urllib.request
-
-# ----------------------------- WRAPPER FUNCTIONS -----------------------------
-
-
-def query(query_string, select=None):
- """ Returns the results of executing a SPARQL query on the Data Commons graph.
-
- Args:
- query_string (:obj:`str`): The SPARQL query string.
- select (:obj:`func` accepting a row in the query result): A function that
- selects rows to be returned by :code:`query`. This function accepts a row
- in the results of executing :code:`query_string` and return True if and
- only if the row is to be returned by :code:`query`. The row passed in as
- an argument is represented as a :obj:`dict` that maps a query variable in
- :code:`query_string` to its value in the given row.
-
- Returns:
- A table, represented as a :obj:`list` of rows, resulting from executing the
- given SPARQL query. Each row is a :obj:`dict` mapping query variable to its
- value in the row. If `select` is not `None`, then a row is included in the
- returned :obj:`list` if and only if `select` returns :obj:`True` for that
- row.
-
- Raises:
- ValueError: If the payload returned by the Data Commons REST API is
- malformed.
-
- Examples:
- We would like to query for the name associated with three states identified
- by their dcids
- `California `_,
- `Kentucky `_, and
- `Maryland `_.
-
- >>> query_str = '''
- ... SELECT ?name ?dcid
- ... WHERE {
- ... ?a typeOf Place .
- ... ?a name ?name .
- ... ?a dcid ("geoId/06" "geoId/21" "geoId/24") .
- ... ?a dcid ?dcid
- ... }
- ... '''
- >>> result = query(query_str)
- >>> for r in result:
- ... print(r)
- {"?name": "Maryland", "?dcid": "geoId/24"}
- {"?name": "Kentucky", "?dcid": "geoId/21"}
- {"?name": "California", "?dcid": "geoId/06"}
-
- Optionally, we can specify which rows are returned by setting :code:`select`
- like so. The following returns all rows where the name is "Maryland".
-
- >>> selector = lambda row: row['?name'] == 'Maryland'
- >>> result = query(query_str, select=selector)
- >>> for r in result:
- ... print(r)
- {"?name": "Maryland", "?dcid": "geoId/24"}
- """
-
- req_url = _API_ROOT + _API_ENDPOINTS['query']
- headers = {
- 'Content-Type': 'application/json'
- }
- if os.environ.get(_ENV_VAR_API_KEY):
- headers['x-api-key'] = os.environ[_ENV_VAR_API_KEY]
-
- req = six.moves.urllib.request.Request(
- req_url,
- data=json.dumps({'sparql': query_string}).encode("utf-8"),
- headers=headers)
-
- try:
- res = six.moves.urllib.request.urlopen(req)
- except six.moves.urllib.error.HTTPError as e:
- raise ValueError('Response error {}:\n{}'.format(e.code, e.read()))
-
- # Verify then store the results.
- res_json = json.loads(res.read())
-
- # Iterate through the query results
- header = res_json.get('header')
- if header is None:
- raise ValueError('Ill-formatted response: does not contain a header.')
- result_rows = []
- for row in res_json.get('rows', []):
- # Construct the map from query variable to cell value.
- row_map = {}
- for idx, cell in enumerate(row.get('cells', [])):
- if idx > len(header):
- raise ValueError(
- 'Query error: unexpected cell {}'.format(cell))
- if 'value' not in cell:
- raise ValueError(
- 'Query error: cell missing value {}'.format(cell))
- cell_var = header[idx]
- row_map[cell_var] = cell['value']
- # Add the row to the result rows if it is selected
- if select is None or select(row_map):
- result_rows.append(row_map)
- return result_rows
diff --git a/datacommons/stat_vars.py b/datacommons/stat_vars.py
deleted file mode 100644
index 6e9e79be..00000000
--- a/datacommons/stat_vars.py
+++ /dev/null
@@ -1,260 +0,0 @@
-# Copyright 2020 Google Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-"""Data Commons Python API Stat Module.
-
-Provides functions for getting data on StatisticalVariables from Data Commons Graph.
-"""
-
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
-import collections
-import six
-
-import datacommons.utils as utils
-
-# stat_var specific batch size.
-_STAT_BATCH_SIZE = 2000
-
-
-def get_stat_value(place,
- stat_var,
- date=None,
- measurement_method=None,
- observation_period=None,
- unit=None,
- scaling_factor=None):
- """Returns a value for `place` based on the `stat_var`.
-
- Args:
- place (`str`): The dcid of Place to query for.
- stat_var (`str`): The dcid of the StatisticalVariable.
- date (`str`): Optional, the preferred date of observation
- in ISO 8601 format. If not specified, returns the latest observation.
- measurement_method (`str`): Optional, the dcid of the preferred
- `measurementMethod` value.
- observation_period (`str`): Optional, the preferred
- `observationPeriod` value.
- unit (`str`): Optional, the dcid of the preferred `unit` value.
- scaling_factor (`int`): Optional, the preferred `scalingFactor` value.
- Returns:
- A `float` the value of `stat_var` for `place`, filtered
- by optional args. If no data, returns nan.
-
- Raises:
- ValueError: If the payload returned by the Data Commons REST API is
- malformed.
-
- Examples:
- >>> get_stat_value("geoId/05", "Count_Person")
- 366331
- """
- url = utils._API_ROOT + utils._API_ENDPOINTS['get_stat_value']
- url += '?place={}&stat_var={}'.format(place, stat_var)
- if date:
- url += '&date={}'.format(date)
- if measurement_method:
- url += '&measurement_method={}'.format(measurement_method)
- if observation_period:
- url += '&observation_period={}'.format(observation_period)
- if unit:
- url += '&unit={}'.format(unit)
- if scaling_factor:
- url += '&scaling_factor={}'.format(scaling_factor)
-
- try:
- res_json = utils._send_request(url, post=False, use_payload=False)
- except ValueError:
- return float('nan')
- if 'value' not in res_json:
- return float('nan')
- return res_json['value']
-
-
-def get_stat_series(place,
- stat_var,
- measurement_method=None,
- observation_period=None,
- unit=None,
- scaling_factor=None):
- """Returns a `dict` mapping dates to value of `stat_var` for `place`.
-
- Args:
- place (`str`): The dcid of Place to query for.
- stat_var (`str`): The dcid of the StatisticalVariable.
- measurement_method (`str`): Optional, the dcid of the preferred
- `measurementMethod` value.
- observation_period (`str`): Optional, the preferred
- `observationPeriod` value.
- unit (`str`): Optional, the dcid of the preferred `unit` value.
- scaling_factor (`int`): Optional, the preferred `scalingFactor` value.
- Returns:
- A `dict` mapping dates to value of `stat_var` for `place`,
- representing a time series that satisfies all input parameters.
-
- Raises:
- ValueError: If the payload returned by the Data Commons REST API is
- malformed.
-
- Examples:
- >>> get_stat_series("geoId/05", "Count_Person")
- {"1962":17072000,"2009":36887615,"1929":5531000,"1930":5711000}
- """
- url = utils._API_ROOT + utils._API_ENDPOINTS['get_stat_series']
- url += '?place={}&stat_var={}'.format(place, stat_var)
- if measurement_method:
- url += '&measurement_method={}'.format(measurement_method)
- if observation_period:
- url += '&observation_period={}'.format(observation_period)
- if unit:
- url += '&unit={}'.format(unit)
- if scaling_factor:
- url += '&scaling_factor={}'.format(scaling_factor)
-
- try:
- res_json = utils._send_request(url, post=False, use_payload=False)
- except ValueError:
- return {}
-
- if 'series' not in res_json:
- return {}
- return res_json['series']
-
-
-def get_stat_all(places, stat_vars):
- """Returns a nested `dict` of all time series for `places` and `stat_vars`.
-
- Args:
- places (`Iterable` of `str`): The dcids of Places to query for.
- stat_vars (`Iterable` of `str`): The dcids of the StatisticalVariables.
- Returns:
- A nested `dict` mapping Places to StatisticalVariables and all available
- time series for each Place and StatisticalVariable pair.
-
- Raises:
- ValueError: If the payload returned by the Data Commons REST API is
- malformed.
-
- Examples:
- >>> get_stat_all(["geoId/05", "geoId/06"], ["Count_Person", "Count_Person_Male"])
- {
- "geoId/05": {
- "Count_Person": {
- "sourceSeries": [
- {
- "val": {
- "2010": 1633,
- "2011": 1509,
- "2012": 1581,
- },
- "observationPeriod": "P1Y",
- "importName": "Wikidata",
- "provenanceDomain": "wikidata.org"
- },
- {
- "val": {
- "2010": 1333,
- "2011": 1309,
- "2012": 131,
- },
- "observationPeriod": "P1Y",
- "importName": "CensusPEPSurvey",
- "provenanceDomain": "census.gov"
- }
- ],
- }
- },
- "Count_Person_Male": {
- "sourceSeries": [
- {
- "val": {
- "2010": 1633,
- "2011": 1509,
- "2012": 1581,
- },
- "observationPeriod": "P1Y",
- "importName": "CensusPEPSurvey",
- "provenanceDomain": "census.gov"
- }
- ],
- }
- },
- "geoId/02": {
- "Count_Person": {},
- "Count_Person_Male": {
- "sourceSeries": [
- {
- "val": {
- "2010": 13,
- "2011": 13,
- "2012": 322,
- },
- "observationPeriod": "P1Y",
- "importName": "CensusPEPSurvey",
- "provenanceDomain": "census.gov"
- }
- ]
- }
- }
- }
- """
- url = utils._API_ROOT + utils._API_ENDPOINTS['get_stat_all']
- # Cast iterable-like to list.
- places = list(places)
- stat_vars = list(stat_vars)
-
- # Aiming for _STAT_BATCH_SIZE entries total.
- # _STAT_BATCH_SIZE = num places x num stat_vars, so aim for
- # _STAT_BATCH_SIZE/len(stat_vars) places per batch.
- places_per_batch = _STAT_BATCH_SIZE // len(stat_vars)
- # Get number of batches via an arithmetic ceiling trick:
- # 11//10 rounds down to 1.
- # -11//10 rounds down to -2.
- # We can divide with, then remove the negative to get the ceiling.
- batches = -(-len(places) // places_per_batch)
- res = {}
- for i in range(batches):
- req_json = {
- 'stat_vars': stat_vars,
- 'places': places[i * places_per_batch:(i + 1) * places_per_batch]
- }
- # Send the request
- res_json = utils._send_request(url,
- req_json=req_json,
- use_payload=False)
- if 'placeData' not in res_json:
- # The REST API spec will always return a dictionary under
- # placeData, even if no places exist or have no
- # data. If no Places are provided, REST will return an
- # error, which will have been caught and passed on in
- # _send_request.
- raise ValueError("Unexpected response from REST stat/all API.")
-
- # Unnest the REST response for keys that have single-element values.
- place_statvar_series = collections.defaultdict(dict)
- for place_dcid, place in res_json['placeData'].items():
- stat_var_data = place.get('statVarData')
- if not stat_var_data:
- # The REST API spec will always return a dictionary under
- # statVarData, even if no StatVars exist or have no
- # data. If no StatVars are provided, REST will return an
- # error, which will have been caught and passed on in
- # _send_request.
- raise ValueError("Unexpected response from REST stat/all API.")
- for stat_var_dcid, stat_var in stat_var_data.items():
- place_statvar_series[place_dcid][stat_var_dcid] = stat_var
- res.update(dict(place_statvar_series))
-
- return res
diff --git a/datacommons/test/__init__.py b/datacommons/test/__init__.py
deleted file mode 100644
index 7c07b241..00000000
--- a/datacommons/test/__init__.py
+++ /dev/null
@@ -1,13 +0,0 @@
-# Copyright 2017 Google Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
diff --git a/datacommons/test/core_test.py b/datacommons/test/core_test.py
deleted file mode 100644
index e64064a6..00000000
--- a/datacommons/test/core_test.py
+++ /dev/null
@@ -1,523 +0,0 @@
-# Copyright 2017 Google Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-""" Data Commons Python API unit tests.
-
-Unit tests for core methods in the Data Commons Python API.
-"""
-
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
-try:
- from unittest.mock import patch
-except ImportError:
- from mock import patch
-
-import six.moves.urllib as urllib
-
-import datacommons as dc
-import datacommons.utils as utils
-import json
-import unittest
-
-
-def request_mock(*args, **kwargs):
- """ A mock urlopen in the urllib package. """
- # Create the mock response object.
- class MockResponse:
- def __init__(self, json_data):
- self.json_data = json_data
-
- def read(self):
- return self.json_data
-
- # Get the request data
- req = args[0]
- data = json.loads(req.data)
-
- # Mock responses for urlopen requests to get_property_labels.
- if req.get_full_url() == utils._API_ROOT + utils._API_ENDPOINTS['get_property_labels']:
- if data['dcids'] == ['geoId/0649670']:
- # Response for sending a single dcid to get_property_labels
- out_arcs = ['containedInPlace', 'name', 'geoId', 'typeOf']
- res_json = json.dumps({
- 'geoId/0649670': {
- 'inLabels': [],
- 'outLabels': out_arcs
- }
- })
- return MockResponse(json.dumps({'payload': res_json}))
- elif data['dcids'] == ['State', 'County', 'City']:
- # Response for sending multiple dcids to get_property_labels
- in_arcs = ['typeOf']
- out_arcs = ['name', 'provenance', 'subClassOf', 'typeOf', 'url']
- res_json = json.dumps({
- 'City': {'inLabels': in_arcs, 'outLabels': out_arcs},
- 'County': {'inLabels': in_arcs, 'outLabels': out_arcs},
- 'State': {'inLabels': in_arcs, 'outLabels': out_arcs}
- })
- return MockResponse(json.dumps({'payload': res_json}))
- elif data['dcids'] == ['dc/MadDcid']:
- # Response for sending a dcid that doesn't exist to get_property_labels
- res_json = json.dumps({
- 'dc/MadDcid': {
- 'inLabels': [],
- 'outLabels': []
- }
- })
- return MockResponse(json.dumps({'payload': res_json}))
- elif data['dcids'] == []:
- # Response for sending no dcids to get_property_labels
- res_json = json.dumps({})
- return MockResponse(json.dumps({'payload': res_json}))
-
- # Mock responses for urlopen requests to get_property_values
- if req.get_full_url() == utils._API_ROOT + utils._API_ENDPOINTS['get_property_values']:
- if data['dcids'] == ['geoId/06085', 'geoId/24031']\
- and data['property'] == 'containedInPlace'\
- and data['value_type'] == 'Town':
- # Response for sending a request for getting Towns containedInPlace of
- # Santa Clara County and Montgomery County.
- res_json = json.dumps({
- 'geoId/06085': {
- 'in': [
- {
- 'dcid': 'geoId/0644112',
- 'name': 'Los Gatos',
- 'provenanceId': 'dc/sm3m2w3',
- 'types': [
- 'City',
- 'Town'
- ]
- },
- {
- 'dcid': 'geoId/0643294',
- 'name': 'Los Altos Hills',
- 'provenanceId': 'dc/sm3m2w3',
- 'types': [
- 'City',
- 'Town'
- ]
- }
- ],
- 'out': []
- },
- 'geoId/24031': {
- 'in': [
- {
- 'dcid': 'geoId/2462850',
- 'name': 'Poolesville',
- 'provenanceId': 'dc/sm3m2w3',
- 'types': [
- 'City',
- 'Town'
- ]
- },
- ],
- 'out': []
- }
- })
- return MockResponse(json.dumps({'payload': res_json}))
- if data['dcids'] == ['geoId/06085', 'geoId/24031']\
- and data['property'] == 'name':
- # Response for sending a request for the name of multiple dcids.
- res_json = json.dumps({
- 'geoId/06085': {
- 'in': [],
- 'out': [
- {
- 'value': 'Santa Clara County',
- 'provenanceId': 'dc/sm3m2w3',
- },
- ]
- },
- 'geoId/24031': {
- 'in': [],
- 'out': [
- {
- 'value': 'Montgomery County',
- 'provenanceId': 'dc/sm3m2w3',
- },
- ]
- }
- })
- return MockResponse(json.dumps({'payload': res_json}))
- if data['dcids'] == ['dc/p/1234'] and data['property'] == 'name':
- # Response for sending a request for the name with no data
- res_json = json.dumps({
- 'dc/p/1234': {}
- })
- return MockResponse(json.dumps({'payload': res_json}))
- if data['dcids'] == ['geoId/06085', 'geoId/24031']\
- and data['property'] == 'madProperty':
- # Response for sending a request with a property that does not exist.
- res_json = json.dumps({
- 'geoId/06085': {
- 'in': [],
- 'out': []
- },
- 'geoId/24031': {
- 'in': [],
- 'out': []
- }
- })
- return MockResponse(json.dumps({'payload': res_json}))
- if data['dcids'] == ['geoId/06085', 'dc/MadDcid']\
- and data['property'] == 'containedInPlace':
- # Response for sending a request with a single dcid that does not exist.
- res_json = json.dumps({
- 'geoId/06085': {
- 'in': [
- {
- 'dcid': 'geoId/0644112',
- 'name': 'Los Gatos',
- 'provenanceId': 'dc/sm3m2w3',
- 'types': [
- 'City',
- 'Town'
- ]
- },
- ],
- 'out': []
- },
- 'dc/MadDcid': {
- 'in': [],
- 'out': []
- }
- })
- return MockResponse(json.dumps({'payload': res_json}))
- if data['dcids'] == ['dc/MadDcid', 'dc/MadderDcid']:
- # Response for sending a request where both dcids do not exist.
- res_json = json.dumps({
- 'dc/MadDcid': {
- 'in': [],
- 'out': []
- },
- 'dc/MadderDcid': {
- 'in': [],
- 'out': []
- }
- })
- return MockResponse(json.dumps({'payload': res_json}))
- if data['dcids'] == [] and data['property'] == 'containedInPlace':
- # Response for sending a request where no dcids are given.
- res_json = json.dumps({})
- return MockResponse(json.dumps({'payload': res_json}))
-
- # Mock responses for urlopen requests to get_triples
- if req.get_full_url() == utils._API_ROOT + utils._API_ENDPOINTS['get_triples']:
- if data['dcids'] == ['geoId/06085', 'geoId/24031']:
- # Response for sending a request with two valid dcids.
- res_json = json.dumps({
- 'geoId/06085': [
- {
- "subjectId": "geoId/06085",
- "predicate": "name",
- "objectValue": "Santa Clara County"
- },
- {
- "subjectId": "geoId/0649670",
- "subjectName": "Mountain View",
- "subjectTypes": [
- "City"
- ],
- "predicate": "containedInPlace",
- "objectId": "geoId/06085",
- "objectName": "Santa Clara County"
- },
- {
- "subjectId": "geoId/06085",
- "predicate": "containedInPlace",
- "objectId": "geoId/06",
- "objectName": "California"
- },
- ],
- 'geoId/24031': [
- {
- "subjectId": "geoId/24031",
- "predicate": "name",
- "objectValue": "Montgomery County"
- },
- {
- "subjectId": "geoId/2467675",
- "subjectName": "Rockville",
- "subjectTypes": [
- "City"
- ],
- "predicate": "containedInPlace",
- "objectId": "geoId/24031",
- "objectName": "Montgomery County"
- },
- {
- "subjectId": "geoId/24031",
- "predicate": "containedInPlace",
- "objectId": "geoId/24",
- "objectName": "Maryland"
- },
- ]
- })
- return MockResponse(json.dumps({'payload': res_json}))
- if data['dcids'] == ['geoId/06085', 'dc/MadDcid']:
- # Response for sending a request where one dcid does not exist.
- res_json = json.dumps({
- 'geoId/06085': [
- {
- "subjectId": "geoId/06085",
- "predicate": "name",
- "objectValue": "Santa Clara County"
- },
- {
- "subjectId": "geoId/0649670",
- "subjectName": "Mountain View",
- "subjectTypes": [
- "City"
- ],
- "predicate": "containedInPlace",
- "objectId": "geoId/06085",
- "objectName": "Santa Clara County"
- },
- {
- "subjectId": "geoId/06085",
- "predicate": "containedInPlace",
- "objectId": "geoId/06",
- "objectName": "California"
- },
- ],
- 'dc/MadDcid': []
- })
- return MockResponse(json.dumps({'payload': res_json}))
- if data['dcids'] == ['dc/MadDcid', 'dc/MadderDcid']:
- # Response for sending a request where both dcids do not exist.
- res_json = json.dumps({
- 'dc/MadDcid': [],
- 'dc/MadderDcid': []
- })
- return MockResponse(json.dumps({'payload': res_json}))
- if data['dcids'] == []:
- # Response for sending a request where no dcids are given.
- res_json = json.dumps({})
- return MockResponse(json.dumps({'payload': res_json}))
-
- # Otherwise, return an empty response and a 404.
- return urllib.error.HTTPError(None, 404, None, None, None)
-
-
-class TestGetPropertyLabels(unittest.TestCase):
- """ Unit tests for get_property_labels. """
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_single_dcid(self, urlopen_mock):
- """ Calling get_property_labels with a single dcid returns a valid
- result.
- """
- # Test for outgoing property labels
- out_props = dc.get_property_labels(['geoId/0649670'])
- self.assertDictEqual(out_props,
- {'geoId/0649670': ["containedInPlace", "name", "geoId", "typeOf"]})
-
- # Test with out=False
- in_props = dc.get_property_labels(['geoId/0649670'], out=False)
- self.assertDictEqual(in_props, {'geoId/0649670': []})
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_multiple_dcids(self, urlopen_mock):
- """ Calling get_property_labels returns valid results with multiple
- dcids.
- """
- dcids = ['State', 'County', 'City']
- expected_in = ["typeOf"]
- expected_out = ["name", "provenance", "subClassOf", "typeOf", "url"]
-
- # Test for outgoing property labels
- out_props = dc.get_property_labels(dcids)
- self.assertDictEqual(out_props, {
- 'State': expected_out,
- 'County': expected_out,
- 'City': expected_out,
- })
-
- # Test for incoming property labels
- in_props = dc.get_property_labels(dcids, out=False)
- self.assertDictEqual(in_props, {
- 'State': expected_in,
- 'County': expected_in,
- 'City': expected_in,
- })
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_bad_dcids(self, urlopen_mock):
- """ Calling get_property_labels with dcids that do not exist returns empty
- results.
- """
- # Test for outgoing property labels
- out_props = dc.get_property_labels(['dc/MadDcid'])
- self.assertDictEqual(out_props, {'dc/MadDcid': []})
-
- # Test for incoming property labels
- in_props = dc.get_property_labels(['dc/MadDcid'], out=False)
- self.assertDictEqual(in_props, {'dc/MadDcid': []})
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_no_dcids(self, urlopen_mock):
- """ Calling get_property_labels with no dcids returns empty results. """
-
- # Test for outgoing property labels
- out_props = dc.get_property_labels([])
- self.assertDictEqual(out_props, {})
-
- # Test for incoming property labels
- in_props = dc.get_property_labels([], out=False)
- self.assertDictEqual(in_props, {})
-
-
-class TestGetPropertyValues(unittest.TestCase):
- """ Unit tests for get_property_values. """
-
- # --------------------------- STANDARD UNIT TESTS ---------------------------
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_multiple_dcids(self, urlopen_mock):
- """ Calling get_property_values with multiple dcids returns valid
- results.
- """
- dcids = ['geoId/06085', 'geoId/24031']
-
- # Get the containedInPlace Towns for Santa Clara and Montgomery County.
- towns = dc.get_property_values(
- dcids, 'containedInPlace', out=False, value_type='Town')
- self.assertDictEqual(towns, {
- 'geoId/06085': ['geoId/0643294', 'geoId/0644112'],
- 'geoId/24031': ['geoId/2462850']
- })
-
- dcids = ['geoId/06085', 'geoId/24031', float('nan')]
- # Handle NaN values
- towns = dc.get_property_values(
- dcids, 'containedInPlace', out=False, value_type='Town')
- self.assertDictEqual(towns, {
- 'geoId/06085': ['geoId/0643294', 'geoId/0644112'],
- 'geoId/24031': ['geoId/2462850']
- })
-
- # Get the name of Santa Clara and Montgomery County.
- names = dc.get_property_values(dcids, 'name')
- self.assertDictEqual(names, {
- 'geoId/06085': ['Santa Clara County'],
- 'geoId/24031': ['Montgomery County']
- })
-
- # Return empty result when there is no data.
- names = dc.get_property_values(['dc/p/1234'], 'name')
- self.assertDictEqual(names, {
- 'dc/p/1234': []
- })
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_bad_dcids(self, urlopen_mock):
- """ Calling get_property_values with dcids that do not exist returns empty
- results.
- """
- bad_dcids_1 = ['geoId/06085', 'dc/MadDcid']
- bad_dcids_2 = ['dc/MadDcid', 'dc/MadderDcid']
-
- # Get entities containedInPlace of Santa Clara County and a dcid that does
- # not exist.
- contained_1 = dc.get_property_values(bad_dcids_1, 'containedInPlace', out=False)
- self.assertDictEqual(contained_1, {
- 'geoId/06085': ['geoId/0644112'],
- 'dc/MadDcid': []
- })
-
- # Get entities containedInPlace for two dcids that do not exist.
- contained_2 = dc.get_property_values(bad_dcids_2, 'containedInPlace')
- self.assertDictEqual(contained_2, {
- 'dc/MadDcid': [],
- 'dc/MadderDcid': []
- })
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_bad_property(self, urlopen_mock):
- """ Calling get_property_values with a property that does not exist returns
- empty results.
- """
- # Get propery values for a property that does not exist.
- prop_vals = dc.get_property_values(
- ['geoId/06085', 'geoId/24031'], 'madProperty')
- self.assertDictEqual(prop_vals, {
- 'geoId/06085': [],
- 'geoId/24031': []
- })
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_no_dcids(self, urlopen_mock):
- """ Calling get_property_values with no dcids returns empty results. """
- # Get property values with an empty list of dcids.
- prop_vals = dc.get_property_values([], 'containedInPlace')
- self.assertDictEqual(prop_vals, {})
-
-class TestGetTriples(unittest.TestCase):
- """ Unit tests for get_triples. """
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_multiple_dcids(self, urlopen_mock):
- """ Calling get_triples with proper dcids returns valid results. """
- # Call get_triples
- triples = dc.get_triples(['geoId/06085', 'geoId/24031'])
- self.assertDictEqual(triples, {
- 'geoId/06085': [
- ('geoId/06085', 'name', 'Santa Clara County'),
- ('geoId/0649670', 'containedInPlace', 'geoId/06085'),
- ('geoId/06085', 'containedInPlace', 'geoId/06'),
- ],
- 'geoId/24031': [
- ('geoId/24031', 'name', 'Montgomery County'),
- ('geoId/2467675', 'containedInPlace', 'geoId/24031'),
- ('geoId/24031', 'containedInPlace', 'geoId/24'),
- ]
- })
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_bad_dcids(self, urlopen_mock):
- """ Calling get_triples with dcids that do not exist returns empty
- results.
- """
- # Call get_triples where one dcid does not exist
- triples_1 = dc.get_triples(['geoId/06085', 'dc/MadDcid'])
- self.assertDictEqual(triples_1, {
- 'geoId/06085': [
- ('geoId/06085', 'name', 'Santa Clara County'),
- ('geoId/0649670', 'containedInPlace', 'geoId/06085'),
- ('geoId/06085', 'containedInPlace', 'geoId/06'),
- ],
- 'dc/MadDcid': []
- })
-
- # Call get_triples where both dcids do not exist
- triples_1 = dc.get_triples(['dc/MadDcid', 'dc/MadderDcid'])
- self.assertDictEqual(triples_1, {
- 'dc/MadDcid': [],
- 'dc/MadderDcid': []
- })
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_no_dcids(self, urlopen_mock):
- """ Calling get_triples with no dcids returns empty results. """
- # Call get_triples with no dcids
- triples_1 = dc.get_triples([])
- self.assertDictEqual(triples_1, {})
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/datacommons/test/places_test.py b/datacommons/test/places_test.py
deleted file mode 100644
index d4147655..00000000
--- a/datacommons/test/places_test.py
+++ /dev/null
@@ -1,473 +0,0 @@
-# Copyright 2017 Google Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-""" Data Commons Python API unit tests.
-
-Unit tests for Place methods in the Data Commons Python API.
-"""
-
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
-try:
- from unittest.mock import patch
-except ImportError:
- from mock import patch
-
-import datacommons as dc
-import datacommons.utils as utils
-import json
-import unittest
-import six.moves.urllib as urllib
-
-
-def request_mock(*args, **kwargs):
- """ A mock urlopen requests sent in the requests package. """
- # Create the mock response object.
- class MockResponse:
- def __init__(self, json_data):
- self.json_data = json_data
-
- def read(self):
- return self.json_data
-
- req = args[0]
- data = json.loads(req.data)
-
- # Mock responses for urlopen requests to get_places_in.
- if req.get_full_url() == utils._API_ROOT + utils._API_ENDPOINTS['get_places_in']:
- if (data['dcids'] == ['geoId/06085', 'geoId/24031']
- and data['place_type'] == 'City'):
- # Response returned when querying for multiple valid dcids.
- res_json = json.dumps([
- {
- 'dcid': 'geoId/06085',
- 'place': 'geoId/0649670',
- },
- {
- 'dcid': 'geoId/24031',
- 'place': 'geoId/2467675',
- },
- {
- 'dcid': 'geoId/24031',
- 'place': 'geoId/2476650',
- },
- ])
- return MockResponse(json.dumps({'payload': res_json}))
- if (data['dcids'] == ['geoId/06085', 'dc/MadDcid']
- and data['place_type'] == 'City'):
- # Response returned when querying for a dcid that does not exist.
- res_json = json.dumps([
- {
- 'dcid': 'geoId/06085',
- 'place': 'geoId/0649670',
- },
- ])
- return MockResponse(json.dumps({'payload': res_json}))
- if data['dcids'] == ['dc/MadDcid', 'dc/MadderDcid']\
- and data['place_type'] == 'City':
- # Response returned when both given dcids do not exist.
- res_json = json.dumps([])
- return MockResponse(json.dumps({'payload': res_json}))
- if data['dcids'] == [] and data['place_type'] == 'City':
- res_json = json.dumps([])
- # Response returned when no dcids are given.
- return MockResponse(json.dumps({'payload': res_json}))
-
-
- # Mock responses for urlopen requests to get_stats.
- if req.get_full_url() == utils._API_ROOT + utils._API_ENDPOINTS['get_stats']:
- if (data['place'] == ['geoId/05', 'geoId/06'] and
- data['stats_var'] == 'dc/0hyp6tkn18vcb'):
- # Response returned when querying for multiple valid dcids.
- res_json = json.dumps({
- 'geoId/05': {
- 'data': {
- '2011': 18136,
- '2012': 17279,
- '2013': 17459,
- '2014': 16966,
- '2015': 17173,
- '2016': 17041,
- '2017': 17783,
- '2018': 18003
- },
- 'place_name': 'Arkansas'
- },
- 'geoId/06': {
- 'data': {
- '2011': 316667,
- '2012': 324116,
- '2013': 331853,
- '2014': 342818,
- '2015': 348979,
- '2016': 354806,
- '2017': 360645,
- '2018': 366331
- },
- 'place_name': 'California'
- }
- })
- return MockResponse(json.dumps({'payload': res_json}))
- if (data['place'] == ['geoId/00'] and
- data['stats_var'] == 'dc/0hyp6tkn18vcb'):
- # No data for the request
- res_json = json.dumps({
- 'geoId/00': None
- })
- return MockResponse(json.dumps({'payload': res_json}))
- if ((data['place'] == ['geoId/05', 'dc/MadDcid'] or
- data['place'] == ['geoId/05']) and
- data['stats_var'] == 'dc/0hyp6tkn18vcb'):
- # Response ignores dcid that does not exist.
- res_json = json.dumps({
- 'geoId/05': {
- 'data': {
- '2011': 18136,
- '2012': 17279,
- '2013': 17459,
- '2014': 16966,
- '2015': 17173,
- '2016': 17041,
- '2017': 17783,
- '2018': 18003
- },
- 'place_name': 'Arkansas'
- }
- })
- return MockResponse(json.dumps({'payload': res_json}))
- if (data['place'] == ['geoId/06'] and
- data['stats_var'] == 'dc/0hyp6tkn18vcb'):
- res_json = json.dumps({
- 'geoId/06': {
- 'data': {
- '2011': 316667,
- '2012': 324116,
- '2013': 331853,
- '2014': 342818,
- '2015': 348979,
- '2016': 354806,
- '2017': 360645,
- '2018': 366331
- },
- 'place_name': 'California'
- }
- })
- return MockResponse(json.dumps({'payload': res_json}))
- if (data['place'] == ['dc/MadDcid', 'dc/MadderDcid'] and
- data['stats_var'] == 'dc/0hyp6tkn18vcb'):
- # Response returned when both given dcids do not exist.
- res_json = json.dumps({})
- return MockResponse(json.dumps({'payload': res_json}))
- if data['place'] == [] and data['stats_var'] == 'dc/0hyp6tkn18vcb':
- res_json = json.dumps({})
- # Response returned when no dcids are given.
- return MockResponse(json.dumps({'payload': res_json}))
- if (data['place'] == ['geoId/48'] and
- data['stats_var'] == 'dc/0hyp6tkn18vcb'):
- if (data.get('measurement_method') == 'MM1' and
- data.get('unit') == 'Inch' and
- data.get('observation_period') == 'P1Y'):
- res_json = json.dumps({
- 'geoId/48': {
- 'data': {
- '2015': 1,
- '2016': 1,
- },
- 'place_name': 'Texas'
- }
- })
- elif data.get('measurement_method') == 'MM1':
- res_json = json.dumps({
- 'geoId/48': {
- 'data': {
- '2015': 2,
- '2016': 2,
- },
- 'place_name': 'Texas'
- }
- })
- else:
- res_json = json.dumps({
- 'geoId/48': {
- 'data': {
- '2015': 3,
- '2016': 3,
- },
- 'place_name': 'Texas'
- }
- })
-
- return MockResponse(json.dumps({'payload': res_json}))
-
- # Otherwise, return an empty response and a 404.
- return urllib.error.HTTPError(None, 404, None, None, None)
-
-class TestGetPlacesIn(unittest.TestCase):
- """ Unit stests for get_places_in. """
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_multiple_dcids(self, urlopen):
- """ Calling get_places_in with proper dcids returns valid results. """
- # Call get_places_in
- places = dc.get_places_in(['geoId/06085', 'geoId/24031'], 'City')
- self.assertDictEqual(places, {
- 'geoId/06085': ['geoId/0649670'],
- 'geoId/24031': ['geoId/2467675', 'geoId/2476650']
- })
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_bad_dcids(self, urlopen):
- """ Calling get_places_in with dcids that do not exist returns empty
- results.
- """
- # Call get_places_in with one dcid that does not exist
- bad_dcids_1 = dc.get_places_in(['geoId/06085', 'dc/MadDcid'], 'City')
- self.assertDictEqual(bad_dcids_1, {
- 'geoId/06085': ['geoId/0649670'],
- 'dc/MadDcid': []
- })
-
- # Call get_places_in when both dcids do not exist
- bad_dcids_2 = dc.get_places_in(['dc/MadDcid', 'dc/MadderDcid'], 'City')
- self.assertDictEqual(bad_dcids_2, {
- 'dc/MadDcid': [],
- 'dc/MadderDcid': []
- })
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_no_dcids(self, urlopen):
- """ Calling get_places_in with no dcids returns empty results. """
- # Call get_places_in with no dcids.
- bad_dcids = dc.get_places_in(['dc/MadDcid', 'dc/MadderDcid'], 'City')
- self.assertDictEqual(bad_dcids, {
- 'dc/MadDcid': [],
- 'dc/MadderDcid': []
- })
-
-
-class TestGetStats(unittest.TestCase):
- """ Unit stests for get_stats. """
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_multiple_dcids(self, urlopen):
- """ Calling get_stats with proper dcids returns valid results. """
- # Call get_stats
- stats = dc.get_stats(['geoId/05', 'geoId/06'], 'dc/0hyp6tkn18vcb', 'all')
- self.assertDictEqual(
- stats, {
- 'geoId/05': {
- 'data': {
- '2011': 18136,
- '2012': 17279,
- '2013': 17459,
- '2014': 16966,
- '2015': 17173,
- '2016': 17041,
- '2017': 17783,
- '2018': 18003
- },
- 'place_name': 'Arkansas'
- },
- 'geoId/06': {
- 'data': {
- '2011': 316667,
- '2012': 324116,
- '2013': 331853,
- '2014': 342818,
- '2015': 348979,
- '2016': 354806,
- '2017': 360645,
- '2018': 366331
- },
- 'place_name': 'California'
- }
- })
-
- # Call get_stats for latest obs
- stats = dc.get_stats(['geoId/05', 'geoId/06'], 'dc/0hyp6tkn18vcb', 'latest')
- self.assertDictEqual(
- stats, {
- 'geoId/05': {
- 'data': {
- '2018': 18003
- },
- 'place_name': 'Arkansas'
- },
- 'geoId/06': {
- 'data': {
- '2018': 366331
- },
- 'place_name': 'California'
- }
- })
-
- # Call get_stats for specific obs
- stats = dc.get_stats(['geoId/05', 'geoId/06'], 'dc/0hyp6tkn18vcb', ['2013', '2018'])
- self.assertDictEqual(
- stats, {
- 'geoId/05': {
- 'data': {
- '2013': 17459,
- '2018': 18003
- },
- 'place_name': 'Arkansas'
- },
- 'geoId/06': {
- 'data': {
- '2013': 331853,
- '2018': 366331
- },
- 'place_name': 'California'
- }
- })
-
- # Call get_stats -- dates must be in interable
- stats = dc.get_stats(['geoId/05', 'geoId/06'], 'dc/0hyp6tkn18vcb', '2018')
- self.assertDictEqual(
- stats, {
- 'geoId/05': {
- 'data': {
- },
- 'place_name': 'Arkansas'
- },
- 'geoId/06': {
- 'data': {
- },
- 'place_name': 'California'
- }
- })
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_opt_args(self, urlopen):
- """ Calling get_stats with mmethod, unit, and obs period returns specific data.
- """
- # Set the API key
- dc.set_api_key('TEST-API-KEY')
-
- # Call get_stats with all optional args
- stats = dc.get_stats(['geoId/48'], 'dc/0hyp6tkn18vcb', 'latest', 'MM1',
- 'Inch', 'P1Y')
- self.assertDictEqual(
- stats, {
- 'geoId/48': {
- 'data': {
- '2016': 1
- },
- 'place_name': 'Texas'
- }
- })
-
- # Call get_stats with mmethod specified
- stats = dc.get_stats(['geoId/48'], 'dc/0hyp6tkn18vcb', 'latest', 'MM1')
- self.assertDictEqual(
- stats, {
- 'geoId/48': {
- 'data': {
- '2016': 2
- },
- 'place_name': 'Texas'
- }
- })
-
- # Call get_stats without optional args
- stats = dc.get_stats(['geoId/48'], 'dc/0hyp6tkn18vcb', 'latest')
- self.assertDictEqual(
- stats, {
- 'geoId/48': {
- 'data': {
- '2016': 3
- },
- 'place_name': 'Texas'
- }
- })
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_bad_dcids(self, urlopen):
- """ Calling get_stats with dcids that do not exist returns empty
- results.
- """
- # Call get_stats with one dcid that does not exist
- bad_dcids_1 = dc.get_stats(['geoId/05', 'dc/MadDcid'], 'dc/0hyp6tkn18vcb')
- self.assertDictEqual(
- bad_dcids_1, {
- 'geoId/05': {
- 'data': {
- '2018': 18003
- },
- 'place_name': 'Arkansas'
- }
- })
-
- # Call get_stats when both dcids do not exist
- bad_dcids_2 = dc.get_stats(['dc/MadDcid', 'dc/MadderDcid'],
- 'dc/0hyp6tkn18vcb')
- self.assertDictEqual({}, bad_dcids_2)
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_no_dcids(self, urlopen):
- """ Calling get_stats with no dcids returns empty results. """
- # Call get_stats with no dcids.
- no_dcids = dc.get_stats([], 'dc/0hyp6tkn18vcb')
- self.assertDictEqual({}, no_dcids)
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_no_data(self, urlopen):
- """ Calling get_stats with for None data. """
- # Call get_stats with no dcids.
- result = dc.get_stats(['geoId/00'], 'dc/0hyp6tkn18vcb')
- self.assertDictEqual({}, result)
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_batch_request(self, mock_urlopen):
- """ Make multiple calls to REST API when number of geos exceeds the batch size. """
- save_batch_size = dc.utils._QUERY_BATCH_SIZE
- dc.utils._QUERY_BATCH_SIZE = 1
-
- self.assertEqual(0, mock_urlopen.call_count)
- stats = dc.get_stats(['geoId/05'], 'dc/0hyp6tkn18vcb', 'latest')
- self.assertDictEqual(
- stats, {
- 'geoId/05': {
- 'data': {
- '2018': 18003
- },
- 'place_name': 'Arkansas'
- },
- })
- self.assertEqual(1, mock_urlopen.call_count)
-
- stats = dc.get_stats(['geoId/05', 'geoId/06'], 'dc/0hyp6tkn18vcb', 'latest')
- self.assertDictEqual(
- stats, {
- 'geoId/05': {
- 'data': {
- '2018': 18003
- },
- 'place_name': 'Arkansas'
- },
- 'geoId/06': {
- 'data': {
- '2018': 366331
- },
- 'place_name': 'California'
- }
- })
- self.assertEqual(3, mock_urlopen.call_count)
-
- dc.utils._QUERY_BATCH_SIZE = save_batch_size
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/datacommons/test/populations_test.py b/datacommons/test/populations_test.py
deleted file mode 100644
index f1fd3323..00000000
--- a/datacommons/test/populations_test.py
+++ /dev/null
@@ -1,383 +0,0 @@
-# Copyright 2017 Google Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-""" Data Commons Python API unit tests.
-
-Unit tests for Population and Observation methods in the Data Commons Python
-Client API.
-"""
-
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
-import base64
-
-try:
- from unittest.mock import patch
-except ImportError:
- from mock import patch
-
-import datacommons as dc
-import datacommons.utils as utils
-import json
-import unittest
-import six.moves.urllib as urllib
-import zlib
-
-
-def request_mock(*args, **kwargs):
- """ A mock urlopen request sent in the requests package. """
- # Create the mock response object.
- class MockResponse:
- def __init__(self, json_data):
- self.json_data = json_data
-
- def read(self):
- return self.json_data
-
- # Get the request json and allowed constraining properties
- req = args[0]
- if req.data:
- data = json.loads(req.data)
-
- constrained_props = [
- {
- 'property': 'placeOfBirth',
- 'value': 'BornInOtherStateInTheUnitedStates'
- },
- {
- 'property': 'age',
- 'value': 'Years5To17'
- }
- ]
-
- def compare_constraint_helper(constrained_props, data_pvs):
- """Py2 workaround for unicode vs str comparison."""
- for cpv in constrained_props:
- satisfied = False
- for k, v in cpv.items():
- for pv in data_pvs:
- if k in pv and pv[k] == cpv[k]:
- print("found ", k, v, " in", pv)
- satisfied = True
- if satisfied == False:
- return False
- return True
-
- # Mock responses for urlopen request to get_populations.
- if req.get_full_url() == utils._API_ROOT + utils._API_ENDPOINTS['get_populations']\
- and data['population_type'] == 'Person'\
- and compare_constraint_helper(constrained_props, data['pvs']):
- if data['dcids'] == ['geoId/06085', 'geoId/4805000']:
- # Response returned when querying for multiple valid dcids.
- res_json = json.dumps([
- {
- 'dcid': 'geoId/06085',
- 'population': 'dc/p/crgfn8blpvl35'
- },
- {
- 'dcid': 'geoId/4805000',
- 'population': 'dc/p/f3q9whmjwbf36'
- }
- ])
- return MockResponse(json.dumps({'payload': res_json}))
- if data['dcids'] == ['geoId/06085', 'dc/MadDcid']:
- # Response returned when querying for a dcid that does not exist.
- res_json = json.dumps([
- {
- 'dcid': 'geoId/06085',
- 'population': 'dc/p/crgfn8blpvl35'
- },
- ])
- return MockResponse(json.dumps({'payload': res_json}))
- if data['dcids'] == ['dc/MadDcid', 'dc/MadderDcid'] or data['dcids'] == []:
- # Response returned when both given dcids do not exist or no dcids are
- # provided to the method.
- res_json = json.dumps([])
- return MockResponse(json.dumps({'payload': res_json}))
-
- # Mock responses for urlopen request to get_observations
- if req.get_full_url() == utils._API_ROOT + utils._API_ENDPOINTS['get_observations']\
- and data['measured_property'] == 'count'\
- and data['stats_type'] == 'measuredValue'\
- and data['observation_date'] == '2018-12'\
- and data['observation_period'] == 'P1M'\
- and data['measurement_method'] == 'BLSSeasonallyAdjusted':
- if data['dcids'] == ['dc/p/x6t44d8jd95rd', 'dc/p/lr52m1yr46r44', 'dc/p/fs929fynprzs']:
- # Response returned when querying for multiple valid dcids.
- res_json = json.dumps([
- {
- 'dcid': 'dc/p/x6t44d8jd95rd',
- 'observation': '18704962.000000'
- },
- {
- 'dcid': 'dc/p/lr52m1yr46r44',
- 'observation': '3075662.000000'
- },
- {
- 'dcid': 'dc/p/fs929fynprzs',
- 'observation': '1973955.000000'
- }
- ])
- return MockResponse(json.dumps({'payload': res_json}))
- if data['dcids'] == ['dc/p/x6t44d8jd95rd', 'dc/MadDcid']:
- # Response returned when querying for a dcid that does not exist.
- res_json = json.dumps([
- {
- 'dcid': 'dc/p/x6t44d8jd95rd',
- 'observation': '18704962.000000'
- },
- ])
- return MockResponse(json.dumps({'payload': res_json}))
- if data['dcids'] == ['dc/MadDcid', 'dc/MadderDcid'] or data['dcids'] == []:
- # Response returned when both given dcids do not exist or no dcids are
- # provided to the method.
- res_json = json.dumps([])
- return MockResponse(json.dumps({'payload': res_json}))
-
- # Mock responses for urlopen request to get_place_obs
- if req.get_full_url() == utils._API_ROOT + utils._API_ENDPOINTS['get_place_obs']\
- and data['place_type'] == 'City'\
- and data['observation_date'] == '2017'\
- and data['population_type'] == 'Person'\
- and compare_constraint_helper(constrained_props, data['pvs']):
- res_json = json.dumps({
- 'places': [
- {
- 'name': 'Marcus Hook borough',
- 'place': 'geoId/4247344',
- 'populations': {
- 'dc/p/pq6frs32sfvk': {
- 'observations': [
- {
- 'marginOfError': 39,
- 'measuredProp': 'count',
- 'measuredValue': 67,
- }
- ],
- }
- }
- }
- ]
- })
- return MockResponse(json.dumps(
- {'payload': base64.b64encode(zlib.compress(res_json.encode('utf-8'))).decode('ascii')}))
-
- # Mock responses for get requests to get_pop_obs.
- if req.get_full_url() == utils._API_ROOT + utils._API_ENDPOINTS['get_pop_obs'] + '?dcid=geoId/06085':
- # Response returned when querying for a city in the graph.
- res_json = json.dumps({
- 'name': 'Mountain View',
- 'placeType': 'City',
- 'populations': {
- 'dc/p/013ldrstf6lnf': {
- 'numConstraints': 6,
- 'observations': [
- {
- 'marginOfError': 119,
- 'measuredProp': 'count',
- 'measuredValue': 225,
- 'measurementMethod': 'CensusACS5yrSurvey',
- 'observationDate': '2014'
- }, {
- 'marginOfError': 108,
- 'measuredProp': 'count',
- 'measuredValue': 180,
- 'measurementMethod': 'CensusACS5yrSurvey',
- 'observationDate': '2012'
- }
- ],
- 'popType': 'Person',
- 'propertyValues': {
- 'age': 'Years16Onwards',
- 'gender': 'Male',
- 'income': 'USDollar30000To34999',
- 'incomeStatus': 'WithIncome',
- 'race': 'USC_HispanicOrLatinoRace',
- 'workExperience': 'USC_NotWorkedFullTime'
- }
- }
- }
- })
- return MockResponse(json.dumps(
- {'payload': base64.b64encode(zlib.compress(res_json.encode('utf-8'))).decode('ascii')}))
-
- # Otherwise, return an empty response and a 404.
- return urllib.error.HTTPError(None, 404, None, None, None)
-
-class TestGetPopulations(unittest.TestCase):
- """ Unit tests for get_populations. """
-
- _constraints = {
- 'placeOfBirth': 'BornInOtherStateInTheUnitedStates',
- 'age': 'Years5To17'
- }
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_multiple_dcids(self, urlopen):
- """ Calling get_populations with proper dcids returns valid results. """
- # Call get_populations
- populations = dc.get_populations(['geoId/06085', 'geoId/4805000'], 'Person',
- constraining_properties=self._constraints)
- self.assertDictEqual(populations, {
- 'geoId/06085': 'dc/p/crgfn8blpvl35',
- 'geoId/4805000': 'dc/p/f3q9whmjwbf36'
- })
-
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_bad_dcids(self, urlopen):
- """ Calling get_populations with dcids that do not exist returns empty
- results.
- """
- # Call get_populations
- pops_1 = dc.get_populations(['geoId/06085', 'dc/MadDcid'], 'Person',
- constraining_properties=self._constraints)
- pops_2 = dc.get_populations(['dc/MadDcid', 'dc/MadderDcid'], 'Person',
- constraining_properties=self._constraints)
-
- # Verify the results
- self.assertDictEqual(pops_1, {'geoId/06085': 'dc/p/crgfn8blpvl35'})
- self.assertDictEqual(pops_2, {})
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_no_dcids(self, urlopen):
- """ Calling get_populations with no dcids returns empty results. """
- pops = dc.get_populations(
- [], 'Person', constraining_properties=self._constraints)
- self.assertDictEqual(pops, {})
-
-class TestGetObservations(unittest.TestCase):
- """ Unit tests for get_observations. """
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_multiple_dcids(self, urlopen):
- """ Calling get_observations with proper dcids returns valid results. """
- dcids = ['dc/p/x6t44d8jd95rd', 'dc/p/lr52m1yr46r44', 'dc/p/fs929fynprzs']
- expected = {
- 'dc/p/lr52m1yr46r44': 3075662.0,
- 'dc/p/fs929fynprzs': 1973955.0,
- 'dc/p/x6t44d8jd95rd': 18704962.0
- }
- actual = dc.get_observations(dcids, 'count', 'measuredValue', '2018-12',
- observation_period='P1M',
- measurement_method='BLSSeasonallyAdjusted')
- self.assertDictEqual(actual, expected)
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_bad_dcids(self, urlopen):
- """ Calling get_observations with dcids that do not exist returns empty
- results.
- """
- # Get the input
- dcids_1 = ['dc/p/x6t44d8jd95rd', 'dc/MadDcid']
- dcids_2 = ['dc/MadDcid', 'dc/MadderDcid']
-
- # Call get_observations
- actual_1 = dc.get_observations(dcids_1, 'count', 'measuredValue', '2018-12',
- observation_period='P1M',
- measurement_method='BLSSeasonallyAdjusted')
- actual_2 = dc.get_observations(dcids_2, 'count', 'measuredValue', '2018-12',
- observation_period='P1M',
- measurement_method='BLSSeasonallyAdjusted')
-
- # Verify the results
- self.assertDictEqual(actual_1, {'dc/p/x6t44d8jd95rd': 18704962.0})
- self.assertDictEqual(actual_2, {})
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_no_dcids(self, urlopen):
- """ Calling get_observations with no dcids returns empty results. """
- actual = dc.get_observations([], 'count', 'measuredValue', '2018-12',
- observation_period='P1M',
- measurement_method='BLSSeasonallyAdjusted')
- self.assertDictEqual(actual, {})
-
-
-class TestGetPopObs(unittest.TestCase):
- """ Unit tests for get_pop_obs. """
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_valid_dcid(self, urlopen):
- """ Calling get_pop_obs with valid dcid returns valid results. """
- # Call get_pop_obs
- pop_obs = dc.get_pop_obs('geoId/06085')
- self.assertDictEqual(pop_obs, {
- 'name': 'Mountain View',
- 'placeType': 'City',
- 'populations': {
- 'dc/p/013ldrstf6lnf': {
- 'numConstraints': 6,
- 'observations': [
- {
- 'marginOfError': 119,
- 'measuredProp': 'count',
- 'measuredValue': 225,
- 'measurementMethod': 'CensusACS5yrSurvey',
- 'observationDate': '2014'
- }, {
- 'marginOfError': 108,
- 'measuredProp': 'count',
- 'measuredValue': 180,
- 'measurementMethod': 'CensusACS5yrSurvey',
- 'observationDate': '2012'
- }
- ],
- 'popType': 'Person',
- 'propertyValues': {
- 'age': 'Years16Onwards',
- 'gender': 'Male',
- 'income': 'USDollar30000To34999',
- 'incomeStatus': 'WithIncome',
- 'race': 'USC_HispanicOrLatinoRace',
- 'workExperience': 'USC_NotWorkedFullTime'
- }
- }
- }
- })
-
-class TestGetPlaceObs(unittest.TestCase):
- """ Unit tests for get_place_obs. """
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_valid(self, urlopen):
- """ Calling get_place_obs with valid parameters returns a valid result. """
- # Call get_place_obs
- pvs = {
- 'placeOfBirth': 'BornInOtherStateInTheUnitedStates',
- 'age': 'Years5To17'
- }
- place_obs = dc.get_place_obs(
- 'City', '2017', 'Person', constraining_properties=pvs)
- self.assertListEqual(place_obs, [
- {
- 'name': 'Marcus Hook borough',
- 'place': 'geoId/4247344',
- 'populations': {
- 'dc/p/pq6frs32sfvk': {
- 'observations': [
- {
- 'marginOfError': 39,
- 'measuredProp': 'count',
- 'measuredValue': 67,
- }
- ],
- }
- }
- }
- ])
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/datacommons/test/query_test.py b/datacommons/test/query_test.py
deleted file mode 100644
index 75b12a66..00000000
--- a/datacommons/test/query_test.py
+++ /dev/null
@@ -1,176 +0,0 @@
-# Copyright 2017 Google Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-""" Data Commons Python API unit tests.
-
-Unit tests for the SPARQL query wrapper.
-"""
-
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
-try:
- from unittest.mock import patch
-except ImportError:
- from mock import patch
-
-import datacommons as dc
-import datacommons.utils as utils
-
-import json
-import unittest
-import six.moves.urllib as urllib
-
-
-def request_mock(*args, **kwargs):
- """ A mock urlopen call sent in the urllib package. """
- # Create the mock response object.
- class MockResponse:
- def __init__(self, json_data):
- self.json_data = json_data
-
- def read(self):
- return self.json_data
-
- # The accepted query.
- accepted_query = ('''
-SELECT ?name ?dcid
-WHERE {
- ?a typeOf Place .
- ?a name ?name .
- ?a dcid ("geoId/06" "geoId/21" "geoId/24") .
- ?a dcid ?dcid
-}
-''')
-
- accepted_query2 = ('''
-SELECT ?name ?dcid
-WHERE {
- ?a typeOf Place .
- ?a name ?name .
- ?a dcid ("geoId/DNE") .
- ?a dcid ?dcid
-}
-''')
- req = args[0]
- data = json.loads(req.data)
-
- if req.get_full_url() == utils._API_ROOT + utils._API_ENDPOINTS['query']:
- if data['sparql'] == accepted_query:
- return MockResponse(json.dumps({
- 'header': [
- '?name',
- '?dcid'
- ],
- 'rows': [
- {
- 'cells': [
- {
- 'value': 'California'
- },
- {
- 'value': 'geoId/06'
- }
- ]
- },
- {
- 'cells': [
- {
- 'value': 'Kentucky'
- },
- {
- 'value': 'geoId/21'
- }
- ]
- },
- {
- 'cells': [
- {
- 'value': 'Maryland'
- },
- {
- 'value': 'geoId/24'
- }
- ]
- }
- ]
- }))
- elif data['sparql'] == accepted_query2:
- return MockResponse(json.dumps({
- 'header': [
- '?name',
- '?dcid'
- ],
- }))
-
- # Otherwise, return an empty response and a 404.
- return urllib.error.HTTPError(None, 404, None, None, None)
-
-
-class TestQuery(unittest.TestCase):
- """ Unit tests for the Query object. """
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_rows(self, urlopen):
- """ Sending a valid query returns the correct response. """
- # Create the SPARQL query
- query_string = ('''
-SELECT ?name ?dcid
-WHERE {
- ?a typeOf Place .
- ?a name ?name .
- ?a dcid ("geoId/06" "geoId/21" "geoId/24") .
- ?a dcid ?dcid
-}
-''')
- selector = lambda row: row['?name'] != 'California'
-
- # Issue the query
- results = dc.query(query_string)
- selected_results = dc.query(query_string, select=selector)
-
- # Execute the query and iterate through the results.
- for idx, row in enumerate(results):
- if idx == 0:
- self.assertDictEqual(row, {'?name': 'California', '?dcid': 'geoId/06'})
- if idx == 1:
- self.assertDictEqual(row, {'?name': 'Kentucky', '?dcid': 'geoId/21'})
- if idx == 2:
- self.assertDictEqual(row, {'?name': 'Maryland', '?dcid': 'geoId/24'})
-
- # Verify that the select function works.
- for idx, row in enumerate(selected_results):
- if idx == 0:
- self.assertDictEqual(row, {'?name': 'Kentucky', '?dcid': 'geoId/21'})
- if idx == 1:
- self.assertDictEqual(row, {'?name': 'Maryland', '?dcid': 'geoId/24'})
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_no_rows(self, urlopen):
- """ Handles row-less response. """
- # Create a SPARQL query
- query_string = ('''
-SELECT ?name ?dcid
-WHERE {
- ?a typeOf Place .
- ?a name ?name .
- ?a dcid ("geoId/DNE") .
- ?a dcid ?dcid
-}
-''')
- # Issue the query
- self.assertEqual(dc.query(query_string), [])
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/datacommons/test/set_api_key_test.py b/datacommons/test/set_api_key_test.py
deleted file mode 100644
index 0c6260e3..00000000
--- a/datacommons/test/set_api_key_test.py
+++ /dev/null
@@ -1,108 +0,0 @@
-# Copyright 2017 Google Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-""" Data Commons Python API unit tests.
-
-Unit tests setting the API Key.
-"""
-
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
-try:
- from unittest.mock import patch
-except ImportError:
- from mock import patch
-import datacommons as dc
-import datacommons.utils as utils
-
-import os
-import json
-import unittest
-import six.moves.urllib as urllib
-
-_TEST_API_KEY = 'TEST-API-KEY'
-
-_SPARQL_NO_KEY = 'query_no_key'
-_SPARQL_W_KEY = 'query_w_key'
-
-_SEND_REQ_NO_KEY = 'https://send_request_no_key.com'
-_SEND_REQ_W_KEY = 'https://send_request_w_key.com'
-
-
-
-def request_mock(*args, **kwargs):
- """ A mock urlopen call sent in the urllib package. """
- # Create the mock response object.
- class MockResponse:
- def __init__(self, json_data):
- self.json_data = json_data
-
- def read(self):
- return self.json_data
-
- req = args[0]
-
- if req.get_full_url() == _SEND_REQ_NO_KEY or json.loads(req.data) == {'sparql': _SPARQL_NO_KEY}:
- assert 'X-api-key' not in req.headers
- else:
- assert req.get_header('X-api-key') == _TEST_API_KEY
-
- if req.get_full_url() == utils._API_ROOT + utils._API_ENDPOINTS['query']:
- # Return a dummy response that will parse into [] by query()
- return MockResponse(json.dumps({
- 'header': [
- '?name',
- '?dcid'
- ],
- }))
- else:
- # Return a dummy response that will parse into {} by _send_request()
- return MockResponse(json.dumps({'payload': json.dumps({})}))
-
-
-class TestApiKey(unittest.TestCase):
- """Unit test for setting or not setting the API Key."""
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_query_no_api_key(self, urlopen):
- del os.environ[utils._ENV_VAR_API_KEY]
- # Issue a dummy SPARQL query that tells the mock to not expect a key
- self.assertEqual(dc.query(_SPARQL_NO_KEY), [])
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_send_request_no_api_key(self, urlopen):
- del os.environ[utils._ENV_VAR_API_KEY]
- # Issue a dummy url that tells the mock to not expect a key
- self.assertEqual(utils._send_request(_SEND_REQ_NO_KEY, {'foo': ['bar']}), {})
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_query_w_api_key(self, urlopen):
- """ Handles row-less response. """
- # Set the API key
- dc.set_api_key('make_sure_I_am_replaced')
- dc.set_api_key(_TEST_API_KEY)
- # Issue a dummy SPARQL query that tells the mock to expect a key
- self.assertEqual(dc.query(_SPARQL_W_KEY), [])
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_send_request_w_api_key(self, urlopen):
- """ Handles row-less response. """
- # Set the API key
- dc.set_api_key(_TEST_API_KEY)
- # Issue a dummy url that tells the mock to expect a key
- self.assertEqual(utils._send_request(_SEND_REQ_W_KEY), {})
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/datacommons/test/stat_vars_test.py b/datacommons/test/stat_vars_test.py
deleted file mode 100644
index b4f409d2..00000000
--- a/datacommons/test/stat_vars_test.py
+++ /dev/null
@@ -1,362 +0,0 @@
-# Copyright 2020 Google Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-""" Data Commons Python API unit tests.
-
-Unit tests for StatVar methods in the Data Commons Python API.
-"""
-
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
-try:
- from unittest.mock import patch
-except ImportError:
- from mock import patch
-
-import datacommons as dc
-import datacommons.utils as utils
-import math
-import json
-import unittest
-import six
-import six.moves.urllib as urllib
-
-# Reusable parts of REST API /stat/all response.
-CA_COUNT_PERSON = {
- "isDcAggregate":
- "true",
- "sourceSeries": [{
- "val": {
- "1990": 23640,
- "1991": 24100,
- "1993": 25090,
- },
- "observationPeriod": "P1Y",
- "importName": "WorldDevelopmentIndicators",
- "provenanceDomain": "worldbank.org"
- }, {
- "val": {
- "1790": 3929214,
- "1800": 5308483,
- "1810": 7239881,
- },
- "measurementMethod": "WikidataPopulation",
- "importName": "WikidataPopulation",
- "provenanceDomain": "wikidata.org"
- }, {
- "val": {
- "1890": 28360,
- "1891": 24910,
- "1892": 25070,
- },
- "measurementMethod": "OECDRegionalStatistics",
- "observationPeriod": "P1Y",
- "importName": "OECDRegionalDemography",
- "provenanceDomain": "oecd.org"
- }]
-}
-
-CA_COUNT_PERSON_MALE = {
- "sourceSeries": [{
- "val": {
- "1990": 12000,
- "1991": 14000,
- "1992": 14000,
- },
- "measurementMethod": "WikidataPopulation",
- "importName": "WikidataPopulation",
- "provenanceDomain": "wikidata.org"
- },]
-}
-
-HU22_COUNT_PERSON = {
- "sourceSeries": [{
- "val": {
- "1990": 2360,
- "1991": 2410,
- "1992": 2500,
- },
- "measurementMethod": "OECDRegionalStatistics",
- "observationPeriod": "P1Y",
- "importName": "OECDRegionalDemography",
- "provenanceDomain": "oecd.org"
- }]
-}
-
-HU22_COUNT_PERSON_MALE = {
- "sourceSeries": [{
- "val": {
- "1990": 1360,
- "1991": 1410,
- "1992": 1500,
- },
- "measurementMethod": "OECDRegionalStatistics",
- "observationPeriod": "P1Y",
- "importName": "OECDRegionalDemography",
- "provenanceDomain": "oecd.org"
- }]
-}
-
-CA_MEDIAN_AGE_PERSON = {
- "sourceSeries": [{
- "val": {
- "1990": 12,
- "1991": 24,
- "1992": 24,
- },
- "measurementMethod": "WikidataPopulation",
- "importName": "WikidataPopulation",
- "provenanceDomain": "wikidata.org"
- }]
-}
-
-
-def request_mock(*args, **kwargs):
- """A mock urlopen requests sent in the requests package."""
-
- # Create the mock response object.
- class MockResponse:
-
- def __init__(self, json_data):
- self.json_data = json_data
-
- def read(self):
- return self.json_data
-
- req = args[0]
-
- stat_value_url_base = utils._API_ROOT + utils._API_ENDPOINTS[
- 'get_stat_value']
- stat_series_url_base = utils._API_ROOT + utils._API_ENDPOINTS[
- 'get_stat_series']
- stat_all_url_base = utils._API_ROOT + utils._API_ENDPOINTS['get_stat_all']
-
- # Mock responses for urlopen requests to get_stat_value.
- if req.get_full_url(
- ) == stat_value_url_base + '?place=geoId/06&stat_var=Count_Person':
- # Response returned when querying with basic args.
- return MockResponse(json.dumps({"value": 123}))
- if req.get_full_url(
- ) == stat_value_url_base + '?place=geoId/06&stat_var=Count_Person&date=2010':
- # Response returned when querying with observationDate.
- return MockResponse(json.dumps({"value": 133}))
- if (req.get_full_url() == stat_value_url_base +
- '?place=geoId/06&stat_var=Count_Person&' +
- 'date=2010&measurement_method=CensusPEPSurvey&' +
- 'observation_period=P1Y&unit=RealPeople&scaling_factor=100'):
- # Response returned when querying with above optional params.
- return MockResponse(json.dumps({"value": 103}))
-
- # Mock responses for urlopen requests to get_stat_series.
- if req.get_full_url(
- ) == stat_series_url_base + '?place=geoId/06&stat_var=Count_Person':
- # Response returned when querying with basic args.
- return MockResponse(json.dumps({"series": {"2000": 1, "2001": 2}}))
- if (req.get_full_url() == stat_series_url_base +
- '?place=geoId/06&stat_var=Count_Person&' +
- 'measurement_method=CensusPEPSurvey&observation_period=P1Y&' +
- 'unit=RealPeople&scaling_factor=100'):
-
- # Response returned when querying with above optional params.
- return MockResponse(json.dumps({"series": {"2000": 3, "2001": 42}}))
- if (req.get_full_url() == stat_series_url_base +
- '?place=geoId/06&stat_var=Count_Person&' +
- 'measurement_method=DNE'):
-
- # Response returned when data not available for optional parameters.
- # /stat/series?place=geoId/06&stat_var=Count_Person&measurement_method=DNE
- return MockResponse(json.dumps({"series": {}}))
-
- # Mock responses for urlopen requests to get_stat_all.
- if req.get_full_url() == stat_all_url_base:
- data = json.loads(req.data)
- if (data['places'] == ['geoId/06', 'nuts/HU22'] and
- data['stat_vars'] == ['Count_Person', 'Count_Person_Male']):
- # Response returned when querying with above params.
- # Response with data for all Place+StatVar combos.
- full_resp = {
- "placeData": {
- "geoId/06": {
- "statVarData": {
- "Count_Person": CA_COUNT_PERSON,
- "Count_Person_Male": CA_COUNT_PERSON_MALE,
- }
- },
- "nuts/HU22": {
- "statVarData": {
- "Count_Person": HU22_COUNT_PERSON,
- "Count_Person_Male": HU22_COUNT_PERSON_MALE
- }
- }
- }
- }
- return MockResponse(json.dumps(full_resp))
-
- if (data['places'] == ['geoId/06', 'nuts/HU22'] and
- data['stat_vars'] == ['Count_Person', 'Median_Age_Person']):
- # Response returned when querying with above params.
- # Median Age missing for HU22.
- resp = {
- "placeData": {
- "geoId/06": {
- "statVarData": {
- "Count_Person": CA_COUNT_PERSON,
- "Median_Age_Person": CA_MEDIAN_AGE_PERSON
- }
- },
- "nuts/HU22": {
- "statVarData": {
- "Count_Person": HU22_COUNT_PERSON,
- "Median_Age_Person": {}
- }
- }
- }
- }
- return MockResponse(json.dumps(resp))
-
- if (data['places'] == ['badPlaceId', 'nuts/HU22'] and
- data['stat_vars'] == ['Count_Person', 'badStatVarId']):
- # Response returned when querying with above params.
- # Bad DCIDs for place or statvar.
- resp = {
- "placeData": {
- "badPlaceId": {
- "statVarData": {
- "Count_Person": {},
- "badStatVarId": {}
- }
- },
- "nuts/HU22": {
- "statVarData": {
- "Count_Person": HU22_COUNT_PERSON,
- "badStatVarId": {}
- }
- }
- }
- }
- return MockResponse(json.dumps(resp))
-
- # Otherwise, return an empty response and a 404.
- return urllib.error.HTTPError(None, 404, None, None, None)
-
-
-class TestGetStatValue(unittest.TestCase):
- """Unit tests for get_stat_value."""
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_basic(self, urlopen):
- """Calling get_stat_value with minimal and proper args."""
- # Call get_stat_value
-
- self.assertEqual(dc.get_stat_value('geoId/06', 'Count_Person'), 123)
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_opt_args(self, urlopen):
- """Calling get_stat_value with optional args returns specific data."""
- # Call get_stat_value for specific obs
- self.assertEqual(dc.get_stat_value('geoId/06', 'Count_Person', '2010'),
- 133)
-
- # Call get_stat_value with all optional args
- stat = dc.get_stat_value('geoId/06', 'Count_Person', '2010',
- 'CensusPEPSurvey', 'P1Y', 'RealPeople', 100)
- self.assertEqual(stat, 103)
-
- # Call get_stat_series with bogus required args
- stat = dc.get_stat_value('foofoo', 'barrbar')
- self.assertTrue(math.isnan(stat))
-
-class TestGetStatSeries(unittest.TestCase):
- """Unit tests for get_stat_series."""
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_basic(self, urlopen):
- """Calling get_stat_value with minimal and proper args."""
- # Call get_stat_series
- stats = dc.get_stat_series('geoId/06', 'Count_Person')
- self.assertEqual(stats, {"2000": 1, "2001": 2})
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_opt_args(self, urlopen):
- """Calling get_stat_value with optional args returns specific data."""
-
- # Call get_stat_series with all optional args
- stats = dc.get_stat_series('geoId/06', 'Count_Person',
- 'CensusPEPSurvey', 'P1Y', 'RealPeople', 100)
- self.assertEqual(stats, {"2000": 3, "2001": 42})
-
- # Call get_stat_series with bogus required args
- stats = dc.get_stat_series('foofoofoo', 'barfoobar')
- self.assertEqual(stats, {})
-
- # Call get_stat_series with non-satisfiable optional args
- stats = dc.get_stat_series('geoId/06', 'Count_Person', 'DNE')
- self.assertEqual(stats, {})
-
-
-class TestGetStatAll(unittest.TestCase):
- """Unit tests for get_stat_all."""
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_basic(self, urlopen):
- """Calling get_stat_all with proper args."""
- # Expecting at least one TS per Place+StatVar
- stats = dc.get_stat_all(['geoId/06', 'nuts/HU22'],
- ['Count_Person', 'Count_Person_Male'])
- exp = {
- "geoId/06": {
- "Count_Person": CA_COUNT_PERSON,
- "Count_Person_Male": CA_COUNT_PERSON_MALE,
- },
- "nuts/HU22": {
- "Count_Person": HU22_COUNT_PERSON,
- "Count_Person_Male": HU22_COUNT_PERSON_MALE
- }
- }
- self.assertDictEqual(stats, exp)
- # Expecting proper handling of no TS for Place+StatVar combo
- stats = dc.get_stat_all(['geoId/06', 'nuts/HU22'],
- ['Count_Person', 'Median_Age_Person'])
- exp = {
- "geoId/06": {
- "Count_Person": CA_COUNT_PERSON,
- "Median_Age_Person": CA_MEDIAN_AGE_PERSON
- },
- "nuts/HU22": {
- "Count_Person": HU22_COUNT_PERSON,
- "Median_Age_Person": {}
- }
- }
- self.assertDictEqual(stats, exp)
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_bad_dcids(self, urlopen):
- stats = dc.get_stat_all(['badPlaceId', 'nuts/HU22'],
- ['Count_Person', 'badStatVarId'])
- exp = {
- "badPlaceId": {
- "Count_Person": {},
- "badStatVarId": {}
- },
- "nuts/HU22": {
- "Count_Person": HU22_COUNT_PERSON,
- "badStatVarId": {}
- }
- }
- self.assertDictEqual(stats, exp)
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/datacommons/utils.py b/datacommons/utils.py
deleted file mode 100644
index 3a0ebd01..00000000
--- a/datacommons/utils.py
+++ /dev/null
@@ -1,149 +0,0 @@
-# Copyright 2017 Google Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-""" Data Commons Utilities Library.
-
-Various functions that can aid in the extension of the Data Commons API.
-"""
-
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
-from collections import defaultdict
-
-import base64
-import json
-import os
-import six.moves.urllib.error
-import six.moves.urllib.request
-import zlib
-
-
-# --------------------------------- CONSTANTS ---------------------------------
-
-
-# REST API endpoint root
-_API_ROOT = "http://api.datacommons.org"
-
-# REST API endpoint paths
-_API_ENDPOINTS = {
- 'query': '/query',
- 'get_property_labels': '/node/property-labels',
- 'get_property_values': '/node/property-values',
- 'get_triples': '/node/triples',
- 'get_places_in': '/node/places-in',
- 'get_related_places': '/node/related-places',
- 'get_populations': '/node/populations',
- 'get_observations': '/node/observations',
- 'get_pop_obs': '/bulk/pop-obs',
- 'get_place_obs': '/bulk/place-obs',
- 'get_stats': '/bulk/stats',
- 'get_stat_value': '/stat/value',
- 'get_stat_series': '/stat/series',
- 'get_stat_all': '/stat/all',
-}
-
-# The default value to limit to
-_MAX_LIMIT = 100
-
-# Batch size for heavyweight queries.
-_QUERY_BATCH_SIZE = 500
-
-# Environment variable names used by the package
-_ENV_VAR_API_KEY = 'DC_API_KEY'
-
-# --------------------------- API UTILITY FUNCTIONS ---------------------------
-
-
-def set_api_key(api_key):
- """Sets an environment variable :code:`"DC_API_KEY"` to given :code:`api_key`.
-
- Users may supply an API key to the Python API, which simply passes it on to
- the REST API for handling. The API key can be provided to the API after
- importing the library, or set as an environment variable
- :code:`"DC_API_KEY"`.
-
- For more details about how to get an API key and provide it to the Python
- Client API, please visit :ref:`getting_started`.
- Args:
- api_key (:obj:`str`): The API key.
- """
- os.environ[_ENV_VAR_API_KEY] = api_key
-
-
-# ------------------------- INTERNAL HELPER FUNCTIONS -------------------------
-
-
-def _send_request(req_url, req_json={}, compress=False, post=True, use_payload=True):
- """ Sends a POST/GET request to req_url with req_json, default to POST.
-
- Returns:
- The payload returned by sending the POST/GET request formatted as a dict.
- """
- headers = {
- 'Content-Type': 'application/json'
- }
-
- # Pass along API key if provided
- if os.environ.get(_ENV_VAR_API_KEY):
- headers['x-api-key'] = os.environ[_ENV_VAR_API_KEY]
-
- # Send the request and verify the request succeeded
- if post:
- req = six.moves.urllib.request.Request(
- req_url,
- data=json.dumps(req_json).encode('utf-8'),
- headers=headers)
- else:
- req = six.moves.urllib.request.Request(req_url, headers=headers)
- try:
- res = six.moves.urllib.request.urlopen(req)
- except six.moves.urllib.error.HTTPError as e:
- raise ValueError(
- 'Response error: An HTTP {} code was returned by the REST API. '
- 'Printing response\n\n{}'.format(e.code, e.read()))
- if isinstance(res, six.moves.urllib.error.HTTPError):
- raise ValueError(
- 'Response error: An HTTP {} code was returned by the REST API. '
- 'Printing response\n\n{}'.format(res.code, res.msg))
- # Get the JSON
- res_json = json.loads(res.read())
- if not use_payload:
- return res_json
- if 'payload' not in res_json:
- raise ValueError(
- 'Response error: Payload not found. Printing response\n\n'
- '{}'.format(res.text))
-
- # If the payload is compressed, decompress and decode it
- payload = res_json['payload']
- if compress:
- payload = zlib.decompress(
- base64.b64decode(payload), zlib.MAX_WBITS|32)
- return json.loads(payload)
-
-
-def _format_expand_payload(payload, new_key, must_exist=[]):
- """ Formats expand type payloads into dicts from dcids to lists of values. """
- # Create the results dictionary from payload
- results = defaultdict(set)
- for entry in payload:
- if 'dcid' in entry and new_key in entry:
- dcid = entry['dcid']
- results[dcid].add(entry[new_key])
-
- # Ensure all dcids in must_exist have some entry in results.
- for dcid in must_exist:
- results[dcid]
- return {k: sorted(list(v)) for k, v in results.items()}
diff --git a/datacommons_client/README.md b/datacommons_client/README.md
new file mode 100644
index 00000000..f4563663
--- /dev/null
+++ b/datacommons_client/README.md
@@ -0,0 +1,24 @@
+# Data Commons Python API
+
+This is a Python library for accessing data in the Data Commons Graph.
+
+To get started, install this package from pip.
+
+```bash
+pip install datacommons-client
+```
+
+To get additional functionality to work with Pandas DataFrames, install the package
+with the optional Pandas dependency.
+
+```bash
+pip install "datacommons-client[Pandas]"
+```
+
+Once the package is installed, import `datacommons_client`.
+
+```python
+import datacommons_client as dc
+```
+
+For more detail on getting started with the API, please visit .
diff --git a/datacommons_client/__init__.py b/datacommons_client/__init__.py
new file mode 100644
index 00000000..ad654341
--- /dev/null
+++ b/datacommons_client/__init__.py
@@ -0,0 +1,22 @@
+__version__ = "2.1.6"
+"""
+Data Commons Client Package
+
+This package provides a Python client for interacting with the Data Commons API.
+"""
+
+from datacommons_client.client import DataCommonsClient
+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.utils.context import use_api_key
+
+__all__ = [
+ "DataCommonsClient",
+ "API",
+ "NodeEndpoint",
+ "ObservationEndpoint",
+ "ResolveEndpoint",
+ "use_api_key",
+]
diff --git a/datacommons_client/client.py b/datacommons_client/client.py
new file mode 100644
index 00000000..cb8b90d1
--- /dev/null
+++ b/datacommons_client/client.py
@@ -0,0 +1,211 @@
+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.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
+from datacommons_client.utils.decorators import requires_pandas
+from datacommons_client.utils.error_handling import NoDataForPropertyError
+
+try:
+ import pandas as pd
+except ImportError:
+ pd = None
+
+
+class DataCommonsClient:
+ """
+ A client for interacting with the Data Commons API.
+
+ This class provides convenient access to the V2 Data Commons API endpoints.
+
+ Attributes:
+ api (API): An instance of the API class that handles requests.
+ node (NodeEndpoint): Provides access to node-related queries, such as fetching property labels
+ and values for individual or multiple nodes in the Data Commons knowledge graph.
+ 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.
+
+ """
+
+ def __init__(self,
+ api_key: Optional[str] = None,
+ *,
+ dc_instance: Optional[str] = "datacommons.org",
+ url: Optional[str] = None,
+ surface_header_value: Optional[str] = None):
+ """
+ Initializes the DataCommonsClient.
+
+ Args:
+ api_key (Optional[str]): The API key for authentication. Defaults to None. Note that
+ 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.
+ """
+ # If a fully resolved URL is provided, and the default dc_instance is used,
+ # ignore that default value
+ if dc_instance == "datacommons.org" and url:
+ dc_instance = None
+
+ # Create an instance of the API class which will be injected to the endpoints
+ self.api = API(api_key=api_key,
+ dc_instance=dc_instance,
+ url=url,
+ surface_header_value=surface_header_value)
+
+ # Create instances of the endpoints
+ self.node = NodeEndpoint(api=self.api)
+ self.observation = ObservationEndpoint(api=self.api)
+ self.resolve = ResolveEndpoint(api=self.api)
+
+ def _find_filter_facet_ids(
+ self,
+ fetch_by: Literal["entity", "entity_type"],
+ date: ObservationDate | str,
+ variable_dcids: str | list[str],
+ entity_dcids: Literal["all"] | list[str] = "all",
+ entity_type: Optional[str] = None,
+ parent_entity: Optional[str] = None,
+ property_filters: Optional[dict[str, str | list[str]]] = None,
+ ) -> list[str] | None:
+ """Finds matching facet IDs for property filters.
+
+ Args:
+ fetch_by (Literal["entity", "entity_type"]): Determines whether to fetch by entity or entity type.
+ variable_dcids (str | list[str]): The variable DCIDs for which to retrieve facet IDs.
+ entity_dcids (Literal["all"] | list[str], optional): The entity DCIDs, or "all" if filtering by entity type.
+ entity_type (Optional[str]): The entity type, required if fetching by entity type.
+ parent_entity (Optional[str]): The parent entity, used when fetching by entity type.
+ property_filters (Optional[dict[str, str | list[str]]): A dictionary of properties to match facets against.
+
+ Returns:
+ list[str] | None: A list of matching facet IDs, or None if no filters are applied.
+ """
+
+ if not property_filters:
+ return None
+
+ if fetch_by == "entity":
+ observations = self.observation.fetch_observations_by_entity_dcid(
+ date=date,
+ entity_dcids=entity_dcids,
+ variable_dcids=variable_dcids,
+ select=["variable", "entity", "facet"],
+ )
+ else:
+ observations = self.observation.fetch_observations_by_entity_type(
+ date=date,
+ entity_type=entity_type,
+ parent_entity=parent_entity,
+ variable_dcids=variable_dcids,
+ select=["variable", "entity", "facet"],
+ )
+
+ facet_sets = [
+ observations.find_matching_facet_id(property_name=p, value=v)
+ for p, v in property_filters.items()
+ ]
+
+ facet_ids = list({facet for facets in facet_sets for facet in facets})
+
+ return facet_ids
+
+ @requires_pandas
+ def observations_dataframe(
+ self,
+ variable_dcids: str | list[str],
+ date: ObservationDate | str,
+ entity_dcids: Literal["all"] | list[str] = "all",
+ entity_type: Optional[str] = None,
+ parent_entity: Optional[str] = None,
+ property_filters: Optional[dict[str, str | list[str]]] = None,
+ include_constraints_metadata: bool = False,
+ ):
+ """
+ Fetches statistical observations and returns them as a Pandas DataFrame.
+
+ The Observation API fetches statistical observations linked to entities and variables
+ at a particular date (e.g., "population of USA in 2020", "GDP of California in 2010").
+
+ Args:
+ variable_dcids (str | list[str]): One or more variable DCIDs for the observation.
+ date (ObservationDate | str): The date for which observations are requested. It can be
+ a specific date, "all" to retrieve all observations, or "latest" to get the most recent observations.
+ entity_dcids (Literal["all"] | list[str], optional): The entity DCIDs for which to retrieve data.
+ Defaults to "all".
+ entity_type (Optional[str]): The type of entities to filter by when `entity_dcids="all"`.
+ Required if `entity_dcids="all"`. Defaults to None.
+ parent_entity (Optional[str]): The parent entity under which the target entities fall.
+ Required if `entity_dcids="all"`. Defaults to None.
+ property_filters (Optional[dict[str, str | list[str]]): An optional dictionary used to filter
+ the data by using observation properties like `measurementMethod`, `unit`, or `observationPeriod`.
+ include_constraints_metadata (bool): If True, includes the dcid and name of any constraint
+ properties associated with the variable DCIDs (based on the `constraintProperties` property)
+ in the returned DataFrame. Defaults to False.
+
+ Returns:
+ pd.DataFrame: A DataFrame containing the requested observations.
+ """
+
+ if entity_dcids == "all" and not (entity_type and parent_entity):
+ raise ValueError(
+ "When 'entity_dcids' is 'all', both 'parent_entity' and 'entity_type' must be specified."
+ )
+
+ if entity_dcids != "all" and (entity_type or parent_entity):
+ raise ValueError(
+ "Specify 'entity_type' and 'parent_entity' only when 'entity_dcids' is 'all'."
+ )
+
+ # If property filters are provided, fetch the required facet IDs. Otherwise, set to None.
+ facets = self._find_filter_facet_ids(
+ fetch_by="entity" if entity_dcids != "all" else "entity_type",
+ date=date,
+ variable_dcids=variable_dcids,
+ entity_dcids=entity_dcids,
+ entity_type=entity_type,
+ parent_entity=parent_entity,
+ property_filters=property_filters,
+ )
+
+ if not facets and property_filters:
+ raise NoDataForPropertyError
+
+ if entity_dcids == "all":
+ observations = self.observation.fetch_observations_by_entity_type(
+ date=date,
+ parent_entity=parent_entity,
+ entity_type=entity_type,
+ variable_dcids=variable_dcids,
+ filter_facet_ids=facets,
+ )
+ else:
+ observations = self.observation.fetch_observations_by_entity_dcid(
+ date=date,
+ entity_dcids=entity_dcids,
+ variable_dcids=variable_dcids,
+ filter_facet_ids=facets,
+ )
+
+ # Convert the observations to a DataFrame
+ df = pd.DataFrame(observations.to_observation_records().model_dump())
+
+ # Add entity names to the DataFrame
+ df = add_entity_names_to_observations_dataframe(
+ endpoint=self.node,
+ observations_df=df,
+ entity_columns=["entity", "variable"],
+ )
+
+ if include_constraints_metadata:
+ df = add_property_constraints_to_observations_dataframe(
+ endpoint=self.node,
+ observations_df=df,
+ )
+
+ return df
diff --git a/datacommons_client/endpoints/__init__.py b/datacommons_client/endpoints/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/datacommons_client/endpoints/base.py b/datacommons_client/endpoints/base.py
new file mode 100644
index 00000000..ff4adfdc
--- /dev/null
+++ b/datacommons_client/endpoints/base.py
@@ -0,0 +1,192 @@
+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 post_request
+from datacommons_client.utils.request_handling import resolve_instance_url
+
+
+class API:
+ """Represents a configured API interface to the Data Commons API.
+
+ This class handles environment setup, resolving the base URL, building headers,
+ or optionally using a fully qualified URL directly. It can be used standalone
+ to interact with the API or in combination with Endpoint classes.
+ """
+
+ def __init__(
+ self,
+ api_key: Optional[str] = None,
+ dc_instance: Optional[str] = None,
+ url: Optional[str] = None,
+ surface_header_value: Optional[str] = None,
+ ):
+ """
+ Initializes the API instance.
+
+ Args:
+ api_key: The API key for authentication. Defaults to None.
+ dc_instance: The Data Commons instance domain. Ignored if `url` is provided.
+ Defaults to 'datacommons.org' if both `url` and `dc_instance` are None.
+ url: A fully qualified URL for the base API. This may be useful if more granular control
+ 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
+
+ Raises:
+ ValueError: If both `dc_instance` and `url` are provided.
+ """
+ if dc_instance and url:
+ raise ValueError("Cannot provide both `dc_instance` and `url`.")
+
+ if not dc_instance and not url:
+ 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)
+ else:
+ # Resolve from dc_instance
+ self.base_url = resolve_instance_url(dc_instance)
+
+ self.headers = self.build_headers(surface_header_value=surface_header_value,
+ api_key=api_key)
+
+ def __repr__(self) -> str:
+ """Returns a readable representation of the API object.
+
+ Indicates the base URL and if it's authenticated.
+
+ Returns:
+ str: A string representation of the API object.
+ """
+ has_auth = " (Authenticated)" if "X-API-Key" in self.headers else ""
+ return f""
+
+ def post(self,
+ payload: dict[str, Any],
+ endpoint: Optional[str] = None,
+ *,
+ all_pages: bool = True,
+ next_token: Optional[str] = None) -> Dict[str, Any]:
+ """Makes a POST request using the configured API environment.
+
+ If `endpoint` is provided, it will be appended to the base_url. Otherwise,
+ it will just POST to the base URL.
+
+ Args:
+ payload: The JSON payload for the POST request.
+ endpoint: An optional endpoint path to append to the base URL.
+ all_pages: If True, fetch all pages of the response. If False, fetch only the first page.
+ Defaults to True. Set to False to only fetch the first page. In that case, a
+ `next_token` key in the response will indicate if more pages are available.
+ That token can be used to fetch the next page.
+
+ Returns:
+ A dictionary containing the merged response data.
+
+ Raises:
+ ValueError: If the payload is not a valid dictionary.
+ """
+ if not isinstance(payload, dict):
+ raise ValueError("Payload must be a dictionary.")
+
+ url = (self.base_url if endpoint is None else f"{self.base_url}/{endpoint}")
+
+ headers = self.headers
+ ctx_api_key = _API_KEY_CONTEXT_VAR.get()
+ if ctx_api_key:
+ # Copy headers to avoid mutating the shared client state
+ headers = self.headers.copy()
+ headers["X-API-Key"] = ctx_api_key
+
+ return post_request(url=url,
+ payload=payload,
+ headers=headers,
+ 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]:
+ """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
+
+ Returns:
+ A dictionary of headers for the request.
+ """
+ headers = {
+ "Content-Type": "application/json",
+ "x-surface": "clientlib-python"
+ }
+ if api_key:
+ headers["X-API-Key"] = api_key
+
+ if surface_header_value:
+ headers["x-surface"] = surface_header_value
+
+ return headers
+
+
+class Endpoint:
+ """Represents a specific endpoint within the Data Commons API.
+
+ This class leverages an API instance to make requests. It does not
+ handle instance resolution or headers directly; that is delegated to the API instance.
+
+ Attributes:
+ endpoint (str): The endpoint path (e.g., 'node').
+ api (API): The API instance providing configuration and the `post` method.
+ """
+
+ def __init__(self, endpoint: str, api: API):
+ """
+ Initializes the Endpoint instance.
+
+ Args:
+ endpoint: The endpoint path (e.g., 'node').
+ api: An API instance that provides the environment configuration.
+ """
+ self.endpoint = endpoint
+ self.api = api
+
+ def __repr__(self) -> str:
+ """Returns a readable representation of the Endpoint object.
+
+ Shows the endpoint and underlying API configuration.
+
+ Returns:
+ str: A string representation of the Endpoint object.
+ """
+ return f"<{self.endpoint.title()} Endpoint using {repr(self.api)}>"
+
+ def post(self,
+ payload: dict[str, Any],
+ all_pages: bool = True,
+ next_token: Optional[str] = None) -> Dict[str, Any]:
+ """Makes a POST request to the specified endpoint using the API instance.
+
+ Args:
+ payload: The JSON payload for the POST request.
+ all_pages: If True, fetch all pages of the response. If False, fetch only the first page.
+ Defaults to True. Set to False to only fetch the first page. In that case, a
+ `next_token` key in the response will indicate if more pages are available.
+ That token can be used to fetch the next page.
+ next_token: Optionally, the token to fetch the next page of results. Defaults to None.
+
+ Returns:
+ A dictionary with the merged API response data.
+
+ Raises:
+ ValueError: If the payload is not a valid dictionary.
+ """
+ return self.api.post(payload=payload,
+ endpoint=self.endpoint,
+ all_pages=all_pages,
+ next_token=next_token)
diff --git a/datacommons_client/endpoints/node.py b/datacommons_client/endpoints/node.py
new file mode 100644
index 00000000..808c3495
--- /dev/null
+++ b/datacommons_client/endpoints/node.py
@@ -0,0 +1,654 @@
+from concurrent.futures import ThreadPoolExecutor
+import contextvars
+from functools import partial
+from functools import wraps
+from typing import Literal, Optional
+import warnings
+
+from datacommons_client.endpoints.base import API
+from datacommons_client.endpoints.base import Endpoint
+from datacommons_client.endpoints.payloads import NodeRequestPayload
+from datacommons_client.endpoints.payloads import normalize_list_to_string
+from datacommons_client.endpoints.response import NodeResponse
+from datacommons_client.models.node import Name
+from datacommons_client.models.node import Node
+from datacommons_client.models.node import StatVarConstraint
+from datacommons_client.models.node import StatVarConstraints
+from datacommons_client.utils.graph import build_graph_map
+from datacommons_client.utils.graph import build_relationship_tree
+from datacommons_client.utils.graph import fetch_relationship_lru
+from datacommons_client.utils.graph import flatten_relationship
+from datacommons_client.utils.names import DEFAULT_NAME_LANGUAGE
+from datacommons_client.utils.names import DEFAULT_NAME_PROPERTY
+from datacommons_client.utils.names import extract_name_from_english_name_property
+from datacommons_client.utils.names import extract_name_from_property_with_language
+from datacommons_client.utils.names import NAME_WITH_LANGUAGE_PROPERTY
+
+PLACES_MAX_WORKERS = 10
+
+CONSTRAINT_PROPERTY: str = "constraintProperties"
+
+_DEPRECATED_METHODS: dict[str, dict[str, str | dict[str, str]]] = {
+ "fetch_entity_parents": {
+ "new_name": "fetch_place_parents",
+ "arg_map": {
+ "entity_dcids": "place_dcids"
+ }
+ },
+ "fetch_entity_ascendancy": {
+ "new_name": "fetch_place_ancestors",
+ "arg_map": {
+ "entity_dcids": "place_dcids"
+ }
+ }
+}
+
+
+class NodeEndpoint(Endpoint):
+ """Initializes the NodeEndpoint with a given API configuration.
+
+ Args:
+ api (API): The API instance providing the environment configuration
+ (base URL, headers, authentication) to be used for requests.
+ """
+
+ def __init__(self, api: API):
+ """Initializes the NodeEndpoint with a given API configuration."""
+ super().__init__(endpoint="node", api=api)
+
+ def __getattr__(self, name):
+ if name in _DEPRECATED_METHODS:
+ method_info = _DEPRECATED_METHODS[name]
+ new_name = method_info["new_name"]
+ arg_map = method_info.get("arg_map", {})
+ new_method = getattr(self, new_name)
+
+ @wraps(new_method)
+ def wrapper(*args, **kwargs):
+ for old_arg, new_arg in arg_map.items():
+ if old_arg in kwargs:
+ warnings.warn(
+ f"Argument '{old_arg}' has been renamed and will removed"
+ f" in a future version. Use '{new_arg}' instead.",
+ category=DeprecationWarning,
+ stacklevel=2)
+ if new_arg not in kwargs:
+ kwargs[new_arg] = kwargs.pop(old_arg)
+
+ warnings.warn(
+ f"'{name}' is deprecated and will be removed in a future version. "
+ f"Use '{new_name}' instead.",
+ category=DeprecationWarning,
+ stacklevel=2)
+ return new_method(*args, **kwargs)
+
+ return wrapper
+ raise AttributeError(
+ f"'{self.__class__.__name__}' object has no attribute '{name}'")
+
+ def fetch(
+ self,
+ node_dcids: str | list[str],
+ expression: str,
+ *,
+ all_pages: bool = True,
+ next_token: Optional[str] = None,
+ ) -> NodeResponse:
+ """Fetches properties or arcs for given nodes and properties.
+
+ Args:
+ node_dcids (str | List[str]): The DCID(s) of the nodes to query.
+ expression (str): The property or relation expression(s) to query.
+ all_pages: If True, fetch all pages of the response. If False, fetch only the first page.
+ Defaults to True. Set to False to only fetch the first page. In that case, a
+ `next_token` key in the response will indicate if more pages are available.
+ That token can be used to fetch the next page.
+ next_token: Optionally, the token to fetch the next page of results. Defaults to None.
+
+ Returns:
+ NodeResponse: The response object containing the queried data.
+
+ Example:
+ ```python
+ response = node.fetch(
+ node_dcids=["geoId/06"],
+ expression="<-"
+ )
+ print(response)
+ ```
+ """
+
+ # Create the payload
+ payload = NodeRequestPayload(node_dcids=node_dcids,
+ expression=expression).to_dict()
+
+ # Make the request and return the response.
+ return NodeResponse.model_validate(
+ self.post(payload, all_pages=all_pages, next_token=next_token))
+
+ def fetch_property_labels(
+ self,
+ node_dcids: str | list[str],
+ out: bool = True,
+ *,
+ all_pages: bool = True,
+ next_token: Optional[str] = None,
+ ) -> NodeResponse:
+ """Fetches all property labels for the given nodes.
+
+ Args:
+ node_dcids (str | list[str]): The DCID(s) of the nodes to query.
+ out (bool): Whether to fetch outgoing properties (`->`). Defaults to True.
+ all_pages: If True, fetch all pages of the response. If False, fetch only the first page.
+ Defaults to True. Set to False to only fetch the first page. In that case, a
+ `next_token` key in the response will indicate if more pages are available.
+ That token can be used to fetch the next page.
+ next_token: Optionally, the token to fetch the next page of results. Defaults to None.
+
+ Returns:
+ NodeResponse: The response object containing the property labels.
+
+ Example:
+ ```python
+ response = node.fetch_property_labels(node_dcids="geoId/06")
+ print(response)
+ ```
+ """
+ # Determine the direction of the properties.
+ expression = "->" if out else "<-"
+
+ # Make the request and return the response.
+ return self.fetch(
+ node_dcids=node_dcids,
+ expression=expression,
+ all_pages=all_pages,
+ next_token=next_token,
+ )
+
+ def fetch_property_values(
+ self,
+ node_dcids: str | list[str],
+ properties: str | list[str],
+ constraints: Optional[str] = None,
+ out: bool = True,
+ *,
+ all_pages: bool = True,
+ next_token: Optional[str] = None,
+ ) -> NodeResponse:
+ """Fetches the values of specific properties for given nodes.
+
+ Args:
+ node_dcids (str | List[str]): The DCID(s) of the nodes to query.
+ properties (str | List[str]): The property or relation expression(s) to query.
+ constraints (Optional[str]): Additional constraints for the query. Defaults to None.
+ out (bool): Whether to fetch outgoing properties. Defaults to True.
+ all_pages: If True, fetch all pages of the response. If False, fetch only the first page.
+ Defaults to True. Set to False to only fetch the first page. In that case, a
+ `next_token` key in the response will indicate if more pages are available.
+ That token can be used to fetch the next page.
+ next_token: Optionally, the token to fetch the next page of results. Defaults to None.
+
+
+ Returns:
+ NodeResponse: The response object containing the property values.
+
+ Example:
+ ```python
+ response = node.fetch_property_values(
+ node_dcids=["geoId/06"],
+ properties="name",
+ out=True
+ )
+ print(response)
+ ```
+ """
+
+ # Normalize the input to a string (if it's a list), otherwise use the string as is.
+ properties = normalize_list_to_string(properties)
+
+ # Construct the expression based on the direction and constraints.
+ direction = "->" if out else "<-"
+ expression = f"{direction}{properties}"
+ if constraints:
+ expression += f"{{{constraints}}}"
+
+ return self.fetch(
+ node_dcids=node_dcids,
+ expression=expression,
+ all_pages=all_pages,
+ next_token=next_token,
+ )
+
+ def fetch_all_classes(
+ self,
+ *,
+ all_pages: bool = True,
+ next_token: Optional[str] = None,
+ ) -> NodeResponse:
+ """Fetches all Classes available in the Data Commons knowledge graph.
+
+ Args:
+ all_pages: If True, fetch all pages of the response. If False, fetch only the first page.
+ Defaults to True. Set to False to only fetch the first page. In that case, a
+ `next_token` key in the response will indicate if more pages are available.
+ That token can be used to fetch the next page.
+ next_token: Optionally, the token to fetch the next page of results. Defaults to None.
+
+
+ Returns:
+ NodeResponse: The response object containing all statistical variables.
+
+ Example:
+ ```python
+ response = node.fetch_all_classes()
+ print(response)
+ ```
+ """
+
+ return self.fetch_property_values(
+ node_dcids="Class",
+ properties="typeOf",
+ out=False,
+ all_pages=all_pages,
+ next_token=next_token,
+ )
+
+ def fetch_entity_names(
+ self,
+ entity_dcids: str | list[str],
+ language: Optional[str] = DEFAULT_NAME_LANGUAGE,
+ fallback_language: Optional[str] = None,
+ ) -> dict[str, Name]:
+ """
+ Fetches entity names in the specified language, with optional fallback to English.
+ Args:
+ entity_dcids: A single DCID or a list of DCIDs to fetch names for.
+ language: Language code (e.g., "en", "es"). Defaults to "en" (DEFAULT_NAME_LANGUAGE).
+ fallback_language: If provided, this language will be used as a fallback if the requested
+ language is not available. If not provided, no fallback will be used.
+ Returns:
+ A dictionary mapping each DCID to a dictionary with the mapped name, language, and
+ the property used.
+ """
+
+ # Check if entity_dcids is a single string. If so, convert it to a list.
+ if isinstance(entity_dcids, str):
+ entity_dcids = [entity_dcids]
+
+ # If langauge is English, use the more efficient 'name' property.
+ name_property = (DEFAULT_NAME_PROPERTY if language == DEFAULT_NAME_LANGUAGE
+ else NAME_WITH_LANGUAGE_PROPERTY)
+
+ # Fetch names the given entity DCIDs.
+ data = self.fetch_property_values(
+ node_dcids=entity_dcids, properties=name_property).get_properties()
+
+ names: dict[str, Name] = {}
+
+ # Iterate through the fetched data and populate the names dictionary.
+ for dcid, properties in data.items():
+ if not properties:
+ continue
+ if language == "en":
+ name = extract_name_from_english_name_property(
+ properties=properties.get(name_property, []))
+ lang_used = "en"
+ else:
+ name, lang_used = extract_name_from_property_with_language(
+ properties=properties.get(name_property, []),
+ language=language,
+ fallback_language=fallback_language,
+ )
+ if name:
+ names[dcid] = Name(
+ value=name,
+ language=lang_used,
+ property=name_property,
+ )
+
+ return names
+
+ def _fetch_contained_in_place(
+ self,
+ node_dcids: str | list[str],
+ out: bool = True,
+ contained_type: Optional[str] = None,
+ as_dict: bool = False,
+ ) -> dict[str, list[Node | dict]]:
+ """Fetches places that contain or are contained in the given nodes. Uses the
+ `containedInPlace` property to fetch parent or child place relationships.
+
+ Args:
+ node_dcids (str | list[str]): One or more DCIDs representing geographic places.
+ out (bool, optional): If True, fetch places contained in the given node(s).
+ If False, fetch places that contain the given node(s). Defaults to True.
+ contained_type (str, optional): Optional type constraint (e.g., 'Country',
+ 'Country'). If provided, only fetches places of that type.
+ as_dict (bool, optional): If True, returns the result as a dictionary of
+ lists of dictionaries. If False, returns Node objects. Defaults to False.
+
+ Returns:
+ dict[str, list[dict]] | dict[str, list[Any]]: A dictionary where keys are DCIDs
+ and values are lists of place relationships, either as raw objects or
+ dictionaries (if `as_dict` is True).
+ """
+ if out and contained_type:
+ raise ValueError("When 'out' is True, `contained_type' must be None.")
+
+ prop = "containedInPlace+" if contained_type else "containedInPlace"
+
+ data = self.fetch_property_values(
+ node_dcids=node_dcids,
+ properties=prop,
+ out=out,
+ constraints=f"typeOf:{contained_type}" if contained_type else None,
+ ).get_properties()
+
+ result = {}
+ for entity, property_nodes in data.items():
+ nodes = property_nodes.get(prop, [])
+ result[entity] = [node.to_dict() for node in nodes] if as_dict else nodes
+
+ return result
+
+ def fetch_place_parents(
+ self,
+ place_dcids: str | list[str],
+ *,
+ as_dict: bool = True,
+ ) -> dict[str, list[Node | dict]]:
+ """Fetches the direct parents of one or more entities using the 'containedInPlace' property.
+
+ Args:
+ place_dcids (str | list[str]): A single place DCID or a list of DCIDs to query.
+ as_dict (bool): If True, returns a dictionary mapping each input DCID to its
+ immediate parent entities. If False, returns a dictionary of Node objects.
+
+ Returns:
+ dict[str, list[Node | dict]]: A dictionary mapping each input DCID to a list of its
+ immediate parent entities. Each parent is represented as a Node object or
+ as a dictionary with the same data.
+ """
+ return self._fetch_contained_in_place(
+ node_dcids=place_dcids,
+ out=True,
+ contained_type=None,
+ as_dict=as_dict,
+ )
+
+ def fetch_place_children(
+ self,
+ place_dcids: str | list[str],
+ *,
+ children_type: Optional[str] = None,
+ as_dict: bool = True,
+ ) -> dict[str, list[Node | dict]]:
+ """Fetches the direct children of one or more entities using the 'containedInPlace' property.
+
+ Args:
+ place_dcids (str | list[str]): A single place DCID or a list of DCIDs to query.
+ children_type (str, optional): The type of the child entities to
+ fetch (e.g., 'Country', 'State', 'IPCCPlace_50'). If None, fetches all child types.
+ as_dict (bool): If True, returns a dictionary mapping each input DCID to its
+ immediate children entities. If False, returns a dictionary of Node objects.
+
+ Returns:
+ dict[str, list[Node | dict]]: A dictionary mapping each input DCID to a list of its
+ immediate children. Each child is represented as a Node object or as a dictionary with
+ the same data.
+ """
+ return self._fetch_contained_in_place(
+ node_dcids=place_dcids,
+ out=False,
+ contained_type=children_type,
+ as_dict=as_dict,
+ )
+
+ def _fetch_place_relationships(
+ self,
+ place_dcids: str | list[str],
+ as_tree: bool = False,
+ *,
+ contained_type: Optional[str] = None,
+ relationship: Literal["parents", "children"],
+ max_concurrent_requests: Optional[int] = PLACES_MAX_WORKERS,
+ ) -> dict[str, list[dict[str, str]] | dict]:
+ """Fetches a full ancestors/descendants map per place DCID.
+
+ For each input place DCID, this method builds the complete graph using a
+ breadth-first traversal and parallel fetching.
+
+ Args:
+ place_dcids (str | list[str]): One or more DCIDs of the entities whose ancestry
+ will be fetched.
+ as_tree (bool): If True, returns a nested tree structure; otherwise, returns a flat list.
+ Defaults to False.
+ contained_type (Optional[str]): The type of the ancestry to fetch (e.g., 'Country', 'State').
+ If None, fetches all ancestry types.
+ relationship (Literal["parents", "children"]): The type of relationship to fetch.
+ max_concurrent_requests (Optional[int]): The maximum number of concurrent requests to make.
+ Defaults to PLACES_MAX_WORKERS.
+ Returns:
+ dict[str, list[dict[str, str]] | dict]: A dictionary mapping each input DCID to either:
+ - A flat list of Node dictionaries (if `as_tree` is False), or
+ - A nested tree (if `as_tree` is True).
+ """
+
+ if isinstance(place_dcids, str):
+ place_dcids = [place_dcids]
+
+ result = {}
+
+ # Create a partial function to fetch relationships with the current parameters
+ fetch_fn = partial(
+ fetch_relationship_lru,
+ self,
+ contained_type=contained_type,
+ relationship=relationship,
+ )
+
+ # Use a thread pool to fetch ancestry graphs in parallel for each input entity
+ ctx = contextvars.copy_context()
+ with ThreadPoolExecutor(max_workers=max_concurrent_requests) as executor:
+ futures = [
+ executor.submit(ctx.run,
+ build_graph_map,
+ root=dcid,
+ fetch_fn=fetch_fn) for dcid in place_dcids
+ ]
+ # Gather ancestry maps and postprocess into flat or nested form
+ for future in futures:
+ dcid, ancestry = future.result()
+ if as_tree:
+ ancestry = build_relationship_tree(root=dcid,
+ graph=ancestry,
+ relationship_key=relationship)
+ else:
+ ancestry = flatten_relationship(ancestry)
+ result[dcid] = ancestry
+
+ return result
+
+ def fetch_place_ancestors(
+ self,
+ place_dcids: str | list[str],
+ as_tree: bool = False,
+ *,
+ max_concurrent_requests: Optional[int] = PLACES_MAX_WORKERS,
+ ) -> dict[str, list[dict[str, str]] | dict]:
+ """Fetches the full ancestry (flat or nested) for one or more entities.
+ For each input DCID, this method builds the complete ancestry graph using a
+ breadth-first traversal and parallel fetching.
+ It returns either a flat list of unique parents or a nested tree structure for
+ each entity, depending on the `as_tree` flag. The flat list matches the structure
+ of the `/api/place/parent` endpoint of the DC website.
+ Args:
+ place_dcids (str | list[str]): One or more DCIDs of the entities whose ancestry
+ will be fetched.
+ as_tree (bool): If True, returns a nested tree structure; otherwise, returns a flat list.
+ Defaults to False.
+ max_concurrent_requests (Optional[int]): The maximum number of concurrent requests to make.
+ Defaults to PLACES_MAX_WORKERS.
+ Returns:
+ dict[str, list[dict[str, str]] | dict]: A dictionary mapping each input DCID to either:
+ - A flat list of parent dictionaries (if `as_tree` is False), or
+ - A nested ancestry tree (if `as_tree` is True). Each parent is represented by
+ a dict with 'dcid', 'name', and 'type'.
+ """
+
+ return self._fetch_place_relationships(
+ place_dcids=place_dcids,
+ as_tree=as_tree,
+ contained_type=None,
+ relationship="parents",
+ max_concurrent_requests=max_concurrent_requests,
+ )
+
+ def fetch_place_descendants(
+ self,
+ place_dcids: str | list[str],
+ descendants_type: Optional[str] = None,
+ as_tree: bool = False,
+ *,
+ max_concurrent_requests: Optional[int] = PLACES_MAX_WORKERS,
+ ) -> dict[str, list[dict[str, str]] | dict]:
+ """Fetches the full descendants (flat or nested) for one or more entities.
+ For each input DCID, this method builds the complete descendants graph using a
+ breadth-first traversal and parallel fetching.
+
+ It returns either a flat list of unique child or a nested tree structure for
+ each entity, depending on the `as_tree` flag.
+
+ Args:
+ place_dcids (str | list[str]): One or more DCIDs of the entities whose descendants
+ will be fetched.
+ descendants_type (Optional[str]): The type of the descendants to fetch (e.g., 'Country', 'State').
+ If None, fetches all descendant types.
+ as_tree (bool): If True, returns a nested tree structure; otherwise, returns a flat list.
+ Defaults to False.
+ max_concurrent_requests (Optional[int]): The maximum number of concurrent requests to make.
+ Defaults to PLACES_MAX_WORKERS.
+ Returns:
+ dict[str, list[dict[str, str]] | dict]: A dictionary mapping each input DCID to either:
+ - A flat list of Node dictionaries (if `as_tree` is False), or
+ - A nested ancestry tree (if `as_tree` is True). Each child is represented by
+ a dict.
+ """
+
+ return self._fetch_place_relationships(
+ place_dcids=place_dcids,
+ as_tree=as_tree,
+ contained_type=descendants_type,
+ relationship="children",
+ max_concurrent_requests=max_concurrent_requests,
+ )
+
+ def _fetch_property_id_names(self, node_dcids: str | list[str],
+ properties: str | list[str]):
+ """Fetch target nodes for given properties and return only (dcid, name).
+
+ For each input node and each requested property, returns the list of target
+ nodes as dictionaries with ``dcid`` and ``name``.
+
+ Args:
+ node_dcids: A single DCID or a list of DCIDs to query.
+ properties: A property string or list of property strings.
+
+ Returns:
+ A mapping:
+ `{ node_dcid: { property: [ {dcid, name}, ... ], ... }, ... }`.
+ """
+ data = self.fetch_property_values(node_dcids=node_dcids,
+ properties=properties).get_properties()
+
+ result: dict[str, dict[str, list[dict]]] = {}
+
+ for node, props in data.items():
+ result.setdefault(node, {})
+ for prop, metadata in props.items():
+ dest = result[node].setdefault(prop, [])
+ for n in metadata:
+ # Prefer 'dcid', but if property is terminal, fall back to 'value'.
+ dcid = n.dcid or n.value
+ name = n.name or n.value
+ dest.append({"dcid": dcid, "name": name})
+ return result
+
+ def fetch_statvar_constraints(
+ self, variable_dcids: str | list[str]) -> StatVarConstraints:
+ """Fetch constraint property/value pairs for statistical variables, using
+ the `constraintProperties` property.
+
+ This returns, for each StatisticalVariable, the constraints that define it.
+
+ Args:
+ variable_dcids: One or more StatisticalVariable DCIDs.
+
+ Returns:
+ StatVarConstraints:
+ ``{
+ : [
+ {
+ "constraint_id": ,
+ "constraint_name": ,
+ "value_id": ,
+ "value_name": ,
+ },
+ ...
+ ],
+ ...
+ }``
+ """
+ # Ensure variable_dcids is a list
+ if isinstance(variable_dcids, str):
+ variable_dcids = [variable_dcids]
+
+ # Get constraints for the given variable DCIDs.
+ constraints_mapping = self._fetch_property_id_names(
+ node_dcids=variable_dcids, properties=[CONSTRAINT_PROPERTY])
+
+ # Per statvar mapping of dcid - name
+ per_sv_constraint_names = {}
+ # Global set of all constraint property IDs
+ all_constraint_prop_ids = set()
+
+ for sv in variable_dcids:
+ # Get the constraint properties for this statvar
+ prop_entries = constraints_mapping.get(sv,
+ {}).get(CONSTRAINT_PROPERTY, [])
+ # Map the constraint properties to their names
+ id_to_name = {entry["dcid"]: entry.get("name") for entry in prop_entries}
+ # Add an entry for this statvar to the constraint names mapping
+ per_sv_constraint_names[sv] = id_to_name
+ # Update the global set of all constraint property IDs
+ all_constraint_prop_ids.update(id_to_name.keys())
+
+ # In a single request, fetch all values for all the constraints, for all statvars.
+ values_map = self._fetch_property_id_names(
+ node_dcids=variable_dcids,
+ properties=sorted(all_constraint_prop_ids),
+ )
+
+ # Build structured response. This will include vars with no constraints (empty dicts).
+ result = {sv: [] for sv in variable_dcids}
+
+ for sv in variable_dcids:
+ constraint_names = per_sv_constraint_names.get(sv, {})
+ sv_values = values_map.get(sv, {})
+
+ for constraintId, constraintName in constraint_names.items():
+ values = sv_values.get(constraintId, [])
+ # Continue if the stat var doesn't actually define a value for one of its constraintProperties.
+ if not values:
+ continue
+
+ # Build the StatVarConstraint object
+ result[sv].append(
+ StatVarConstraint(
+ constraintId=constraintId,
+ constraintName=constraintName,
+ valueId=values[0]["dcid"],
+ valueName=values[0].get("name"),
+ ))
+
+ return StatVarConstraints.model_validate(result)
diff --git a/datacommons_client/endpoints/observation.py b/datacommons_client/endpoints/observation.py
new file mode 100644
index 00000000..a6871abb
--- /dev/null
+++ b/datacommons_client/endpoints/observation.py
@@ -0,0 +1,191 @@
+from typing import Optional
+
+from datacommons_client.endpoints.base import API
+from datacommons_client.endpoints.base import Endpoint
+from datacommons_client.endpoints.payloads import ObservationRequestPayload
+from datacommons_client.endpoints.response import ObservationResponse
+from datacommons_client.models.observation import ObservationDate
+from datacommons_client.models.observation import ObservationSelect
+from datacommons_client.utils.data_processing import group_variables_by_entity
+
+
+class ObservationEndpoint(Endpoint):
+ """
+ A class to interact with the observation API endpoint.
+
+ Args:
+ api (API): The API instance providing the environment configuration
+ (base URL, headers, authentication) to be used for requests.
+ """
+
+ def __init__(self, api: API):
+ """Initializes the ObservationEndpoint instance."""
+ super().__init__(endpoint="observation", api=api)
+
+ def fetch(
+ self,
+ variable_dcids: str | list[str],
+ date: ObservationDate | str = ObservationDate.LATEST,
+ select: Optional[list[ObservationSelect | str]] = None,
+ entity_dcids: Optional[str | list[str]] = None,
+ entity_expression: Optional[str] = None,
+ filter_facet_domains: Optional[str | list[str]] = None,
+ filter_facet_ids: Optional[str | list[str]] = None
+ ) -> ObservationResponse:
+ """
+ Fetches data from the observation endpoint.
+
+ Args:
+ variable_dcids (str | list[str]): One or more variable IDs for the data.
+ date (str | ObservationDate): The date for which data is being requested.
+ Defaults to the latest observation.
+ select (list[ObservationSelect]): Fields to include in the response.
+ Defaults to ["date", "variable", "entity", "value"].
+ entity_dcids (Optional[str | list[str]]): One or more entity IDs to filter the data.
+ entity_expression (Optional[str]): A string expression to filter entities.
+ filter_facet_domains (Optional[str | list[str]]): One or more domain names to filter the data.
+ filter_facet_ids (Optional[str | list[str]]): One or more facet IDs to filter the data.
+
+ Returns:
+ ObservationResponse: The response object containing observations for the specified query.
+ """
+ # Construct the payload
+ payload = ObservationRequestPayload(
+ date=date,
+ variable_dcids=variable_dcids,
+ select=select,
+ entity_dcids=entity_dcids,
+ entity_expression=entity_expression,
+ filter_facet_domains=filter_facet_domains,
+ filter_facet_ids=filter_facet_ids,
+ ).to_dict()
+
+ response = self.post(payload=payload)
+
+ # Send the request
+ return ObservationResponse.model_validate(response)
+
+ def fetch_observations_by_entity_type(
+ self,
+ date: ObservationDate | str,
+ parent_entity: str,
+ entity_type: str,
+ variable_dcids: str | list[str],
+ *,
+ select: Optional[list[ObservationSelect | str]] = None,
+ filter_facet_domains: Optional[str | list[str]] = None,
+ filter_facet_ids: Optional[str | list[str]] = None
+ ) -> ObservationResponse:
+ """
+ Fetches all observations for a given entity type.
+
+ Args:
+ date (ObservationDate | str): The date option for the observations.
+ Use 'all' for all dates, 'latest' for the most recent data,
+ or provide a date as a string (e.g., "2024").
+ parent_entity (str): The parent entity under which the target entities fall.
+ For example, "africa" for African countries, or "Earth" for all countries.
+ entity_type (str): The type of entities for which to fetch observations.
+ For example, "Country" or "Region".
+ variable_dcids (str | list[str]): The variable(s) to fetch observations for.
+ This can be a single variable ID or a list of IDs.
+ select (Optional[list[ObservationSelect | str]]): Fields to include in the response.
+ If not provided, defaults to ["date", "variable", "entity", "value"].
+ filter_facet_domains: Optional[str | list[str]: One or more domain names to filter the data.
+ filter_facet_ids: Optional[str | list[str]: One or more facet IDs to filter the data.
+
+ Returns:
+ ObservationResponse: The response object containing observations for the specified entity type.
+
+ Example:
+ To fetch all observations for African countries for a specific variable:
+
+ ```python
+ api = API()
+ ObservationEndpoint(api).fetch_observations_by_entity_type(
+ date="all",
+ parent_entity="africa",
+ entity_type="Country",
+ variable_dcids="sdg/SI_POV_DAY1"
+ )
+ ```
+ """
+
+ return self.fetch(
+ variable_dcids=variable_dcids,
+ date=date,
+ select=[s for s in ObservationSelect] if not select else select,
+ entity_expression=
+ f"{parent_entity}<-containedInPlace+{{typeOf:{entity_type}}}",
+ filter_facet_domains=filter_facet_domains,
+ filter_facet_ids=filter_facet_ids)
+
+ def fetch_observations_by_entity_dcid(
+ self,
+ date: ObservationDate | str,
+ entity_dcids: str | list[str],
+ variable_dcids: str | list[str],
+ *,
+ select: Optional[list[ObservationSelect | str]] = None,
+ filter_facet_domains: Optional[str | list[str]] = None,
+ filter_facet_ids: Optional[str | list[str]] = None
+ ) -> ObservationResponse:
+ """
+ Fetches all observations for a given entity type.
+
+ Args:
+ date (ObservationDate | str): The date option for the observations.
+ Use 'all' for all dates, 'latest' for the most recent data,
+ or provide a date as a string (e.g., "2024").
+ entity_dcids (str | list[str]): One or more entity IDs to filter the data.
+ variable_dcids (str | list[str]): The variable(s) to fetch observations for.
+ This can be a single variable ID or a list of IDs.
+ select (Optional[list[ObservationSelect | str]]): Fields to include in the response.
+ If not provided, defaults to ["date", "variable", "entity", "value"].
+ filter_facet_domains: Optional[str | list[str]: One or more domain names to filter the data.
+ filter_facet_ids: Optional[str | list[str]: One or more facet IDs to filter the data.
+
+ Returns:
+ ObservationResponse: The response object containing observations for the specified entity type.
+
+ Example:
+ To fetch all observations for Nigeria for a specific variable:
+
+ ```python
+ api = API()
+ ObservationEndpoint(api).fetch_observations_by_entity_dcid(
+ date="all",
+ entity_dcids="country/NGA",
+ variable_dcids="sdg/SI_POV_DAY1"
+ )
+ ```
+ """
+
+ return self.fetch(
+ variable_dcids=variable_dcids,
+ date=date,
+ select=[s for s in ObservationSelect] if not select else select,
+ entity_dcids=entity_dcids,
+ filter_facet_domains=filter_facet_domains,
+ filter_facet_ids=filter_facet_ids)
+
+ def fetch_available_statistical_variables(
+ self,
+ entity_dcids: str | list[str],
+ ) -> dict[str, list[str]]:
+ """
+ Fetches available statistical variables (which have observations) for given entities.
+ Args:
+ entity_dcids (str | list[str]): One or more entity DCIDs(s) to fetch variables for.
+ Returns:
+ dict[str, list[str]]: A dictionary mapping entity DCIDs to their available statistical variables.
+ """
+
+ # Fetch observations for the given entity DCIDs. If variable is empty list
+ # all available variables are retrieved.
+ data = self.fetch(
+ entity_dcids=entity_dcids,
+ select=[ObservationSelect.VARIABLE, ObservationSelect.ENTITY],
+ variable_dcids=[]).get_data_by_entity()
+
+ return group_variables_by_entity(data=data)
diff --git a/datacommons_client/endpoints/payloads.py b/datacommons_client/endpoints/payloads.py
new file mode 100644
index 00000000..af66334b
--- /dev/null
+++ b/datacommons_client/endpoints/payloads.py
@@ -0,0 +1,139 @@
+from typing import Optional
+
+from pydantic import Field
+from pydantic import field_serializer
+from pydantic import field_validator
+from pydantic import model_serializer
+from pydantic import model_validator
+
+from datacommons_client.models.base import BaseDCModel
+from datacommons_client.models.base import ListOrStr
+from datacommons_client.models.observation import ObservationDate
+from datacommons_client.models.observation import ObservationSelect
+from datacommons_client.models.observation import ObservationSelectList
+
+
+def normalize_list_to_string(value: str | list[str]) -> str:
+ """Converts a list of properties to a string."""
+
+ if isinstance(value, list):
+ return f"[{', '.join(value)}]"
+
+ return value
+
+
+class NodeRequestPayload(BaseDCModel):
+ """
+ A Pydantic model to structure, normalize, and validate the payload for a Node V2 API request.
+
+ Attributes:
+ node_dcids (str | list[str]): The DCID(s) of the nodes to query.
+ expression (str): The property or relation expression(s) to query.
+ """
+
+ node_dcids: ListOrStr = Field(..., serialization_alias="nodes")
+ expression: list | str = Field(..., serialization_alias="property")
+
+
+class ObservationRequestPayload(BaseDCModel):
+ """
+ A Pydantic model to structure, normalize, and validate the payload for an Observation V2 API request.
+
+ Attributes:
+ date (str): The date for which data is being requested.
+ variable_dcids (str | list[str]): One or more variable IDs for the data.
+ select (list[ObservationSelect]): Fields to include in the response.
+ Defaults to ["date", "variable", "entity", "value"].
+ entity_dcids (Optional[str | list[str]]): One or more entity IDs to filter the data.
+ entity_expression (Optional[str]): A string expression to filter entities.
+ filter_facet_domains (Optional[str | list[str]]): One or more domain names to filter the data.
+ filter_facet_ids (Optional[str | list[str]]): One or more facet IDs to filter the data.
+ """
+
+ date: ObservationDate | str = Field(default_factory=str,
+ validate_default=True)
+ variable_dcids: Optional[ListOrStr] = Field(default=None,
+ serialization_alias="variable")
+ select: Optional[list[str]] = None
+ entity_dcids: Optional[ListOrStr] = None
+ entity_expression: Optional[str | list[str]] = None
+ filter_facet_domains: Optional[ListOrStr] = None
+ filter_facet_ids: Optional[ListOrStr] = None
+
+ @field_validator("date", mode="before")
+ def _validate_date(cls, v):
+ try:
+ return ObservationDate(v)
+ except ValueError:
+ return v
+
+ @field_validator("select", mode="before")
+ def _coerce_select(cls, v):
+ return ObservationSelectList.model_validate(v).select
+
+ @field_validator("entity_expression", mode="before")
+ def _coerce_expr(cls, v):
+ if v is None:
+ return v
+ if isinstance(v, list):
+ return normalize_list_to_string(v)
+ if isinstance(v, str):
+ return v
+ raise TypeError("expression must be a string or list[str]")
+
+ @field_serializer("variable_dcids", "entity_dcids", when_used="unless-none")
+ def _serialise_dcids_fields(self, v):
+ return {"dcids": v}
+
+ @field_serializer("entity_expression", when_used="unless-none")
+ def _serialise_expression_field(self, v):
+ return {"expression": v}
+
+ @model_validator(mode="after")
+ def _check_one(self):
+ if bool(self.entity_dcids) == bool(self.entity_expression):
+ raise ValueError("Exactly one of dcids or expression must be set")
+ return self
+
+ @model_serializer(mode="wrap")
+ def _wrap_filter(self, handler):
+ # Normal dump
+ data = handler(self)
+
+ # pull out entity dcid or expression
+ entity = data.pop("entity_dcids", None) or data.pop("entity_expression",
+ None)
+
+ # add entity to the data dictionary
+ data["entity"] = entity
+
+ # pull out the two filter keys if present
+ domains = data.pop("filter_facet_domains", None)
+ ids = data.pop("filter_facet_ids", None)
+
+ # only add "filter" if at least one is set
+ if domains or ids:
+ filter_dict = {}
+ if domains is not None:
+ filter_dict["domains"] = domains
+ if ids is not None:
+ filter_dict["facet_ids"] = ids
+ data["filter"] = filter_dict
+
+ return data
+
+
+class ResolveRequestPayload(BaseDCModel):
+ """
+ A Pydantic model to structure, normalize, and validate the payload for a Resolve V2 API request.
+
+ Attributes:
+ node_dcids (str | list[str]): The DCID(s) of the nodes to query.
+ expression (str): The relation expression to query.
+ """
+
+ node_dcids: ListOrStr = Field(..., serialization_alias="nodes")
+ expression: str | list[str] | None = Field(default=None,
+ serialization_alias="property")
+ resolver: str | None = Field(default=None, serialization_alias="resolver")
+ target: str | None = Field(default=None, serialization_alias="target")
diff --git a/datacommons_client/endpoints/resolve.py b/datacommons_client/endpoints/resolve.py
new file mode 100644
index 00000000..ca7c87c4
--- /dev/null
+++ b/datacommons_client/endpoints/resolve.py
@@ -0,0 +1,161 @@
+from typing import Optional
+
+from datacommons_client.endpoints.base import API
+from datacommons_client.endpoints.base import Endpoint
+from datacommons_client.endpoints.payloads import ResolveRequestPayload
+from datacommons_client.endpoints.response import ResolveResponse
+
+
+def _resolve_correspondence_expression(from_type: str,
+ to_type: str,
+ entity_type: str | None = None) -> str:
+ """
+ Constructs a relation expression for fetching correspondence between entities of two types.
+
+ Args:
+ from_type (str): The source entity type.
+ to_type (str): The target entity type.
+ entity_type (Optional[str]): Optional type of the entities.
+
+ Returns:
+ str: The relation expression to fetch correspondence between entities of the given types.
+ """
+ return (f"<-{from_type}{{typeOf:{entity_type}}}->{to_type}"
+ if entity_type else f"<-{from_type}->{to_type}")
+
+
+class ResolveEndpoint(Endpoint):
+ """
+ A class to interact with the resolve API endpoint.
+
+ Args:
+ api (API): The API instance providing the environment configuration
+ (base URL, headers, authentication) to be used for requests.
+ """
+
+ def __init__(self, api: API):
+ """Initializes the ResolveEndpoint instance."""
+ super().__init__(endpoint="resolve", api=api)
+
+ def fetch(self,
+ node_ids: str | list[str],
+ expression: str | list[str] | None = None,
+ resolver: str | None = None,
+ target: str | None = None) -> ResolveResponse:
+ """
+ Fetches resolved data for the given nodes and expressions, identified by name,
+ coordinates, or wiki ID.
+
+ Args:
+ node_ids (str | list[str]): One or more node IDs to resolve.
+ expression (str): The relation expression to query.
+ resolver (str | None): The resolver type to use (e.g., "indicator").
+ target (str | None): The resolution target (e.g., "custom_only").
+
+ Returns:
+ ResolveResponse: The response object containing the resolved data.
+ """
+ # Check if the node_ids is a single string. If so, convert it to a list.
+ if isinstance(node_ids, str):
+ node_ids = [node_ids]
+
+ # Construct the payload
+ payload = ResolveRequestPayload(node_dcids=node_ids,
+ expression=expression,
+ resolver=resolver,
+ target=target).to_dict()
+
+ # Send the request and return the response
+ return ResolveResponse.model_validate(self.post(payload))
+
+ def fetch_indicators(self,
+ queries: str | list[str],
+ target: str | None = None) -> ResolveResponse:
+ """
+ Fetches resolved indicators (StatisticalVariables or Topics) for the given queries.
+
+ Args:
+ queries (str | list[str]): One or more queries (e.g. "population", "gdp").
+ target (str | None): Optional target for resolution (e.g., "base_only", "custom_only", "base_and_custom").
+
+ Returns:
+ ResolveResponse: The response object containing the resolved indicators.
+ """
+ return self.fetch(node_ids=queries, resolver="indicator", target=target)
+
+ def fetch_dcids_by_name(self,
+ names: str | list[str],
+ entity_type: Optional[str] = None) -> ResolveResponse:
+ """
+ Fetches DCIDs for entities by their names.
+
+ Args:
+ names (str | list[str]): One or more entity names to resolve.
+ entity_type (Optional[str]): Optional type of the entities.
+
+ Returns:
+ ResolveResponse: The response object containing the resolved DCIDs.
+ """
+
+ expression = _resolve_correspondence_expression(from_type="description",
+ to_type="dcid",
+ entity_type=entity_type)
+
+ return self.fetch(node_ids=names, expression=expression)
+
+ def fetch_dcids_by_wikidata_id(
+ self,
+ wikidata_ids: str | list[str],
+ entity_type: Optional[str] = None) -> ResolveResponse:
+ """
+ Fetches DCIDs for entities by their Wikidata IDs.
+
+ Args:
+ wikidata_ids (str | list[str]): One or more Wikidata IDs to resolve.
+ entity_type (Optional[str]): Optional type of the entities.
+
+ Returns:
+ ResolveResponse: The response object containing the resolved DCIDs.
+ """
+ expression = _resolve_correspondence_expression(from_type="wikidataId",
+ to_type="dcid",
+ entity_type=entity_type)
+
+ return self.fetch(node_ids=wikidata_ids, expression=expression)
+
+ def fetch_dcid_by_coordinates(
+ self,
+ latitude: str,
+ longitude: str,
+ entity_type: Optional[str] = None) -> ResolveResponse:
+ """
+ Fetches DCIDs for entities by their geographic coordinates.
+
+ Args:
+ latitude (str): Latitude of the entity.
+ longitude (str): Longitude of the entity.
+ entity_type (Optional[str]): Optional type of the entities to refine results
+ (e.g., "City", "State", "Country").
+
+ Returns:
+ ResolveResponse: The response object containing the resolved DCIDs.
+
+ Example:
+ To find the DCID for "Mountain View" using its latitude and longitude:
+ ```python
+ latitude = "37.42"
+ longitude = "-122.08"
+ response = client.fetch_dcid_by_coordinates(latitude=latitude, longitude=longitude)
+ print(response.entities)
+ ```
+ Note:
+ - For ambiguous results, providing an entity type (e.g., "City") can help disambiguate.
+ - The coordinates should be passed as strings in decimal format (e.g., "37.42", "-122.08").
+
+
+ """
+ expression = _resolve_correspondence_expression(from_type="geoCoordinate",
+ to_type="dcid",
+ entity_type=entity_type)
+ coordinates = f"{latitude}#{longitude}"
+ return self.fetch(node_ids=coordinates, expression=expression)
diff --git a/datacommons_client/endpoints/response.py b/datacommons_client/endpoints/response.py
new file mode 100644
index 00000000..9131ede4
--- /dev/null
+++ b/datacommons_client/endpoints/response.py
@@ -0,0 +1,266 @@
+from typing import Any, Dict, List, Optional
+
+from pydantic import Field
+from pydantic import field_validator
+
+from datacommons_client.models.base import BaseDCModel
+from datacommons_client.models.base import facetID
+from datacommons_client.models.base import NextToken
+from datacommons_client.models.base import NodeDCID
+from datacommons_client.models.base import Property
+from datacommons_client.models.node import Arcs
+from datacommons_client.models.node import FlattenedPropertiesMapping
+from datacommons_client.models.node import NodeDCIDList
+from datacommons_client.models.node import NodeList
+from datacommons_client.models.node import Properties
+from datacommons_client.models.observation import ByVariable
+from datacommons_client.models.observation import Facet
+from datacommons_client.models.observation import ObservationRecords
+from datacommons_client.models.observation import VariableByEntity
+from datacommons_client.models.resolve import Entity
+from datacommons_client.models.resolve import FlatCandidateMapping
+from datacommons_client.utils.data_processing import flatten_properties
+from datacommons_client.utils.data_processing import observations_as_records
+
+
+class NodeResponse(BaseDCModel):
+ """Represents a response from the Node endpoint of the Data Commons API.
+
+ Attributes:
+ data: A dictionary mapping node DCIDs to Arcs or Properties objects.
+ nextToken: A token for pagination, if present.
+ """
+
+ data: Dict[NodeDCID, Arcs | Properties] = Field(default_factory=dict)
+ nextToken: NextToken = None
+
+ @field_validator("data", mode="before")
+ def _discriminate_data(cls, raw_data):
+ """Discriminates between Arcs and Properties based on the presence of 'arcs' key."""
+
+ def _parse_data(
+ data: Arcs | Properties | dict[str, Any]) -> Arcs | Properties:
+ if isinstance(data, (Arcs, Properties)):
+ return data
+ if "arcs" in data:
+ return Arcs.model_validate(data)
+ return Properties.model_validate(data)
+
+ return {dcid: _parse_data(data) for dcid, data in raw_data.items()}
+
+ def get_properties(self) -> FlattenedPropertiesMapping:
+ return flatten_properties(self.data)
+
+ def extract_connected_nodes(
+ self,
+ subject_dcid: NodeDCID,
+ property_dcid: Property,
+ connected_node_types: Optional[str | list[str]] = None) -> NodeList:
+ """Retrieves Node objects in the NodeResponse connected to the subject node
+ via the specified property.
+
+ Args:
+ subject_dcid: The DCID of the starting node in the arc.
+ property_dcid: The property connecting the subject node to the desired
+ target nodes.
+ connected_node_types: Optional. A type or list of types to filter the
+ connected nodes. If provided, only connected nodes that have at least
+ one of the specified types will be returned. If omitted, all nodes from
+ the arc are returned.
+
+ Returns:
+ A list of Node objects that are connected to the subject node via the
+ specified property.
+ """
+ if isinstance(connected_node_types, str):
+ connected_node_types = [connected_node_types]
+
+ nodes = self.get_properties().get(subject_dcid, {}).get(property_dcid, [])
+
+ connected_nodes = []
+ for node in nodes:
+ if connected_node_types:
+ # Filter out nodes that are missing a list of types or do not have the
+ # desired type
+ if not node.types or not any(nt in node.types
+ for nt in connected_node_types):
+ continue
+
+ connected_nodes.append(node)
+
+ return NodeList.model_validate(connected_nodes)
+
+ def extract_connected_dcids(
+ self,
+ subject_dcid: NodeDCID,
+ property_dcid: Property,
+ connected_node_types: Optional[str | list[str]] = None) -> NodeDCIDList:
+ """Retrieves DCIDs of the Nodes in the NodeResponse connected to the subject
+ node via the specified property.
+
+ Args:
+ subject_dcid: The DCID of the starting node.
+ property_dcid: The property connecting the subject node to the desired
+ target nodes.
+ connected_node_types: Optional. A type or list of types to filter the
+ connected nodes. If provided, only DCIDs of connected nodes that have at
+ least one of the specified types will be returned. If omitted, DCIDs of
+ all nodes from the arc are returned.
+
+ Returns:
+ A list of NodeDCIDs for the nodes connected via the specified property
+ from the subject node.
+ """
+
+ connected_nodes = self.extract_connected_nodes(subject_dcid, property_dcid,
+ connected_node_types)
+
+ return NodeDCIDList.model_validate(
+ [node.dcid for node in connected_nodes if node.dcid])
+
+
+class ObservationResponse(BaseDCModel):
+ """Represents a response from the Observation endpoint of the Data Commons API.
+
+ Attributes:
+ byVariable: A dictionary of variable DCIDs and their corresponding data.
+ facets: A dictionary of facet IDs and their corresponding data.
+ """
+
+ byVariable: ByVariable = Field(default_factory=ByVariable)
+ facets: Dict[facetID, Facet] = Field(default_factory=dict)
+
+ def get_data_by_entity(self) -> VariableByEntity:
+ """Unpacks the data for each entity, for each variable.
+
+ Returns:
+ Dict: The variables object from the response.
+ """
+ raw_payload = {
+ var_dcid: var_model.byEntity
+ for var_dcid, var_model in self.byVariable.items()
+ }
+ return VariableByEntity.model_validate(raw_payload)
+
+ def to_observation_records(self) -> ObservationRecords:
+ """Returns a flat list of observation records combining date, variable, entity,
+ observation, and facet metadata.
+
+ This method transforms the nested `byVariable` and `facets` data in the ObservationResponse
+ into a flat list of dictionaries. Each dictionary (or "record") represents a single observation
+ for a variable and entity, enriched with its associated facet metadata (e.g., measurement method,
+ observation period, unit).
+
+ This format is suitable for exporting to a DataFrame or serializing to JSON for tabular or analytical use.
+
+ Returns:
+ ObservationRecord: A list of observation records, where each record contains the variable,
+ entity, date, value, facetId, and any additional metadata provided by the facet.
+ """
+ return observations_as_records(data=self.get_data_by_entity(),
+ facets=self.facets)
+
+ def get_facets_metadata(self) -> Dict[str, dict]:
+ """Extract metadata about StatVars from the response. This data is
+ structured as a dictionary of StatVars, each containing a dictionary of
+ facets with their corresponding metadata.
+
+ Returns:
+ Dict[str, dict]: A dictionary of StatVars with their associated metadata,
+ including earliest and latest observation dates, observation counts,
+ measurementMethod, observationPeriod, and unit, etc.
+ """
+ # Dictionary to store metadata
+ metadata = {}
+
+ # Extract information from byVariable
+ data_by_entity = self.get_data_by_entity()
+
+ # Extract facet information
+ facets_info = self.facets
+
+ for dcid, variables in data_by_entity.items():
+ metadata[dcid] = {}
+
+ for entity_id, entity in variables.items():
+ for facet in entity.orderedFacets:
+ facet_metadata = metadata[dcid].setdefault(
+ facet.facetId,
+ {
+ "earliestDate": {},
+ "latestDate": {},
+ "obsCount": {},
+ },
+ )
+
+ facet_metadata["earliestDate"][entity_id] = facet.earliestDate
+ facet_metadata["latestDate"][entity_id] = facet.latestDate
+ facet_metadata["obsCount"][entity_id] = facet.obsCount
+
+ # Merge additional facet details
+ facet_metadata.update(facets_info.get(facet.facetId, {}))
+
+ return metadata
+
+ def find_matching_facet_id(self, property_name: str,
+ value: str | list[str]) -> list[str]:
+ """Finds facet IDs that match a given property and value.
+
+ Args:
+ property_name (str): The property to match.
+ value (str | list): The value to match. Can be a string, number, or a list of values.
+ Returns:
+ list[str]: A list of facet IDs that match the property and value.
+ """
+ # Initialize an empty list to store matching facet IDs
+ matching_facet_ids = []
+
+ # Iterate over the facets metadata to find matching facet IDs
+ for facet_data in self.get_facets_metadata().values():
+
+ # Iterate over each facet and its associated metadata
+ for facet_id, metadata in facet_data.items():
+
+ # Get the value of the specified property from the data
+ prop_value = metadata.get(property_name)
+
+ # Check if the property value matches the specified value
+ if isinstance(value, list):
+ if prop_value in value:
+ matching_facet_ids.append(facet_id)
+ elif prop_value == value:
+ matching_facet_ids.append(facet_id)
+
+ return matching_facet_ids
+
+
+class ResolveResponse(BaseDCModel):
+ """Represents a response from the Resolve endpoint of the Data Commons API.
+
+ Attributes:
+ entities (List[Entity]): A list of entities resolved by the API, each
+ containing the query node and its associated candidates.
+ """
+
+ entities: list[Entity] = Field(default_factory=list)
+
+ def to_flat_dict(self) -> FlatCandidateMapping:
+ """
+ Flattens resolved candidate data into a dictionary where each node maps to its candidates.
+
+ Returns:
+ dict[str, Any]: A dictionary mapping nodes to their candidates.
+ If a node has only one candidate, it maps directly to the candidate instead of a list.
+ """
+ items = {}
+
+ for entity in self.entities:
+ node = entity.node
+ if len(entity.candidates) == 1:
+ items[node] = entity.candidates[0].dcid
+ else:
+ items[node] = [candidate.dcid for candidate in entity.candidates]
+
+ items = FlatCandidateMapping.model_validate(items)
+
+ return items
diff --git a/datacommons_client/models/__init__.py b/datacommons_client/models/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/datacommons_client/models/base.py b/datacommons_client/models/base.py
new file mode 100644
index 00000000..6f3e3e6f
--- /dev/null
+++ b/datacommons_client/models/base.py
@@ -0,0 +1,123 @@
+from collections.abc import Mapping, MutableSequence
+from pprint import pformat
+from typing import Annotated, Any, Iterable, Optional, TypeAlias
+
+from pydantic import BaseModel
+from pydantic import BeforeValidator
+from pydantic import ConfigDict
+from pydantic import RootModel
+
+
+def listify(v: Any) -> list[str]:
+ if isinstance(v, (str, bytes)):
+ return [v]
+ if not isinstance(v, Iterable):
+ return [v]
+ return list(v)
+
+
+variableDCID: TypeAlias = str
+entityDCID: TypeAlias = str
+facetID: TypeAlias = str
+ListOrStr = Annotated[list[str] | str, BeforeValidator(listify)]
+NextToken: TypeAlias = Optional[str]
+NodeDCID: TypeAlias = str
+ArcLabel: TypeAlias = str
+Property: TypeAlias = str
+PropertyList: TypeAlias = list[Property]
+Query: TypeAlias = str
+DominantType: TypeAlias = str
+
+
+class BaseDCModel(BaseModel):
+ """Provides serialization methods for the Pydantic models used by the client."""
+
+ model_config = ConfigDict(validate_by_name=True,
+ validate_default=True,
+ validate_by_alias=True,
+ use_enum_values=True,
+ serialize_by_alias=True)
+
+ def __str__(self) -> str:
+ """Returns a string representation of the instance."""
+ return self.to_json()
+
+ def to_dict(self, exclude_none: bool = True) -> dict[str, Any]:
+ """Converts the instance to a dictionary.
+
+ Args:
+ exclude_none: If True, only include non-empty values in the response.
+
+ Returns:
+ Dict[str, Any]: The dictionary representation of the instance.
+ """
+
+ return self.model_dump(mode="python", exclude_none=exclude_none)
+
+ def to_json(self, exclude_none: bool = True) -> str:
+ """Converts the instance to a JSON string.
+
+ Args:
+ exclude_none: If True, only include non-empty values in the response.
+
+ Returns:
+ str: The JSON string representation of the instance.
+ """
+ return self.model_dump_json(exclude_none=exclude_none, indent=2)
+
+
+class DictLikeRootModel(RootModel, Mapping):
+ """A base class for models that can be treated as dictionaries."""
+
+ def __repr__(self) -> str:
+ return f"{self.__class__.__name__}({self.root})"
+
+ def __str__(self) -> str:
+ return pformat(self.root, compact=True, width=80)
+
+ def __getitem__(self, key: str) -> Any:
+ return self.root[key]
+
+ def __iter__(self) -> Iterable[Any]:
+ return iter(self.root)
+
+ def __len__(self) -> int:
+ return len(self.root)
+
+ def __eq__(self, other: Any) -> bool:
+ if isinstance(other, DictLikeRootModel):
+ return self.root == other.root
+ else:
+ return self.root == other
+
+
+class ListLikeRootModel(MutableSequence, RootModel):
+ """A base class for models that can be treated as lists."""
+
+ def __repr__(self) -> str:
+ return f"{self.__class__.__name__}({self.root})"
+
+ def __str__(self) -> str:
+ return pformat(self.root, compact=True, width=80)
+
+ def __getitem__(self, index: int) -> Any:
+ return self.root[index]
+
+ def __setitem__(self, index: int, value: Any) -> None:
+ self.root[index] = value
+
+ def __delitem__(self, index: int) -> None:
+ del self.root[index]
+
+ def __len__(self) -> int:
+ return len(self.root)
+
+ def __eq__(self, other: Any) -> bool:
+ if isinstance(other, ListLikeRootModel):
+ return self.root == other.root
+ else:
+ return self.root == other
+
+ def insert(self, index: int, item: Any) -> None:
+ """Inserts an item at a specified index in the root list."""
+ self.root.insert(index, item)
diff --git a/datacommons_client/models/node.py b/datacommons_client/models/node.py
new file mode 100644
index 00000000..00bf3472
--- /dev/null
+++ b/datacommons_client/models/node.py
@@ -0,0 +1,109 @@
+from typing import Optional
+
+from pydantic import Field
+
+from datacommons_client.models.base import ArcLabel
+from datacommons_client.models.base import BaseDCModel
+from datacommons_client.models.base import DictLikeRootModel
+from datacommons_client.models.base import ListLikeRootModel
+from datacommons_client.models.base import NodeDCID
+from datacommons_client.models.base import Property
+from datacommons_client.models.base import PropertyList
+
+
+class Node(BaseDCModel):
+ """Represents an individual node in the Data Commons knowledge graph.
+
+ Attributes:
+ dcid: The unique identifier for the node.
+ name: The name of the node.
+ provenanceId: The provenance ID for the node.
+ types: The types associated with the node.
+ value: The value of the node.
+ """
+ dcid: Optional[str] = None
+ name: Optional[str] = None
+ provenanceId: Optional[str | list[str]] = None
+ types: Optional[list[str]] = None
+ value: Optional[str] = None
+
+
+class Name(BaseDCModel):
+ """Represents a name associated with an Entity (node).
+
+ Attributes:
+ value: The name of the Entity
+ language: The language of the name
+ property: The property used to get the name
+ """
+
+ value: str
+ language: str
+ property: str
+
+
+class NodeGroup(BaseDCModel):
+ """Represents a group of nodes in the Data Commons knowledge graph.
+
+ Attributes:
+ nodes: A list of Node objects in the group.
+ """
+
+ nodes: list[Node] = Field(default_factory=list)
+
+
+class Arcs(BaseDCModel):
+ """Represents arcs in the Data Commons knowledge graph.
+
+ Attributes:
+ arcs: A dictionary mapping arc labels to NodeGroup objects.
+ """
+
+ arcs: dict[ArcLabel, NodeGroup] = Field(default_factory=dict)
+
+
+class Properties(BaseDCModel):
+ """Represents a group of properties in the Data Commons knowledge graph.
+
+ Attributes:
+ properties: A list of property strings.
+ """
+
+ properties: Optional[PropertyList] = None
+
+
+class FlattenedPropertiesMapping(BaseDCModel,
+ DictLikeRootModel[dict[NodeDCID,
+ PropertyList]]):
+ """A model to represent a mapping of node DCIDs to their properties."""
+
+
+class FlattenedArcsMapping(BaseDCModel,
+ DictLikeRootModel[dict[NodeDCID, dict[Property,
+ list[Node]]]]):
+ """A model to represent a mapping of node DCIDs to their arcs."""
+
+
+class NodeList(BaseDCModel, ListLikeRootModel[list[Node]]):
+ """A root model whose value is a list of Node objects."""
+
+
+class NodeDCIDList(BaseDCModel, ListLikeRootModel[list[NodeDCID]]):
+ """A root model whose value is a list of NodeDCID strings."""
+
+
+class StatVarConstraint(BaseDCModel):
+ """Represents a constraint for a statistical variable."""
+
+ constraintId: NodeDCID
+ constraintName: Optional[str] = None
+ valueId: NodeDCID
+ valueName: Optional[str] = None
+
+
+class StatVarConstraints(BaseDCModel,
+ DictLikeRootModel[dict[NodeDCID,
+ list[StatVarConstraint]]]):
+ """A root model whose value is a dictionary of statvar ids - a list of StatVarConstraint objects.
+ This model is used to represent constraints associated with statistical variables.
+ """
diff --git a/datacommons_client/models/observation.py b/datacommons_client/models/observation.py
new file mode 100644
index 00000000..43465889
--- /dev/null
+++ b/datacommons_client/models/observation.py
@@ -0,0 +1,208 @@
+from enum import Enum
+from typing import List, Optional
+
+from pydantic import Field
+from pydantic import field_validator
+from pydantic import model_serializer
+from pydantic import RootModel
+
+from datacommons_client.models.base import BaseDCModel
+from datacommons_client.models.base import DictLikeRootModel
+from datacommons_client.models.base import entityDCID
+from datacommons_client.models.base import facetID
+from datacommons_client.models.base import ListLikeRootModel
+from datacommons_client.models.base import variableDCID
+from datacommons_client.utils.error_handling import InvalidObservationSelectError
+
+
+class ObservationDate(str, Enum):
+ LATEST = "LATEST"
+ ALL = ""
+
+ @classmethod
+ def _missing_(cls, value):
+ if isinstance(value, str):
+ u = value.strip().upper()
+ if u == "LATEST":
+ return cls.LATEST
+ if u in ("ALL", ""):
+ return cls.ALL
+ raise ValueError(f"Invalid date value: '{value}'. Only 'LATEST' or"
+ f" '' (empty string) are allowed.")
+
+
+class ObservationSelect(str, Enum):
+ DATE = "date"
+ VARIABLE = "variable"
+ ENTITY = "entity"
+ VALUE = "value"
+ FACET = "facet"
+
+ @classmethod
+ def valid_values(cls):
+ """Returns a list of valid enum values."""
+ return sorted(cls._value2member_map_.keys())
+
+ @classmethod
+ def _missing_(cls, value):
+ """Handle missing enum values by raising a custom error."""
+ message = f"Invalid `select` Field: '{value}'. Only {', '.join(cls.valid_values())} are allowed."
+ raise InvalidObservationSelectError(message=message)
+
+
+class ObservationSelectList(RootModel[list[ObservationSelect]]):
+ """A model to represent a list of ObservationSelect values.
+
+ Attributes:
+ select (List[ObservationSelect]): A list of ObservationSelect enum values.
+ """
+
+ root: Optional[list[ObservationSelect | str]] = None
+
+ @field_validator("root", mode="before")
+ def _validate_select(cls, v):
+ if v is None:
+ select = [
+ ObservationSelect.DATE,
+ ObservationSelect.VARIABLE,
+ ObservationSelect.ENTITY,
+ ObservationSelect.VALUE,
+ ]
+ else:
+ select = v
+
+ select = [ObservationSelect(s).value for s in select]
+
+ required_select = {"variable", "entity"}
+
+ missing_fields = required_select - set(select)
+ if missing_fields:
+ raise InvalidObservationSelectError(message=(
+ f"The 'select' field must include at least the following: {', '.join(required_select)} "
+ f"(missing: {', '.join(missing_fields)})"))
+
+ return select
+
+ @property
+ def select(self) -> list[str]:
+ """Return select values directly as list"""
+ return self.root or []
+
+
+class Observation(BaseDCModel):
+ """Represents an observation with a date and value.
+
+ Attributes:
+ date (str): The date of the observation.
+ value (float): Optional. The value of the observation.
+ """
+
+ date: Optional[str] = None
+ value: Optional[float] = None
+
+
+class OrderedFacet(BaseDCModel):
+ """Represents ordered facets of observations.
+
+ Attributes:
+ earliestDate (str): The earliest date in the observations.
+ facetId (str): The identifier for the facet.
+ latestDate (str): The latest date in the observations.
+ obsCount (int): The total number of observations.
+ observations (List[Observation]): A list of observations associated with the facet.
+ """
+
+ earliestDate: Optional[str] = None
+ facetId: Optional[str] = None
+ latestDate: Optional[str] = None
+ obsCount: Optional[int] = None
+ observations: list[Observation] = Field(default_factory=list)
+
+
+class OrderedFacets(BaseDCModel):
+ """Represents a list of ordered facets.
+ """
+ orderedFacets: list[OrderedFacet] = Field(default_factory=list)
+
+
+class Variable(BaseDCModel):
+ """Represents a variable with data grouped by entity.
+
+ Attributes:
+ byEntity (dict[entityDCID, OrderedFacets]): A dictionary mapping
+ entities to their ordered facets.
+ """
+
+ byEntity: dict[entityDCID, OrderedFacets] = Field(default_factory=dict)
+
+
+class Facet(BaseDCModel):
+ """Represents metadata for a facet.
+
+ Attributes:
+ importName (str): The name of the data import.
+ measurementMethod (str): The method used to measure the data.
+ observationPeriod (str): The period over which the observations were made.
+ provenanceUrl (str): The URL of the data's provenance.
+ unit (str): The unit of the observations.
+ """
+
+ importName: Optional[str] = None
+ measurementMethod: Optional[str] = None
+ observationPeriod: Optional[str] = None
+ provenanceUrl: Optional[str] = None
+ unit: Optional[str] = None
+
+
+class ByVariable(BaseDCModel, DictLikeRootModel[dict[variableDCID, Variable]]):
+ """A root model whose value is a dict mapping variableDCID to Variable."""
+
+
+class VariableByEntity(BaseDCModel,
+ DictLikeRootModel[dict[variableDCID,
+ dict[entityDCID,
+ OrderedFacets]]]):
+ """A root model whose value is a dict mapping entityDCID to Variable."""
+
+
+class ObservationRecord(Observation, Facet):
+ """Represents a record of observations for a specific variable and entity.
+
+ Attributes:
+ date (str): The date of the observation.
+ value (float): The value of the observation.
+ """
+
+ entity: Optional[entityDCID] = None
+ variable: Optional[variableDCID] = None
+ facetId: Optional[facetID] = None
+
+ _order = [
+ "date", "entity", "variable", "facetId", "importName",
+ "measurementMethod", "observationPeriod", "provenanceUrl", "unit", "value"
+ ]
+
+ @model_serializer(mode="wrap")
+ def _reorder(self, helper):
+ """Reorders the fields for serialization."""
+ data = helper(self)
+ ordered = {}
+
+ # Ensure the order of fields matches the specified order
+ for key in self._order:
+ if key in data:
+ ordered[key] = data.pop(key)
+
+ # Add any remaining fields that were not in the order list
+ ordered.update(data)
+
+ # Ensure the 'value' field is always at the end
+ if "value" in ordered:
+ ordered["value"] = ordered.pop("value")
+
+ return ordered
+
+
+class ObservationRecords(BaseDCModel,
+ ListLikeRootModel[list[ObservationRecord]]):
+ """A root model whose value is a list of ObservationRecord."""
diff --git a/datacommons_client/models/resolve.py b/datacommons_client/models/resolve.py
new file mode 100644
index 00000000..919a9e36
--- /dev/null
+++ b/datacommons_client/models/resolve.py
@@ -0,0 +1,42 @@
+from typing import List, Optional
+
+from pydantic import Field
+
+from datacommons_client.models.base import BaseDCModel
+from datacommons_client.models.base import DictLikeRootModel
+from datacommons_client.models.base import DominantType
+from datacommons_client.models.base import NodeDCID
+from datacommons_client.models.base import Query
+
+
+class Candidate(BaseDCModel):
+ """Represents a candidate in the resolution response.
+
+ Attributes:
+ dcid (DCID): The Data Commons ID for the candidate.
+ dominantType (Optional[DominantType]): The dominant type of the candidate,
+ if available. This represents the primary type associated with the DCID.
+ """
+
+ dcid: NodeDCID = Field(default_factory=str)
+ dominantType: Optional[DominantType] = None
+ metadata: dict[str, str] | None = None
+ typeOf: list[str] | None = None
+
+
+class Entity(BaseDCModel):
+ """Represents an entity with its resolution candidates.
+
+ Attributes:
+ node (Query): The query string or node being resolved.
+ candidates (List[Candidate]): A list of candidates that match the query.
+ """
+
+ node: Query
+ candidates: list[Candidate] = Field(default_factory=list)
+
+
+class FlatCandidateMapping(BaseDCModel,
+ DictLikeRootModel[dict[Query,
+ list[NodeDCID] | NodeDCID]]):
+ """A model to represent a mapping of queries to candidates."""
diff --git a/datacommons_client/tests/README.MD b/datacommons_client/tests/README.MD
new file mode 100644
index 00000000..e69de29b
diff --git a/datacommons_client/tests/endpoints/test_base.py b/datacommons_client/tests/endpoints/test_base.py
new file mode 100644
index 00000000..eb81c71a
--- /dev/null
+++ b/datacommons_client/tests/endpoints/test_base.py
@@ -0,0 +1,291 @@
+from unittest.mock import patch
+
+import pytest
+
+from datacommons_client.endpoints.base import API
+from datacommons_client.endpoints.base import Endpoint
+
+
+@patch(
+ "datacommons_client.endpoints.base.resolve_instance_url",
+ return_value="https://api.datacommons.org/v2",
+)
+@patch(
+ "datacommons_client.endpoints.base.check_instance_is_valid",
+ return_value="https://api.datacommons.org/v2",
+)
+def test_api_initialization_default(mock_check_instance, mock_resolve_instance):
+ """Tests default API initialization with `datacommons.org` instance."""
+ api = API()
+
+ assert api.base_url == "https://api.datacommons.org/v2"
+ assert api.headers == {
+ "Content-Type": "application/json",
+ "x-surface": "clientlib-python"
+ }
+ mock_resolve_instance.assert_called_once_with("datacommons.org")
+
+
+@patch(
+ "datacommons_client.endpoints.base.check_instance_is_valid",
+ return_value="https://custom_instance.api/v2",
+)
+def test_api_initialization_with_url(mock_check_instance):
+ """Tests API initialization with a fully qualified URL."""
+ api = API(url="https://custom_instance.api/v2")
+ assert api.base_url == "https://custom_instance.api/v2"
+ assert api.headers == {
+ "Content-Type": "application/json",
+ "x-surface": "clientlib-python"
+ }
+ mock_check_instance.assert_called_once_with("https://custom_instance.api/v2",
+ api_key=None)
+
+
+@patch(
+ "datacommons_client.endpoints.base.resolve_instance_url",
+ return_value="https://custom-instance/api/v2",
+)
+def test_api_initialization_with_dc_instance(mock_resolve_instance_url):
+ """Tests API initialization with a custom Data Commons instance."""
+ api = API(dc_instance="custom-instance")
+
+ assert api.base_url == "https://custom-instance/api/v2"
+ assert api.headers == {
+ "Content-Type": "application/json",
+ "x-surface": "clientlib-python"
+ }
+ mock_resolve_instance_url.assert_called_once_with("custom-instance")
+
+
+@patch(
+ "datacommons_client.endpoints.base.resolve_instance_url",
+ return_value="https://custom-instance/api/v2",
+)
+def test_api_initialization_with_surface_header(mock_url):
+ """Tests API initialization with a surface header """
+ api = API(dc_instance="custom-instance", surface_header_value="mcp-1.1.0")
+ assert api.headers == {
+ "Content-Type": "application/json",
+ "x-surface": "mcp-1.1.0"
+ }
+
+
+def test_api_initialization_invalid_args():
+ """Tests API initialization with both `dc_instance` and `url` raises a ValueError."""
+ with pytest.raises(ValueError):
+ API(dc_instance="custom-instance", url="https://custom.api/v2")
+
+
+@patch(
+ "datacommons_client.endpoints.base.resolve_instance_url",
+ return_value="https://custom-instance/api/v2",
+)
+def test_build_headers_without_api_key(mock_url):
+ """Tests building headers without an API key."""
+ api = API(dc_instance="custom-instance")
+ assert api.headers["Content-Type"] == "application/json"
+ assert "X-API-Key" not in api.headers
+
+
+@patch(
+ "datacommons_client.endpoints.base.resolve_instance_url",
+ return_value="https://custom-instance/api/v2",
+)
+def test_build_headers_with_api_key_no_surface(mock_url):
+ """Tests building headers with an API key."""
+ api = API(dc_instance="custom-instance", api_key="test_key")
+ assert api.headers["Content-Type"] == "application/json"
+ assert api.headers["X-API-Key"] == "test_key"
+
+
+@patch(
+ "datacommons_client.endpoints.base.resolve_instance_url",
+ return_value="https://custom-instance/api/v2",
+)
+def test_build_headers_with_api_key_and_surface(mock_url):
+ """Tests building headers with an API key and a surface header."""
+ api = API(dc_instance="custom-instance",
+ api_key="test_key",
+ surface_header_value="mcp-1.0")
+ assert api.headers["Content-Type"] == "application/json"
+ assert api.headers["X-API-Key"] == "test_key"
+ assert api.headers["x-surface"] == "mcp-1.0"
+
+
+@patch(
+ "datacommons_client.endpoints.base.post_request",
+ return_value={"success": True},
+)
+@patch(
+ "datacommons_client.endpoints.base.check_instance_is_valid",
+ return_value="https://custom_instance.api/v2",
+)
+def test_api_post_request(mock_check_instance, mock_post_request):
+ """Tests making a POST request using the API object."""
+ api = API(url="https://custom_instance.api/v2")
+ payload = {"key": "value"}
+
+ response = api.post(payload=payload, endpoint="test-endpoint", all_pages=True)
+ assert response == {"success": True}
+ mock_post_request.assert_called_once_with(
+ url="https://custom_instance.api/v2/test-endpoint",
+ payload=payload,
+ headers=api.headers,
+ all_pages=True,
+ next_token=True)
+
+
+@patch(
+ "datacommons_client.endpoints.base.post_request",
+ return_value={"success": True},
+)
+@patch(
+ "datacommons_client.endpoints.base.check_instance_is_valid",
+ return_value="https://custom.api/v2",
+)
+def test_endpoint_post_request(mock_check_instance, mock_post_request):
+ """Tests making a POST request using the Endpoint object."""
+ api = API(url="https://custom.api/v2")
+ endpoint = Endpoint(endpoint="node", api=api)
+ payload = {"key": "value"}
+
+ response = endpoint.post(payload=payload, all_pages=True)
+ assert response == {"success": True}
+ mock_post_request.assert_called_once_with(url="https://custom.api/v2/node",
+ payload=payload,
+ headers=api.headers,
+ all_pages=True,
+ next_token=None)
+
+
+@patch(
+ "datacommons_client.endpoints.base.check_instance_is_valid",
+ return_value="https://custom_instance.api/v2",
+)
+def test_api_post_request_invalid_payload(mock_check_instance):
+ """Tests that an invalid payload raises a ValueError."""
+ api = API(url="https://custom_instance.api/v2")
+
+ with pytest.raises(ValueError):
+ api.post(payload=["invalid", "payload"], endpoint="test-endpoint")
+
+
+@patch(
+ "datacommons_client.endpoints.base.check_instance_is_valid",
+ return_value="https://custom_instance.api/v2",
+)
+def test_endpoint_initialization(mock_check_instance):
+ """Tests initializing an Endpoint with a valid API instance."""
+ api = API(url="https://custom_instance.api/v2")
+ endpoint = Endpoint(endpoint="node", api=api)
+
+ assert endpoint.endpoint == "node"
+ assert endpoint.api is api
+
+
+@patch(
+ "datacommons_client.endpoints.base.check_instance_is_valid",
+ return_value="https://custom.api/v2",
+)
+def test_endpoint_repr(mock_check_instance):
+ """Tests the string representation of the Endpoint object."""
+ api = API(url="https://custom.api/v2")
+ endpoint = Endpoint(endpoint="node", api=api)
+
+ assert (
+ repr(endpoint) == ">")
+
+
+@patch(
+ "datacommons_client.endpoints.base.post_request",
+ return_value={"success": True},
+)
+@patch(
+ "datacommons_client.endpoints.base.check_instance_is_valid",
+ return_value="https://custom.api/v2",
+)
+def test_endpoint_post_request(mock_check_instance, mock_post_request):
+ """Tests making a POST request using the Endpoint object."""
+ api = API(url="https://custom.api/v2")
+ endpoint = Endpoint(endpoint="node", api=api)
+ payload = {"key": "value"}
+
+ response = endpoint.post(payload=payload, all_pages=True)
+ assert response == {"success": True}
+ mock_post_request.assert_called_once_with(url="https://custom.api/v2/node",
+ payload=payload,
+ headers=api.headers,
+ all_pages=True,
+ next_token=None)
+
+
+@pytest.mark.parametrize("all_pages", [True, False])
+@pytest.mark.parametrize("next_token", ["token123", None])
+@patch(
+ "datacommons_client.endpoints.base.post_request",
+ return_value={"success": True},
+)
+@patch(
+ "datacommons_client.endpoints.base.check_instance_is_valid",
+ return_value="https://custom_instance.api/v2",
+)
+def test_api_post_request(mock_check_instance, mock_post_request, all_pages,
+ next_token):
+ """Tests making a POST request using the API object with and without max_pages."""
+ api = API(url="https://custom_instance.api/v2")
+ payload = {"key": "value"}
+
+ response = api.post(payload=payload,
+ endpoint="test-endpoint",
+ all_pages=all_pages,
+ next_token=next_token)
+ assert response == {"success": True}
+ mock_post_request.assert_called_once_with(
+ url="https://custom_instance.api/v2/test-endpoint",
+ payload=payload,
+ headers=api.headers,
+ all_pages=all_pages,
+ next_token=next_token)
+
+
+@patch(
+ "datacommons_client.endpoints.base.check_instance_is_valid",
+ return_value="https://custom.api/v2",
+)
+def test_endpoint_post_request_invalid_payload(mock_check_instance):
+ """Tests that an invalid payload raises a ValueError in the Endpoint post method."""
+ api = API(url="https://custom.api/v2")
+ endpoint = Endpoint(endpoint="node", api=api)
+
+ with pytest.raises(ValueError):
+ endpoint.post(payload=["invalid", "payload"])
+
+
+@patch(
+ "datacommons_client.endpoints.base.check_instance_is_valid",
+ return_value="https://custom.api/v2",
+)
+def test_api_repr(mock_check_instance):
+ """Tests the __repr__ method of the API class."""
+ # Without API key
+ api = API(url="https://custom.api/v2")
+ assert repr(api) == ""
+
+ # With API key
+ api_with_key = API(url="https://custom.api/v2", api_key="test_key")
+ assert (
+ repr(api_with_key) == "")
+
+
+@patch(
+ "datacommons_client.endpoints.base.check_instance_is_valid",
+ return_value="https://custom.api/v2",
+)
+def test_endpoint_repr(mock_check_instance):
+ """Tests the __repr__ method of the Endpoint class."""
+ api = API(url="https://custom.api/v2")
+ endpoint = Endpoint(endpoint="node", api=api)
+
+ expected_repr = ">"
+ assert repr(endpoint) == expected_repr
diff --git a/datacommons_client/tests/endpoints/test_error_handling.py b/datacommons_client/tests/endpoints/test_error_handling.py
new file mode 100644
index 00000000..89e841cc
--- /dev/null
+++ b/datacommons_client/tests/endpoints/test_error_handling.py
@@ -0,0 +1,71 @@
+from requests import Request
+from requests import Response
+
+from datacommons_client.utils.error_handling import APIError
+from datacommons_client.utils.error_handling import DataCommonsError
+from datacommons_client.utils.error_handling import DCAuthenticationError
+from datacommons_client.utils.error_handling import DCConnectionError
+from datacommons_client.utils.error_handling import DCStatusError
+from datacommons_client.utils.error_handling import InvalidDCInstanceError
+from datacommons_client.utils.error_handling import NoDataForPropertyError
+
+
+def test_data_commons_error_default_message():
+ """Tests that DataCommonsError uses the default message."""
+ error = DataCommonsError()
+ assert str(error) == DataCommonsError.default_message
+
+
+def test_data_commons_error_custom_message():
+ """Tests that DataCommonsError uses a custom message when provided."""
+ error = DataCommonsError("Custom message")
+ assert str(error) == "Custom message"
+
+
+def test_api_error_without_response():
+ """Tests APIError initialization without a Response object."""
+ error = APIError()
+ assert str(error) == f"\n{APIError.default_message}"
+
+
+def test_api_error_with_response():
+ """Tests APIError initialization with a mocked Response object.
+
+ Verifies that the string representation includes status code,
+ request URL, and response text.
+ """
+ mock_request = Request("GET", "http://example.com").prepare()
+ mock_response = Response()
+ mock_response.request = mock_request
+ mock_response.status_code = 404
+ mock_response._content = b"Not Found"
+
+ error = APIError(response=mock_response)
+ assert "Status Code: 404" in str(error)
+ assert "Request URL: http://example.com" in str(error)
+ assert "Not Found" in str(error)
+
+
+def test_subclass_default_messages():
+ """Tests that subclasses use their default messages."""
+ connection_error = DCConnectionError()
+ assert DCConnectionError.default_message in str(connection_error)
+
+ status_error = DCStatusError()
+ assert DCStatusError.default_message in str(status_error)
+
+ auth_error = DCAuthenticationError()
+ assert DCAuthenticationError.default_message in str(auth_error)
+
+ instance_error = InvalidDCInstanceError()
+ assert InvalidDCInstanceError.default_message in str(instance_error)
+
+ filter_error = NoDataForPropertyError()
+ assert NoDataForPropertyError.default_message in str(filter_error)
+
+
+def test_subclass_custom_message():
+ """Tests that subclasses use custom messages when provided."""
+ error = DCAuthenticationError(response=Response(),
+ message="Custom auth error")
+ assert str(error) == "\nCustom auth error"
diff --git a/datacommons_client/tests/endpoints/test_node_endpoint.py b/datacommons_client/tests/endpoints/test_node_endpoint.py
new file mode 100644
index 00000000..c0f006e7
--- /dev/null
+++ b/datacommons_client/tests/endpoints/test_node_endpoint.py
@@ -0,0 +1,556 @@
+from unittest.mock import MagicMock
+from unittest.mock import patch
+
+from datacommons_client.endpoints.base import API
+from datacommons_client.endpoints.node import NodeEndpoint
+from datacommons_client.endpoints.response import NodeResponse
+from datacommons_client.models.node import Arcs
+from datacommons_client.models.node import Name
+from datacommons_client.models.node import Node
+from datacommons_client.models.node import NodeGroup
+from datacommons_client.models.node import StatVarConstraints
+from datacommons_client.utils.names import DEFAULT_NAME_PROPERTY
+from datacommons_client.utils.names import NAME_WITH_LANGUAGE_PROPERTY
+
+
+def test_node_endpoint_initialization():
+ """Test if the NodeEndpoint initializes correctly."""
+ api_mock = MagicMock(spec=API)
+ endpoint = NodeEndpoint(api=api_mock)
+
+ assert endpoint.endpoint == "node"
+ assert endpoint.api == api_mock
+
+
+def test_node_endpoint_fetch():
+ """Test the fetch method of NodeEndpoint."""
+ api_mock = MagicMock(spec=API)
+ api_mock.post.return_value = {"data": {"test_node": {"properties": ["name"]}}}
+
+ endpoint = NodeEndpoint(api=api_mock)
+ response = endpoint.fetch(node_dcids="test_node", expression="name")
+
+ api_mock.post.assert_called_once_with(payload={
+ "nodes": ["test_node"],
+ "property": "name"
+ },
+ endpoint="node",
+ all_pages=True,
+ next_token=None)
+ assert isinstance(response, NodeResponse)
+ assert "test_node" in response.data
+
+
+def test_node_endpoint_fetch_property_labels():
+ """Test fetch_property_labels method."""
+ api_mock = MagicMock(spec=API)
+ endpoint = NodeEndpoint(api=api_mock)
+ endpoint.fetch = MagicMock(return_value=NodeResponse(
+ data={"node1": {
+ "properties": []
+ }}))
+
+ response = endpoint.fetch_property_labels(node_dcids="node1", out=False)
+ endpoint.fetch.assert_called_once_with(node_dcids="node1",
+ expression="<-",
+ all_pages=True,
+ next_token=None)
+ assert isinstance(response, NodeResponse)
+ assert "node1" in response.data
+
+
+def test_node_endpoint_fetch_property_values_out():
+ """Test fetch_property_values method with constraints and direction (out)"""
+
+ api_mock = MagicMock(spec=API)
+ api_mock.post.return_value = {"data": {"node1": {"properties": ["name"]}}}
+
+ endpoint = NodeEndpoint(api=api_mock)
+ response = endpoint.fetch_property_values(node_dcids="node1",
+ properties="name",
+ constraints="typeOf:City",
+ out=True)
+
+ expected_expression = "->name{typeOf:City}"
+ api_mock.post.assert_called_once_with(payload={
+ "nodes": ["node1"],
+ "property": expected_expression
+ },
+ endpoint="node",
+ all_pages=True,
+ next_token=None)
+ assert isinstance(response, NodeResponse)
+ assert "node1" in response.data
+
+
+def test_node_endpoint_fetch_property_values_in():
+ """Test fetch_property_values method with constraints and direction (in)"""
+
+ api_mock = MagicMock(spec=API)
+ api_mock.post.return_value = {"data": {"node1": {"properties": ["name"]}}}
+
+ endpoint = NodeEndpoint(api=api_mock)
+ response = endpoint.fetch_property_values(node_dcids="node1",
+ properties="name",
+ constraints="typeOf:City",
+ out=False)
+
+ expected_expression = "<-name{typeOf:City}"
+ api_mock.post.assert_called_once_with(payload={
+ "nodes": ["node1"],
+ "property": expected_expression
+ },
+ endpoint="node",
+ all_pages=True,
+ next_token=None)
+ assert isinstance(response, NodeResponse)
+ assert "node1" in response.data
+
+
+def test_node_endpoint_fetch_all_classes():
+ """Test fetch_all_classes method."""
+ api_mock = MagicMock(spec=API)
+ endpoint = NodeEndpoint(api=api_mock)
+ endpoint.fetch_property_values = MagicMock(return_value=NodeResponse(
+ data={"Class": {
+ "arcs": {}
+ }}))
+
+ response = endpoint.fetch_all_classes()
+ endpoint.fetch_property_values.assert_called_once_with(
+ node_dcids="Class",
+ properties="typeOf",
+ out=False,
+ all_pages=True,
+ next_token=None,
+ )
+ assert isinstance(response, NodeResponse)
+ assert "Class" in response.data
+
+
+def test_node_endpoint_fetch_property_values_string_vs_list():
+ """Test fetch_property_values with string and list expressions."""
+ api_mock = MagicMock(spec=API)
+ api_mock.post.return_value = {"data": {"node1": {"properties": ["name"]}}}
+
+ endpoint = NodeEndpoint(api=api_mock)
+
+ # String input
+ response = endpoint.fetch_property_values(node_dcids="node1",
+ properties="name",
+ constraints=None,
+ out=True)
+ api_mock.post.assert_called_with(payload={
+ "nodes": ["node1"],
+ "property": "->name"
+ },
+ endpoint="node",
+ all_pages=True,
+ next_token=None)
+
+ # List input
+ response = endpoint.fetch_property_values(node_dcids="node1",
+ properties=["name", "typeOf"],
+ constraints=None,
+ out=True)
+ api_mock.post.assert_called_with(payload={
+ "nodes": ["node1"],
+ "property": "->[name, typeOf]"
+ },
+ endpoint="node",
+ all_pages=True,
+ next_token=None)
+
+
+@patch(
+ "datacommons_client.endpoints.node.extract_name_from_english_name_property")
+def test_fetch_entity_names_english(mock_extract_name):
+ """Test fetching names in English (default behavior)."""
+ mock_extract_name.return_value = "Guatemala"
+ api_mock = MagicMock()
+ endpoint = NodeEndpoint(api=api_mock)
+
+ # Mock the response from fetch_property_values
+ endpoint.fetch_property_values = MagicMock(return_value=NodeResponse(
+ data={
+ 'dc/123':
+ Arcs(arcs={
+ DEFAULT_NAME_PROPERTY:
+ NodeGroup(nodes=[Node(value='Guatemala')])
+ })
+ }))
+
+ result = endpoint.fetch_entity_names("dc/123")
+ endpoint.fetch_property_values.assert_called_once_with(
+ node_dcids=["dc/123"], properties=DEFAULT_NAME_PROPERTY)
+ assert result == {
+ "dc/123":
+ Name(
+ value="Guatemala",
+ language="en",
+ property=DEFAULT_NAME_PROPERTY,
+ )
+ }
+
+ mock_extract_name.assert_called_once_with(
+ properties=[Node(value="Guatemala")])
+
+
+@patch(
+ "datacommons_client.endpoints.node.extract_name_from_property_with_language"
+)
+def test_fetch_entity_names_non_english(mock_extract_name):
+ """Test fetching names in a non-English language."""
+ mock_extract_name.return_value = ("Californie", "fr")
+ api_mock = MagicMock()
+ endpoint = NodeEndpoint(api=api_mock)
+
+ endpoint.fetch_property_values = MagicMock(return_value=NodeResponse(
+ data={
+ 'dc/123':
+ Arcs(
+ arcs={
+ NAME_WITH_LANGUAGE_PROPERTY:
+ NodeGroup(nodes=[Node(value='Californie')])
+ })
+ }))
+
+ result = endpoint.fetch_entity_names("dc/123", language="fr")
+ endpoint.fetch_property_values.assert_called_once_with(
+ node_dcids=["dc/123"], properties=NAME_WITH_LANGUAGE_PROPERTY)
+ assert result == {
+ "dc/123":
+ Name(
+ value="Californie",
+ language="fr",
+ property=NAME_WITH_LANGUAGE_PROPERTY,
+ )
+ }
+
+ mock_extract_name.assert_called_once_with(
+ properties=[Node(value='Californie')],
+ language='fr',
+ fallback_language=None)
+
+
+@patch(
+ "datacommons_client.endpoints.node.extract_name_from_property_with_language"
+)
+def test_fetch_entity_names_with_fallback(mock_extract_name_lang):
+ """Test fallback to another language when target language is unavailable."""
+ mock_extract_name_lang.return_value = ("Chiquimula", "en")
+ api_mock = MagicMock()
+ endpoint = NodeEndpoint(api=api_mock)
+
+ endpoint.fetch_property_values = MagicMock(return_value=NodeResponse(
+ data={
+ 'dc/123':
+ Arcs(
+ arcs={
+ NAME_WITH_LANGUAGE_PROPERTY:
+ NodeGroup(nodes=[Node(value='Chiquimula')])
+ })
+ }))
+
+ result = endpoint.fetch_entity_names("dc/123",
+ language="fr",
+ fallback_language="en")
+
+ assert result == {
+ "dc/123":
+ Name(
+ value="Chiquimula",
+ language="en",
+ property=NAME_WITH_LANGUAGE_PROPERTY,
+ )
+ }
+ mock_extract_name_lang.assert_called_once_with(
+ properties=[Node(value='Chiquimula')],
+ language='fr',
+ fallback_language='en')
+
+
+@patch(
+ "datacommons_client.endpoints.node.extract_name_from_property_with_language"
+)
+def test_fetch_entity_names_no_result(mock_extract_name_lang):
+ """Test case when no name is found."""
+ mock_extract_name_lang.return_value = (None, None)
+ api_mock = MagicMock()
+ endpoint = NodeEndpoint(api=api_mock)
+
+ endpoint.fetch_property_values = MagicMock(return_value=NodeResponse(
+ data={"dc/999": {
+ "properties": []
+ }}))
+
+ result = endpoint.fetch_entity_names("dc/999",
+ language="es",
+ fallback_language="en")
+ assert result == {}
+
+
+@patch("datacommons_client.endpoints.node.flatten_relationship")
+@patch("datacommons_client.endpoints.node.build_graph_map")
+@patch("datacommons_client.endpoints.node.fetch_relationship_lru")
+def test_fetch_entity_relationships_delegates_to_lru(mock_lru, mock_build_map,
+ mock_flatten):
+ """Ensure that the private helper builds a fetch‑function that ultimately
+ calls through to ``fetch_relationship_lru`` for each root DCID."""
+
+ mock_lru.return_value = [Node(dcid="B", name="B name", types=["Region"])]
+
+ def _fake_build_graph_map(root, fetch_fn):
+ # simulate the internal traversal by invoking the provided fetch_fn once
+ fetch_fn(dcid=root)
+ return root, {}
+
+ mock_build_map.side_effect = _fake_build_graph_map
+ mock_flatten.return_value = []
+
+ endpoint = NodeEndpoint(api=MagicMock())
+ result = endpoint._fetch_place_relationships(place_dcids="X",
+ as_tree=False,
+ contained_type="Region",
+ relationship="parents")
+
+ assert result == {"X": []}
+ mock_lru.assert_called_once_with(
+ endpoint,
+ dcid="X",
+ contained_type="Region",
+ relationship="parents",
+ )
+
+
+@patch("datacommons_client.endpoints.node.flatten_relationship")
+@patch("datacommons_client.endpoints.node.build_graph_map")
+def test_fetch_entity_ancestry_flat(mock_build_map, mock_flatten):
+ """Flat ancestry structure should be derived via ``flatten_relationship``."""
+ mock_build_map.return_value = (
+ "X",
+ {
+ "X": [Node(dcid="A", name="A name", types=["Country"])],
+ "A": [],
+ },
+ )
+ mock_flatten.return_value = [{
+ "dcid": "A",
+ "name": "A name",
+ "type": "Country"
+ }]
+
+ endpoint = NodeEndpoint(api=MagicMock())
+ result = endpoint.fetch_place_ancestors("X", as_tree=False)
+
+ assert result == {"X": [{"dcid": "A", "name": "A name", "type": "Country"}]}
+ mock_build_map.assert_called_once()
+ mock_flatten.assert_called_once()
+
+
+@patch("datacommons_client.endpoints.node.build_relationship_tree")
+@patch("datacommons_client.endpoints.node.build_graph_map")
+def test_fetch_entity_ancestry_tree(mock_build_map, mock_build_tree):
+ """Nested ancestry structure should be derived via
+ ``build_relationship_tree``."""
+ mock_build_map.return_value = (
+ "Y",
+ {
+ "Y": [Node(dcid="Z", name="Z name", types=["Region"])],
+ "Z": [],
+ },
+ )
+
+ mock_build_tree.return_value = {
+ "dcid":
+ "Y",
+ "name":
+ None,
+ "type":
+ None,
+ "parents": [{
+ "dcid": "Z",
+ "name": "Z name",
+ "type": "Region",
+ "parents": []
+ }],
+ }
+
+ endpoint = NodeEndpoint(api=MagicMock())
+ result = endpoint.fetch_place_ancestors("Y", as_tree=True)
+
+ assert "Y" in result
+ assert result["Y"]["dcid"] == "Y"
+ assert result["Y"]["parents"][0]["dcid"] == "Z"
+ mock_build_map.assert_called_once()
+ mock_build_tree.assert_called_once_with(root="Y",
+ graph=mock_build_map.return_value[1],
+ relationship_key="parents")
+
+
+def test__fetch_property_id_names_flattens_to_dcid_and_name():
+ """Private helper should return only dcid and name per target node."""
+ api_mock = MagicMock(spec=API)
+ endpoint = NodeEndpoint(api=api_mock)
+
+ # Simulate fetch_property_values response with Arcs, NodeGroup, Node
+ endpoint.fetch_property_values = MagicMock(return_value=NodeResponse(
+ data={
+ "sv/1":
+ Arcs(
+ arcs={
+ "constraintProperties":
+ NodeGroup(nodes=[
+ Node(dcid="p1", name="Prop One"),
+ Node(dcid="p2", name="Prop Two"),
+ ])
+ })
+ }))
+
+ result = endpoint._fetch_property_id_names("sv/1", "constraintProperties")
+
+ assert result == {
+ "sv/1": {
+ "constraintProperties": [
+ {
+ "dcid": "p1",
+ "name": "Prop One",
+ },
+ {
+ "dcid": "p2",
+ "name": "Prop Two",
+ },
+ ]
+ }
+ }
+ endpoint.fetch_property_values.assert_called_once_with(
+ node_dcids="sv/1", properties="constraintProperties")
+
+
+def test_fetch_statvar_constraints_builds_constraints_and_values():
+ """fetch_statvar_constraints should combine constraint properties and values."""
+ endpoint = NodeEndpoint(api=MagicMock())
+
+ # First call returns constraint property ids and names
+ constraints_map = {
+ "sv/1": {
+ "constraintProperties": [
+ {
+ "dcid": "p1",
+ "name": "Prop One"
+ },
+ {
+ "dcid": "p2",
+ "name": "Prop Two"
+ },
+ ]
+ }
+ }
+
+ # Second call returns values for those properties
+ values_map = {
+ "sv/1": {
+ "p1": [{
+ "dcid": "v1",
+ "name": "Val One"
+ }],
+ "p2": [{
+ "dcid": "v2",
+ "name": "Val Two"
+ }],
+ }
+ }
+
+ with patch.object(endpoint,
+ "_fetch_property_id_names",
+ side_effect=[constraints_map, values_map]) as mock_helper:
+ result = endpoint.fetch_statvar_constraints(["sv/1"])
+
+ # Ensure helper called twice (once for constraintProperties, once for values)
+ assert mock_helper.call_count == 2
+ assert isinstance(result, StatVarConstraints)
+ assert "sv/1" in result
+ # Two constraints returned
+ assert len(result["sv/1"]) == 2
+ ids = {(c.constraintId, c.valueId) for c in result["sv/1"]}
+ assert ids == {("p1", "v1"), ("p2", "v2")}
+
+
+def test_fetch_statvar_constraints_handles_string_input_and_no_constraints():
+ """Single sv input and empty constraints should yield empty list for that sv."""
+ endpoint = NodeEndpoint(api=MagicMock())
+
+ # No constraintProperties for sv/empty
+ constraints_map = {"sv/empty": {"constraintProperties": []}}
+ # Second call won't be used but provide empty map
+ values_map = {"sv/empty": {}}
+
+ with patch.object(endpoint,
+ "_fetch_property_id_names",
+ side_effect=[constraints_map, values_map]):
+ # string input
+ result = endpoint.fetch_statvar_constraints("sv/empty")
+
+ assert isinstance(result, StatVarConstraints)
+ assert result["sv/empty"] == []
+
+
+def test__fetch_property_id_names_handles_literal_values():
+ """_fetch_property_id_names should handle string literal values gracefully."""
+ api_mock = MagicMock(spec=API)
+ endpoint = NodeEndpoint(api=api_mock)
+
+ # Simulate a response where the target value is a literal string (no dcid)
+ endpoint.fetch_property_values = MagicMock(return_value=NodeResponse(
+ data={
+ "sv/1":
+ Arcs(arcs={"p1": NodeGroup(nodes=[Node(value="LiteralValue")])})
+ }))
+
+ result = endpoint._fetch_property_id_names("sv/1", "p1")
+
+ assert result == {
+ "sv/1": {
+ "p1": [{
+ "dcid": "LiteralValue",
+ "name": "LiteralValue"
+ }]
+ }
+ }
+ endpoint.fetch_property_values.assert_called_once_with(node_dcids="sv/1",
+ properties="p1")
+
+
+def test_fetch_statvar_constraints_skips_missing_constraint_values():
+ """If a constraintProperty has no value for a SV, skip it without error."""
+ endpoint = NodeEndpoint(api=MagicMock())
+
+ constraints_map = {
+ "sv/1": {
+ "constraintProperties": [
+ {
+ "dcid": "p1",
+ "name": "Prop One"
+ },
+ {
+ "dcid": "p2",
+ "name": "Prop Two"
+ },
+ ]
+ }
+ }
+
+ # p1 has a value, p2 is missing/empty
+ values_map = {"sv/1": {"p1": [{"dcid": "v1", "name": "Val One"}], "p2": []}}
+
+ with patch.object(endpoint,
+ "_fetch_property_id_names",
+ side_effect=[constraints_map, values_map]):
+ result = endpoint.fetch_statvar_constraints(["sv/1"])
+
+ assert isinstance(result, StatVarConstraints)
+ assert "sv/1" in result
+ # Only one well-formed constraint should be included (p1)
+ assert len(result["sv/1"]) == 1
+ assert result["sv/1"][0].constraintId == "p1"
+ assert result["sv/1"][0].valueId == "v1"
diff --git a/datacommons_client/tests/endpoints/test_observation_endpoint.py b/datacommons_client/tests/endpoints/test_observation_endpoint.py
new file mode 100644
index 00000000..da6387b8
--- /dev/null
+++ b/datacommons_client/tests/endpoints/test_observation_endpoint.py
@@ -0,0 +1,156 @@
+from unittest.mock import MagicMock
+
+from datacommons_client.endpoints.base import API
+from datacommons_client.endpoints.observation import ObservationEndpoint
+from datacommons_client.endpoints.response import ObservationResponse
+from datacommons_client.models.observation import ByVariable
+from datacommons_client.models.observation import ObservationDate
+from datacommons_client.models.observation import ObservationSelect
+
+
+def test_fetch():
+ """Tests the fetch method of ObservationEndpoint."""
+ api_mock = MagicMock(spec=API)
+ api_mock.post.return_value = {"byVariable": {}}
+ endpoint = ObservationEndpoint(api=api_mock)
+
+ response = endpoint.fetch(variable_dcids="dcid/variableID",
+ date=ObservationDate.LATEST,
+ select=["date", "variable", "entity", "value"],
+ entity_dcids="dc/EntityID",
+ filter_facet_domains="domain1",
+ filter_facet_ids="facet1")
+
+ # Check the response
+ assert isinstance(response, ObservationResponse)
+
+ # Check the post request
+ api_mock.post.assert_called_once_with(payload={
+ "date": ObservationDate.LATEST,
+ "variable": {
+ "dcids": ["dcid/variableID"]
+ },
+ "entity": {
+ "dcids": ["dc/EntityID"],
+ },
+ "select": ["date", "variable", "entity", "value"],
+ "filter": {
+ "domains": ["domain1"],
+ "facet_ids": ["facet1"]
+ }
+ },
+ endpoint="observation",
+ all_pages=True,
+ next_token=None)
+
+
+def test_fetch_observations_by_entity_type():
+ """Tests the fetch_observations_by_entity_type method."""
+ api_mock = MagicMock(spec=API)
+ api_mock.post.return_value = {"byVariable": {}}
+ endpoint = ObservationEndpoint(api=api_mock)
+
+ response = endpoint.fetch_observations_by_entity_type(
+ date="2023",
+ parent_entity="Earth",
+ entity_type="Country",
+ select=["variable", "entity", "facet"],
+ variable_dcids="dc/VariableID")
+
+ # Check the response
+ assert isinstance(response, ObservationResponse)
+
+ # Check the post request
+ api_mock.post.assert_called_once_with(payload={
+ "date": "2023",
+ "variable": {
+ "dcids": ["dc/VariableID"]
+ },
+ "entity": {
+ "expression": "Earth<-containedInPlace+{typeOf:Country}"
+ },
+ "select": ["variable", "entity", "facet"],
+ },
+ endpoint="observation",
+ all_pages=True,
+ next_token=None)
+
+
+def test_fetch_observations_facets_by_entity_type():
+ """Tests the fetch_observations_by_entity_type method."""
+ api_mock = MagicMock(spec=API)
+ api_mock.post.return_value = {"byVariable": {}}
+ endpoint = ObservationEndpoint(api=api_mock)
+
+ response = endpoint.fetch_observations_by_entity_type(
+ date="2023",
+ parent_entity="Earth",
+ entity_type="Country",
+ variable_dcids="dc/VariableID",
+ select=["variable", "entity", "facet"],
+ )
+
+ # Check the response
+ assert isinstance(response, ObservationResponse)
+
+ # Check the post request
+ api_mock.post.assert_called_once_with(payload={
+ "date": "2023",
+ "variable": {
+ "dcids": ["dc/VariableID"]
+ },
+ "entity": {
+ "expression": "Earth<-containedInPlace+{typeOf:Country}"
+ },
+ "select": ["variable", "entity", "facet"],
+ },
+ endpoint="observation",
+ all_pages=True,
+ next_token=None)
+
+
+def test_fetch_available_statistical_variables_single_entity():
+ """Test fetching variables for a single entity."""
+ mock_data = {
+ "var1": ["ent1"],
+ "var2": ["ent1"],
+ }
+
+ # Mock the fetch method on the ObservationEndpoint instance
+ endpoint = ObservationEndpoint(api=MagicMock())
+ endpoint.fetch = MagicMock()
+ endpoint.fetch.return_value.get_data_by_entity = MagicMock(
+ return_value=mock_data)
+
+ result = endpoint.fetch_available_statistical_variables("ent1")
+
+ expected = {
+ "ent1": ["var1", "var2"],
+ }
+ assert result == expected
+
+ endpoint.fetch.assert_called_once_with(
+ entity_dcids="ent1",
+ select=[ObservationSelect.VARIABLE, ObservationSelect.ENTITY],
+ variable_dcids=[])
+
+
+def test_fetch_available_statistical_variables_multiple_entities():
+ """Test fetching variables for multiple entities."""
+ mock_data = {
+ "var1": ["ent1", "ent2"],
+ "var2": ["ent2"],
+ }
+
+ endpoint = ObservationEndpoint(api=MagicMock())
+ endpoint.fetch = MagicMock()
+ endpoint.fetch.return_value.get_data_by_entity = MagicMock(
+ return_value=mock_data)
+
+ result = endpoint.fetch_available_statistical_variables(["ent1", "ent2"])
+
+ expected = {
+ "ent1": ["var1"],
+ "ent2": ["var1", "var2"],
+ }
+ assert result == expected
diff --git a/datacommons_client/tests/endpoints/test_payloads.py b/datacommons_client/tests/endpoints/test_payloads.py
new file mode 100644
index 00000000..28c7f1e1
--- /dev/null
+++ b/datacommons_client/tests/endpoints/test_payloads.py
@@ -0,0 +1,147 @@
+import pytest
+
+from datacommons_client.endpoints.payloads import NodeRequestPayload
+from datacommons_client.endpoints.payloads import ObservationRequestPayload
+from datacommons_client.endpoints.payloads import ResolveRequestPayload
+from datacommons_client.models.observation import ObservationDate
+from datacommons_client.models.observation import ObservationSelect
+from datacommons_client.utils.error_handling import InvalidObservationSelectError
+
+
+def test_node_payload_normalize():
+ """Tests that NodeRequestPayload correctly normalizes single and multiple node_dcids."""
+ payload = NodeRequestPayload(node_dcids="node1", expression="prop1")
+ assert payload.node_dcids == ["node1"]
+
+ payload = NodeRequestPayload(node_dcids=["node1", "node2"],
+ expression="prop1")
+ assert payload.node_dcids == ["node1", "node2"]
+
+
+def test_node_payload_validate():
+ """Tests that NodeRequestPayload validates its inputs correctly."""
+ with pytest.raises(ValueError):
+ NodeRequestPayload(node_dcids="node1",
+ expression=123) # `expression` must be a string
+
+
+def test_node_payload_to_dict():
+ """Tests NodeRequestPayload conversion to dictionary."""
+ payload = NodeRequestPayload(node_dcids="node1", expression="prop1")
+ assert payload.to_dict() == {"nodes": ["node1"], "property": "prop1"}
+
+
+def test_observation_payload_normalize():
+ """Tests that ObservationRequestPayload normalizes inputs correctly."""
+ payload = ObservationRequestPayload(
+ date="LATEST",
+ variable_dcids="var1",
+ select=["variable", "entity"],
+ entity_dcids="ent1",
+ filter_facet_domains="domain1",
+ filter_facet_ids="facets1",
+ )
+ assert payload.variable_dcids == ["var1"]
+ assert payload.entity_dcids == ["ent1"]
+ assert payload.filter_facet_domains == ["domain1"]
+ assert payload.filter_facet_ids == ["facets1"]
+ assert payload.date == ObservationDate.LATEST
+
+ assert "filter" in payload.to_dict()
+ assert "facet_ids" in payload.to_dict()["filter"]
+ assert "domains" in payload.to_dict()["filter"]
+
+ # Check that when domain and facets are not included, they are not in the payload
+ payload = ObservationRequestPayload(
+ date="all",
+ variable_dcids=["var1"],
+ select=["variable", "entity"],
+ entity_dcids=["ent1"],
+ )
+ assert payload.date == ObservationDate.ALL
+ assert payload.variable_dcids == ["var1"]
+ assert payload.entity_dcids == ["ent1"]
+ assert "filter" not in payload.to_dict()
+
+
+def test_observation_select_invalid_value():
+ """Tests that an invalid ObservationSelect value raises InvalidObservationSelectError."""
+ with pytest.raises(InvalidObservationSelectError):
+ ObservationSelect("invalid")
+
+
+def test_observation_payload_validate():
+ """Tests that ObservationRequestPayload validates its inputs."""
+ with pytest.raises(InvalidObservationSelectError):
+ ObservationRequestPayload(
+ date="LATEST",
+ variable_dcids="var1",
+ select=["variable"],
+ entity_dcids=None,
+ entity_expression=None,
+ ) # Requires either `entity_dcids` or `entity_expression`
+
+ with pytest.raises(InvalidObservationSelectError):
+ ObservationRequestPayload(
+ date="LATEST",
+ variable_dcids="var1",
+ select=["value"], # Missing required "variable" and "entity"
+ entity_expression="expression",
+ )
+
+ with pytest.raises(ValueError):
+ ObservationRequestPayload(
+ date="LATEST",
+ variable_dcids="var1",
+ select=["variable", "entity"],
+ entity_dcids="ent1",
+ entity_expression=
+ "expression", # Both `entity_dcids` and `entity_expression` set
+ )
+
+
+def test_observation_payload_to_dict():
+ """Tests ObservationRequestPayload conversion to dictionary."""
+ payload = ObservationRequestPayload(
+ date="LATEST",
+ variable_dcids="var1",
+ select=["variable", "entity"],
+ entity_dcids="ent1",
+ filter_facet_ids="facets1",
+ )
+ assert payload.to_dict() == {
+ "date": ObservationDate.LATEST,
+ "variable": {
+ "dcids": ["var1"]
+ },
+ "entity": {
+ "dcids": ["ent1"]
+ },
+ "select": ["variable", "entity"],
+ "filter": {
+ "facet_ids": ["facets1"]
+ }
+ }
+
+
+def test_resolve_payload_normalize():
+ """Tests that ResolveRequestPayload normalizes single and multiple node_dcids."""
+ payload = ResolveRequestPayload(node_dcids="node1", expression="expr1")
+ assert payload.node_dcids == ["node1"]
+
+ payload = ResolveRequestPayload(node_dcids=["node1", "node2"],
+ expression="expr1")
+ assert payload.node_dcids == ["node1", "node2"]
+
+
+def test_resolve_payload_validate():
+ """Tests that ResolveRequestPayload validates its inputs correctly."""
+ with pytest.raises(ValueError):
+ ResolveRequestPayload(node_dcids="node1",
+ expression=123) # `expression` must be a string
+
+
+def test_resolve_payload_to_dict():
+ """Tests ResolveRequestPayload conversion to dictionary."""
+ payload = ResolveRequestPayload(node_dcids="node1", expression="expr1")
+ assert payload.to_dict() == {"nodes": ["node1"], "property": "expr1"}
diff --git a/datacommons_client/tests/endpoints/test_request_handling.py b/datacommons_client/tests/endpoints/test_request_handling.py
new file mode 100644
index 00000000..8579db6b
--- /dev/null
+++ b/datacommons_client/tests/endpoints/test_request_handling.py
@@ -0,0 +1,421 @@
+from unittest.mock import MagicMock
+from unittest.mock import patch
+
+import pytest
+import requests
+
+from datacommons_client.utils.error_handling import APIError
+from datacommons_client.utils.error_handling import DCAuthenticationError
+from datacommons_client.utils.error_handling import DCConnectionError
+from datacommons_client.utils.error_handling import DCStatusError
+from datacommons_client.utils.error_handling import InvalidDCInstanceError
+from datacommons_client.utils.request_handling import _fetch_with_pagination
+from datacommons_client.utils.request_handling import _merge_values
+from datacommons_client.utils.request_handling import _recursively_merge_dicts
+from datacommons_client.utils.request_handling import _send_post_request
+from datacommons_client.utils.request_handling import check_instance_is_valid
+from datacommons_client.utils.request_handling import post_request
+from datacommons_client.utils.request_handling import resolve_instance_url
+
+
+def test_resolve_instance_url_default():
+ """Tests resolving the default Data Commons instance."""
+ assert (resolve_instance_url("datacommons.org") ==
+ "https://api.datacommons.org/v2")
+
+
+@patch("requests.get")
+def test_check_instance_is_valid_request_exception(mock_get):
+ """Tests that a RequestException raises InvalidDCInstanceError."""
+ mock_get.side_effect = requests.exceptions.RequestException("Request failed")
+ with pytest.raises(InvalidDCInstanceError):
+ check_instance_is_valid("https://invalid-instance")
+
+
+@patch("requests.post")
+def test_send_post_request_connection_error(mock_post):
+ """Tests that a ConnectionError raises DCConnectionError."""
+ mock_post.side_effect = requests.exceptions.ConnectionError(
+ "Connection failed")
+ with pytest.raises(DCConnectionError):
+ _send_post_request("https://api.test.com", {}, {})
+
+
+@patch("datacommons_client.utils.request_handling.check_instance_is_valid")
+def test_resolve_instance_url_custom(mock_check_instance_is_valid):
+ """Tests resolving a custom Data Commons instance."""
+ mock_check_instance_is_valid.return_value = (
+ "https://custom-instance/core/api/v2")
+
+ assert (resolve_instance_url("custom-instance") ==
+ "https://custom-instance/core/api/v2")
+ mock_check_instance_is_valid.assert_called_once_with(
+ "https://custom-instance/core/api/v2")
+
+
+@patch("requests.get")
+def test_check_instance_is_valid_valid(mock_get):
+ """Tests that a valid instance URL is correctly validated."""
+
+ # Create a mock response object with the expected JSON data and status code
+ mock_response = MagicMock()
+ mock_response.json.return_value = {"data": {"country/GTM": {}}}
+ mock_response.status_code = 200
+ mock_get.return_value = mock_response
+
+ # Mock the instance URL to test
+ instance_url = "https://valid-instance"
+
+ # Assert that the instance URL is returned if it is valid
+ assert check_instance_is_valid(instance_url) == instance_url
+ mock_get.assert_called_once_with(
+ f"{instance_url}/node?nodes=country%2FGTM&property=->name", headers={})
+
+
+@patch("requests.get")
+def test_check_instance_is_valid_with_key_valid(mock_get):
+ """Tests that a valid instance URL is correctly validated."""
+
+ # Create a mock response object with the expected JSON data and status code
+ mock_response = MagicMock()
+ mock_response.json.return_value = {"data": {"country/GTM": {}}}
+ mock_response.status_code = 200
+ mock_get.return_value = mock_response
+
+ # Mock the instance URL to test
+ instance_url = "https://valid-instance"
+ api_key = "test-api-key"
+ # Assert that the instance URL is returned if it is valid
+ assert check_instance_is_valid(instance_url, api_key=api_key) == instance_url
+ mock_get.assert_called_once_with(
+ f"{instance_url}/node?nodes=country%2FGTM&property=->name",
+ headers={"X-API-Key": api_key})
+
+
+@patch("requests.get")
+def test_check_instance_is_valid_invalid(mock_get):
+ """Tests that an invalid instance URL raises the appropriate exception."""
+ mock_response = MagicMock()
+ mock_response.json.return_value = {"error": "Not Found"}
+ mock_response.status_code = 404
+ mock_get.return_value = mock_response
+
+ with pytest.raises(InvalidDCInstanceError):
+ check_instance_is_valid("https://invalid-instance")
+
+
+@patch("requests.post")
+def test_send_post_request_500_status_error(mock_post):
+ """Tests that a 500-level HTTP error raises DCStatusError."""
+ mock_response = MagicMock()
+ mock_response.status_code = 500
+ mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError(
+ response=mock_response)
+ mock_post.return_value = mock_response
+
+ with pytest.raises(DCStatusError):
+ _send_post_request("https://api.test.com", {}, {})
+
+
+@patch("requests.post")
+def test_send_post_request_other_http_error(mock_post):
+ """Tests that non-500 HTTP errors raise APIError."""
+ mock_response = MagicMock()
+ mock_response.status_code = 404
+ mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError(
+ response=mock_response)
+ mock_post.return_value = mock_response
+
+ with pytest.raises(APIError):
+ _send_post_request("https://api.test.com", {}, {})
+
+
+@patch("requests.post")
+def test_send_post_request_success(mock_post):
+ """Tests a successful POST request."""
+
+ # Create a mock response object with the expected JSON data and status code
+ mock_response = MagicMock()
+ mock_response.status_code = 200
+ mock_response.json.return_value = {"success": True}
+ mock_post.return_value = mock_response
+
+ # Mock the POST request
+ url = "https://api.test.com"
+ payload = {"key": "value"}
+ headers = {"Content-Type": "application/json"}
+
+ response = _send_post_request(url, payload, headers)
+ assert response.status_code == 200
+ assert response.json() == {"success": True}
+
+
+@patch("requests.post")
+def test_send_post_request_surface_header(mock_post):
+ """Tests a successful POST request with a surface header."""
+
+ mock_response = MagicMock()
+ mock_response.status_code = 200
+ mock_response.json.return_value = {"success": True}
+ mock_post.return_value = mock_response
+
+ # Mock the POST request
+ url = "https://api.test.com"
+ payload = {"key": "value"}
+ headers = {"Content-Type": "application/json", "x-surface": "mcp-1.0"}
+
+ _send_post_request(url, payload, headers)
+ mock_post.assert_called_once_with(url, json=payload, headers=headers)
+
+
+@patch("requests.post")
+def test_send_post_request_http_error(mock_post):
+ """Tests handling an HTTP error during a POST request."""
+ # Create a mock response object with a 401 status code
+ mock_response = MagicMock()
+ mock_response.status_code = 401
+ mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError(
+ response=mock_response)
+ mock_post.return_value = mock_response
+
+ # Mock the POST request and assert that a DCAuthenticationError is raised
+ with pytest.raises(DCAuthenticationError):
+ _send_post_request("https://api.test.com", {}, {})
+
+
+def test_recursively_merge_dicts():
+ """Tests recursive merging of dictionaries."""
+ # Test merging two dictionaries with nested dictionaries and lists
+ base = {"a": {"b": 1}, "c": [1, 2]}
+ new = {"a": {"d": 2}, "c": [3], "e": 5}
+ result = _recursively_merge_dicts(base, new)
+
+ # Assert that the dictionaries are merged correctly
+ assert result == {"a": {"b": 1, "d": 2}, "c": [1, 2, 3], "e": 5}
+
+
+def test_merge_values_dicts():
+ """Tests merging of dictionary values."""
+ base = {"a": 1}
+ new = {"b": 2}
+ result = _merge_values(base, new)
+ assert result == {"a": 1, "b": 2}
+
+
+def test_merge_values_lists():
+ """Tests merging of list values."""
+ base = [1, 2]
+ new = [3, 4]
+ result = _merge_values(base, new)
+ assert result == [1, 2, 3, 4]
+
+
+def test_merge_values_other():
+ """Tests merging non-dict, non-list values."""
+ base = "value1"
+ new = "value2"
+ result = _merge_values(base, new)
+ assert result == ["value1", "value2"]
+
+
+def test_merge_values_complex_conflict():
+ """Tests merging deeply nested, repeated objects."""
+
+ # Nested but simple base
+ base = {
+ "key1": {
+ "nested1": {
+ "subkey1": "value1",
+ "subkey2": [1, 2],
+ },
+ "nested2": "value2",
+ },
+ "key2": [1, 2, 3],
+ "key3": "conflict1",
+ }
+
+ # Nested but complex new
+ new = {
+ "key1": {
+ "nested1": {
+ "subkey1": "new_value1", # Already in base
+ "subkey3": "new_value2", # New key
+ },
+ "nested2": ["new_value3"], # Conflicts with base (type change)
+ },
+ "key2": [4, 5], # Should merge lists
+ "key3": "conflict2", # Should create a list due to conflict
+ "key4": {
+ "new_nested": "new_value4"
+ }, # New key
+ }
+
+ # Expected result which keeps all data
+ expected_result = {
+ "key1": {
+ "nested1": {
+ "subkey1": ["value1", "new_value1"],
+ "subkey2": [1, 2],
+ "subkey3": "new_value2",
+ },
+ "nested2": ["value2", ["new_value3"]],
+ },
+ "key2": [1, 2, 3, 4, 5],
+ "key3": ["conflict1", "conflict2"],
+ "key4": {
+ "new_nested": "new_value4"
+ },
+ }
+
+ result = _merge_values(base, new)
+ assert result == expected_result
+
+
+@patch("datacommons_client.utils.request_handling._send_post_request")
+def test_fetch_with_pagination(mock_send_post_request):
+ """Tests fetching and merging paginated API responses."""
+
+ # Mock the response JSON data for two pages
+ mock_response = MagicMock()
+ mock_response.json.side_effect = [
+ {
+ "data": {
+ "page1": True
+ },
+ "nextToken": "token1"
+ },
+ {
+ "data": {
+ "page2": True
+ }
+ },
+ ]
+ mock_send_post_request.side_effect = [mock_response, mock_response]
+
+ # Mock the POST request and assert that the results are merged correctly
+ url = "https://api.test.com"
+ payload = {}
+ headers = {}
+
+ result = _fetch_with_pagination(url, payload, headers)
+ assert result == {"data": {"page1": True, "page2": True}, 'nextToken': None}
+
+
+@patch("datacommons_client.utils.request_handling._send_post_request")
+def test_fetch_with_pagination_next_token(mock_send_post_request):
+ """Tests fetching and merging paginated API responses, including a next_token"""
+
+ # Mock the response JSON data for two pages
+ mock_response = MagicMock()
+
+ # Should stop at first page given that all_pages is set to False
+ mock_response.json.side_effect = [
+ {
+ "data": {
+ "page1": True
+ },
+ "nextToken": "token2"
+ },
+ ]
+ mock_send_post_request.side_effect = [mock_response, mock_response]
+
+ # Mock the POST request and assert that the results are merged correctly
+ url = "https://api.test.com"
+ payload = {}
+ headers = {}
+
+ result = _fetch_with_pagination(url,
+ payload,
+ headers,
+ all_pages=False,
+ next_token="token1")
+ assert result == {"data": {"page1": True}, 'nextToken': "token2"}
+
+
+@patch("datacommons_client.utils.request_handling._send_post_request")
+def test_fetch_with_pagination_next_token_all_pages(mock_send_post_request):
+ """Tests fetching and merging paginated API responses, including a next_token"""
+
+ # Mock the response JSON data for two pages
+ mock_response = MagicMock()
+
+ mock_response.json.side_effect = [
+ {
+ "data": {
+ "page1": True
+ },
+ "nextToken": "token1"
+ },
+ {
+ "data": {
+ "page2": True
+ },
+ "nextToken": "token2",
+ },
+ {
+ "data": {
+ "page3": True
+ },
+ "nextToken": None,
+ },
+ ]
+ mock_send_post_request.side_effect = [
+ mock_response, mock_response, mock_response
+ ]
+
+ # Mock the POST request and assert that the results are merged correctly
+ url = "https://api.test.com"
+ payload = {}
+ headers = {}
+
+ result = _fetch_with_pagination(url,
+ payload,
+ headers,
+ all_pages=True,
+ next_token="token1")
+ assert result == {
+ "data": {
+ "page1": True,
+ "page2": True,
+ "page3": True
+ },
+ 'nextToken': None
+ }
+
+
+@patch("datacommons_client.utils.request_handling._send_post_request")
+def test_fetch_with_pagination_invalid_json(mock_send_post_request):
+ """Tests that invalid JSON response raises APIError."""
+ mock_response = MagicMock()
+ mock_response.json.side_effect = ValueError("Invalid JSON")
+ mock_send_post_request.return_value = mock_response
+
+ with pytest.raises(APIError):
+ _fetch_with_pagination("https://api.test.com", {}, {})
+
+
+@patch("datacommons_client.utils.request_handling._fetch_with_pagination")
+def test_post_request(mock_fetch_with_pagination):
+ """Tests the `post_request` function with mock pagination."""
+ mock_fetch_with_pagination.return_value = {"result": "data"}
+ result = post_request("https://api.test.com", {}, {})
+ assert result == {"result": "data"}
+
+
+@patch("datacommons_client.utils.request_handling._fetch_with_pagination")
+def test_post_request_next_token(mock_fetch_with_pagination):
+ """Tests the `post_request` function including a check for next_token"""
+ mock_fetch_with_pagination.return_value = {"result": "data"}
+ result = post_request("https://api.test.com", {}, {}, next_token="token123")
+
+ mock_fetch_with_pagination.assert_called_once_with(url="https://api.test.com",
+ payload={},
+ headers={},
+ all_pages=True,
+ next_token="token123")
+
+
+def test_post_request_invalid_payload():
+ """Tests that a non-dictionary payload raises a ValueError."""
+ with pytest.raises(ValueError):
+ post_request("https://api.test.com", ["not", "a", "dict"], {})
diff --git a/datacommons_client/tests/endpoints/test_resolve_endpoint.py b/datacommons_client/tests/endpoints/test_resolve_endpoint.py
new file mode 100644
index 00000000..ea337bbb
--- /dev/null
+++ b/datacommons_client/tests/endpoints/test_resolve_endpoint.py
@@ -0,0 +1,227 @@
+from unittest.mock import MagicMock
+
+from datacommons_client.endpoints.base import API
+from datacommons_client.endpoints.resolve import _resolve_correspondence_expression
+from datacommons_client.endpoints.resolve import ResolveEndpoint
+from datacommons_client.endpoints.response import ResolveResponse
+from datacommons_client.models.resolve import Candidate
+from datacommons_client.models.resolve import Entity
+
+
+def test_fetch():
+ """Tests the fetch method of ResolveEndpoint."""
+ api_mock = MagicMock(spec=API)
+ api_mock.post = MagicMock(return_value={})
+ endpoint = ResolveEndpoint(api=api_mock)
+
+ response = endpoint.fetch(node_ids="Node1", expression="some_expression")
+
+ # Check the response
+ assert isinstance(response, ResolveResponse)
+
+ # Check the post request
+ api_mock.post.assert_called_once_with(payload={
+ "nodes": ["Node1"],
+ "property": "some_expression",
+ },
+ endpoint="resolve",
+ all_pages=True,
+ next_token=None)
+
+
+def test_fetch_dcid_by_name():
+ """Tests the fetch_dcid_by_name method."""
+ api_mock = MagicMock(spec=API)
+ api_mock.post = MagicMock(return_value={})
+ endpoint = ResolveEndpoint(api=api_mock)
+
+ response = endpoint.fetch_dcids_by_name(names=["Entity1"],
+ entity_type="Place")
+
+ # Check the response
+ assert isinstance(response, ResolveResponse)
+
+ # Check the post request
+ api_mock.post.assert_called_once_with(payload={
+ "nodes": ["Entity1"],
+ "property": "<-description{typeOf:Place}->dcid"
+ },
+ endpoint="resolve",
+ all_pages=True,
+ next_token=None)
+
+
+def test_fetch_dcid_by_wikidata_id():
+ """Tests the fetch_dcid_by_wikidata_id method."""
+ api_mock = MagicMock(spec=API)
+ api_mock.post = MagicMock(return_value={})
+ endpoint = ResolveEndpoint(api=api_mock)
+
+ response = endpoint.fetch_dcids_by_wikidata_id(wikidata_ids="Q12345",
+ entity_type="Country")
+
+ # Check the response
+ assert isinstance(response, ResolveResponse)
+
+ # Check the post request
+ api_mock.post.assert_called_once_with(payload={
+ "nodes": ["Q12345"],
+ "property": "<-wikidataId{typeOf:Country}->dcid",
+ },
+ endpoint="resolve",
+ all_pages=True,
+ next_token=None)
+
+
+def test_fetch_dcids_list_by_wikidata_id():
+ """Tests the fetch_dcid_by_wikidata_id method."""
+ api_mock = MagicMock(spec=API)
+ api_mock.post = MagicMock(return_value={})
+ endpoint = ResolveEndpoint(api=api_mock)
+
+ response = endpoint.fetch_dcids_by_wikidata_id(
+ wikidata_ids=["Q12345", "Q695660"])
+
+ # Check the response
+ assert isinstance(response, ResolveResponse)
+
+ # Check the post request
+ api_mock.post.assert_called_once_with(payload={
+ "nodes": ["Q12345", "Q695660"],
+ "property": "<-wikidataId->dcid",
+ },
+ endpoint="resolve",
+ all_pages=True,
+ next_token=None)
+
+
+def test_fetch_dcid_by_coordinates():
+ """Tests the fetch_dcid_by_coordinates method."""
+ api_mock = MagicMock(spec=API)
+ api_mock.post = MagicMock(return_value={})
+ endpoint = ResolveEndpoint(api=api_mock)
+
+ response = endpoint.fetch_dcid_by_coordinates(latitude="37.7749",
+ longitude="-122.4194",
+ entity_type="City")
+
+ # Check the response
+ assert isinstance(response, ResolveResponse)
+
+ # Check the post request
+ api_mock.post.assert_called_once_with(payload={
+ "nodes": ["37.7749#-122.4194"],
+ "property": "<-geoCoordinate{typeOf:City}->dcid",
+ },
+ endpoint="resolve",
+ all_pages=True,
+ next_token=None)
+
+
+def test_resolve_correspondence_expression():
+ """Tests the resolve_correspondence_expression function."""
+ expression = _resolve_correspondence_expression(from_type="description",
+ to_type="dcid",
+ entity_type="Place")
+ assert expression == "<-description{typeOf:Place}->dcid"
+
+ expression_no_entity_type = _resolve_correspondence_expression(
+ from_type="description", to_type="dcid")
+ assert expression_no_entity_type == "<-description->dcid"
+
+
+def test_flatten_resolve_response():
+ """Tests the flatten_resolve_response function."""
+ # Mock ResolveResponse with multiple entities
+ mock_data = ResolveResponse(entities=[
+ Entity(node="Node1", candidates=[Candidate(dcid="Candidate1")]),
+ Entity(node="Node2",
+ candidates=[
+ Candidate(dcid="Candidate2"),
+ Candidate(dcid="Candidate3")
+ ]),
+ Entity(node="Node3", candidates=[]) # No candidates
+ ])
+
+ # Call the function
+ result = mock_data.to_flat_dict()
+
+ # Expected output
+ expected = {
+ "Node1": "Candidate1", # Single candidate
+ "Node2": ["Candidate2", "Candidate3"], # Multiple candidates
+ "Node3": [], # No candidates
+ }
+
+ # Assertions
+ assert result == expected
+
+
+def test_fetch_indicators_calls_endpoints_correctly():
+ """Tests the fetch_indicators method."""
+ api_mock = MagicMock()
+ # Mock response data structure
+ mock_response_data = {
+ "entities": [{
+ "node":
+ "population",
+ "candidates": [{
+ "dcid": "Count_Person",
+ "dominantType": "StatisticalVariable",
+ "metadata": {
+ "score": "0.9",
+ "sentence": "population count"
+ },
+ "typeOf": ["StatisticalVariable"]
+ }]
+ }]
+ }
+ api_mock.post = MagicMock(return_value=mock_response_data)
+ endpoint = ResolveEndpoint(api=api_mock)
+
+ # Call the method
+ response = endpoint.fetch_indicators(queries=["population"],
+ target="custom_only")
+
+ # Verify post was called with correct payload
+ api_mock.post.assert_called_once_with(payload={
+ "nodes": ["population"],
+ "resolver": "indicator",
+ "target": "custom_only"
+ },
+ endpoint="resolve",
+ all_pages=True,
+ next_token=None)
+
+ # Verify response parsing
+ expected = ResolveResponse(entities=[
+ Entity(node="population",
+ candidates=[
+ Candidate(dcid="Count_Person",
+ dominantType="StatisticalVariable",
+ metadata={
+ "score": "0.9",
+ "sentence": "population count"
+ },
+ typeOf=["StatisticalVariable"])
+ ])
+ ])
+ assert response == expected
+
+
+def test_fetch_still_works_with_expression():
+ """Tests that fetch still works with expression (regression test)."""
+ api_mock = MagicMock()
+ mock_response_data = {"entities": []}
+ api_mock.post = MagicMock(return_value=mock_response_data)
+ endpoint = ResolveEndpoint(api=api_mock)
+
+ endpoint.fetch(node_ids=["geoId/06"], expression="<-containedInPlace")
+
+ api_mock.post.assert_called_once_with(payload={
+ "nodes": ["geoId/06"],
+ "property": "<-containedInPlace"
+ },
+ endpoint="resolve",
+ all_pages=True,
+ next_token=None)
diff --git a/datacommons_client/tests/endpoints/test_response.py b/datacommons_client/tests/endpoints/test_response.py
new file mode 100644
index 00000000..9faa1d88
--- /dev/null
+++ b/datacommons_client/tests/endpoints/test_response.py
@@ -0,0 +1,1173 @@
+import json
+
+from datacommons_client.endpoints.response import NodeResponse
+from datacommons_client.endpoints.response import ObservationResponse
+from datacommons_client.endpoints.response import ResolveResponse
+from datacommons_client.models.node import Node
+from datacommons_client.models.node import NodeGroup
+from datacommons_client.models.observation import ByVariable
+from datacommons_client.models.observation import Facet
+from datacommons_client.models.observation import Observation
+from datacommons_client.models.observation import OrderedFacet
+from datacommons_client.models.observation import OrderedFacets
+from datacommons_client.models.observation import Variable
+from datacommons_client.utils.data_processing import extract_observations
+from datacommons_client.utils.data_processing import flatten_properties
+from datacommons_client.utils.data_processing import unpack_arcs
+
+### ----- Test Node Response ----- ###
+
+
+def test_node_response_model_validation():
+ """Test that NodeResponse.model_validate correctly parses JSON data."""
+
+ # Mocking JSON data
+ json_data = {
+ "data": {
+ "geoId/06": {
+ "properties": [
+ "affectedPlace",
+ "containedInPlace",
+ "location",
+ "member",
+ "overlapsWith",
+ ]
+ }
+ },
+ "nextToken": "token123",
+ }
+
+ response = NodeResponse.model_validate(json_data)
+
+ assert response.nextToken == "token123"
+ assert response.data["geoId/06"].properties == [
+ "affectedPlace",
+ "containedInPlace",
+ "location",
+ "member",
+ "overlapsWith",
+ ]
+ assert "geoId/06" in response.data
+
+
+def test_node_as_dict():
+ """Test that the NodeResponse.json property returns the correct dictionary."""
+ json_data = {
+ "data": {
+ "geoId/06": {
+ "properties": [
+ "affectedPlace",
+ "containedInPlace",
+ "location",
+ "member",
+ "overlapsWith",
+ ]
+ }
+ },
+ "nextToken": "token123",
+ }
+
+ response = NodeResponse.model_validate(json_data)
+ result = response.to_dict()
+
+ assert result == json_data
+
+
+def test_node_as_dict_exclude_none():
+ """Test that the NodeResponse.json property returns the correct dictionary."""
+ json_data = {
+ "data": {
+ "geoId/06": {
+ "properties": None
+ }
+ },
+ "nextToken": "token123",
+ }
+
+ expected = {
+ "data": {
+ "geoId/06": {}
+ },
+ "nextToken": "token123",
+ }
+
+ response = NodeResponse.model_validate(json_data)
+ result = response.to_dict(exclude_none=True)
+
+ assert result == expected
+
+
+def test_node_as_dict_include_none():
+ """Test that the NodeResponse.json property returns the correct dictionary."""
+ json_data = {
+ "data": {
+ "geoId/06": {
+ "properties": None
+ }
+ },
+ "nextToken": "token123",
+ }
+
+ response = NodeResponse.model_validate(json_data)
+ result = response.to_dict(exclude_none=False)
+
+ assert result == json_data
+
+
+def test_flatten_properties():
+ """Test that the flatten_properties function correctly flattens the properties."""
+
+ # Mocking node data
+ json_data = {
+ "data": {
+ "geoId/06": {
+ "properties": [
+ "affectedPlace",
+ "containedInPlace",
+ "location",
+ "member",
+ "overlapsWith",
+ ]
+ }
+ }
+ }
+
+ response = NodeResponse.model_validate(json_data)
+ result = flatten_properties(response.data)
+ function_result = response.get_properties()
+
+ assert result == function_result
+ assert "geoId/06" in result
+ assert result["geoId/06"] == [
+ "affectedPlace",
+ "containedInPlace",
+ "location",
+ "member",
+ "overlapsWith",
+ ]
+
+
+def test_flatten_arcs():
+ """Test that the flatten_properties function correctly flattens the arcs."""
+ json_data = {
+ "data": {
+ "dc/03lw9rhpendw5": {
+ "arcs": {
+ "name": {
+ "nodes": [{
+ "provenanceId": "dc/base/EIA_860",
+ "value": "191 Peachtree Tower",
+ }]
+ }
+ }
+ }
+ }
+ }
+ response = NodeResponse.model_validate(json_data)
+ result = flatten_properties(response.data)
+
+ assert "dc/03lw9rhpendw5" in result
+ assert result["dc/03lw9rhpendw5"] == {
+ "name": [
+ Node(value="191 Peachtree Tower", provenanceId="dc/base/EIA_860")
+ ]
+ }
+
+
+def test_flatten_multiple_arcs_with_multiple_nodes():
+ """Test that the flatten_properties function correctly flattens the
+ NodeResponse containing multiple property arcs and mutliple nodes within the
+ arcs."""
+
+ # Mocking node data
+ json_data = {
+ "data": {
+ "geoId/06": {
+ "arcs": {
+ "containedInPlace": {
+ "nodes": [{
+ "dcid": "country/USA",
+ "name": "United States",
+ "provenanceId": "dc/base/WikidataOtherIdGeos",
+ "types": ["Country"]
+ }, {
+ "dcid": "usc/PacificDivision",
+ "name": "Pacific Division",
+ "provenanceId": "dc/base/WikidataOtherIdGeos",
+ "types": ["CensusDivision"]
+ }]
+ },
+ "name": {
+ "nodes": [{
+ "provenanceId": "dc/base/WikidataOtherIdGeos",
+ "value": "California"
+ }]
+ }
+ }
+ }
+ }
+ }
+
+ response = NodeResponse.model_validate(json_data)
+ result = flatten_properties(response.data)
+ function_result = response.get_properties()
+
+ assert result == function_result
+ assert "geoId/06" in result
+ assert result["geoId/06"] == {
+ "containedInPlace": [
+ Node(dcid='country/USA',
+ name='United States',
+ provenanceId='dc/base/WikidataOtherIdGeos',
+ types=['Country']),
+ Node(dcid='usc/PacificDivision',
+ name='Pacific Division',
+ provenanceId='dc/base/WikidataOtherIdGeos',
+ types=['CensusDivision'])
+ ],
+ "name": [
+ Node(
+ value='California',
+ provenanceId='dc/base/WikidataOtherIdGeos',
+ )
+ ],
+ }
+
+
+def test_unpack_arcs_missing_nodes_key():
+ """Test that unpack_arcs handles arcs with no 'nodes' key."""
+ arcs = {
+ "prop1": NodeGroup(nodes=[Node(
+ dcid='node1'), Node(dcid='node2')]),
+ "prop2": NodeGroup(), # No 'nodes' key here
+ "prop3": NodeGroup(nodes=[]),
+ }
+
+ result = unpack_arcs(arcs)
+ assert result == {
+ "prop1": [Node(dcid='node1'), Node(dcid='node2')],
+ "prop2": [],
+ "prop3": [],
+ }
+
+
+def test_unpack_arcs_multiple_properties():
+ """Test that _unpack_arcs correctly handles multiple properties with nodes."""
+ arcs = {
+ "prop1": NodeGroup(nodes=[Node(
+ dcid='node1'), Node(dcid='node2')]),
+ "prop2": NodeGroup(nodes=[Node(dcid='node3')]),
+ "prop3": NodeGroup(nodes=[]), # Empty nodes for completeness
+ }
+
+ result = unpack_arcs(arcs)
+
+ # Expected output
+ expected = {
+ "prop1": [Node(dcid='node1'), Node(dcid='node2')],
+ "prop2": [Node(dcid='node3')],
+ "prop3": [],
+ }
+
+ assert result == expected
+
+
+def test_extract_connected_dcids():
+ """Test that extract_connected_dcids is successful when multiple dcid and multiple
+ properties are in the response."""
+ json_data = {
+ "data": {
+ "geoId/06": {
+ "arcs": {
+ "containedInPlace": {
+ "nodes": [{
+ "dcid": "country/USA",
+ "name": "United States",
+ "provenanceId": "dc/base/WikidataOtherIdGeos",
+ "types": ["Country"]
+ }, {
+ "dcid": "usc/PacificDivision",
+ "name": "Pacific Division",
+ "provenanceId": "dc/base/WikidataOtherIdGeos",
+ "types": ["CensusDivision"]
+ }]
+ },
+ "name": {
+ "nodes": [{
+ "provenanceId": "dc/base/WikidataOtherIdGeos",
+ "value": "California"
+ }]
+ }
+ }
+ },
+ "geoId/07": {
+ "arcs": {
+ "containedInPlace": {
+ "nodes": [{
+ "dcid": "country/USA",
+ "name": "United States",
+ "provenanceId": "dc/base/WikidataOtherIdGeos",
+ "types": ["Country"]
+ }]
+ },
+ }
+ }
+ }
+ }
+ response = NodeResponse.model_validate(json_data)
+ result = response.extract_connected_dcids(subject_dcid='geoId/06',
+ property_dcid='containedInPlace')
+ assert result == ['country/USA', 'usc/PacificDivision']
+
+
+def test_extract_connected_dcids_with_nonexistent_dcid():
+ """Test that extract_connected_dcids returns empty when requested dcid is not in the
+ NodeResponse."""
+ json_data = {
+ "data": {
+ "geoId/06": {
+ "arcs": {
+ "name": {
+ "nodes": [{
+ "provenanceId": "dc/base/WikidataOtherIdGeos",
+ "value": "California"
+ }]
+ }
+ }
+ },
+ }
+ }
+ response = NodeResponse.model_validate(json_data)
+ result = response.extract_connected_dcids(subject_dcid='geoId/07',
+ property_dcid='name')
+ assert result == []
+
+
+def test_extract_connected_dcids_with_nonexistent_property():
+ """Test that extract_connected_dcids returns empty when requested property is not in
+ the NodeResponse."""
+ json_data = {
+ "data": {
+ "geoId/06": {
+ "arcs": {
+ "name": {
+ "nodes": [{
+ "provenanceId": "dc/base/WikidataOtherIdGeos",
+ "value": "California"
+ }]
+ }
+ }
+ },
+ }
+ }
+ response = NodeResponse.model_validate(json_data)
+ result = response.extract_connected_dcids(subject_dcid='geoId/06',
+ property_dcid='containedInPlace')
+ assert result == []
+
+
+def test_extract_connected_dcids_does_not_include_none_for_value_only_nodes():
+ """Test that extract_connected_dcids does not include None in the returned list
+ when the nodes in the response only contain values and not dcids."""
+ json_data = {
+ "data": {
+ "geoId/06": {
+ "arcs": {
+ "name": {
+ "nodes": [{
+ "provenanceId": "dc/base/WikidataOtherIdGeos",
+ "value": "California"
+ }]
+ }
+ }
+ },
+ }
+ }
+ response = NodeResponse.model_validate(json_data)
+ result = response.extract_connected_dcids(subject_dcid='geoId/06',
+ property_dcid='name')
+ assert result == []
+
+
+def test_extract_connected_dcids_with_node_type_filter():
+ """Test that extract_connected_dcids returns dcids with the corresponding
+ node_type."""
+
+ json_data = {
+ "data": {
+ "geoId/06": {
+ "arcs": {
+ "relatedPlaces": {
+ "nodes": [{
+ "dcid": "country/USA",
+ "name": "United States",
+ "provenanceId": "dc/base/WikidataOtherIdGeos",
+ "types": ["Country"]
+ }, {
+ "dcid": "usc/PacificDivision",
+ "name": "Pacific Division",
+ "provenanceId": "dc/base/WikidataOtherIdGeos",
+ "types": ["CensusDivision"]
+ }, {
+ "dcid": "node3",
+ }]
+ }
+ }
+ },
+ }
+ }
+ response = NodeResponse.model_validate(json_data)
+ result = response.extract_connected_dcids(subject_dcid='geoId/06',
+ property_dcid='relatedPlaces',
+ connected_node_types="Country")
+ assert result == ['country/USA']
+
+
+def test_extract_connected_dcids_with_multiple_node_type_filter():
+ """Test that extract_connected_dcids returns dcids with the corresponding
+ connected_node_types."""
+ json_data = {
+ "data": {
+ "geoId/06": {
+ "arcs": {
+ "relatedPlaces": {
+ "nodes": [{
+ "dcid": "country/USA",
+ "name": "United States",
+ "provenanceId": "dc/base/WikidataOtherIdGeos",
+ "types": ["Country"]
+ }, {
+ "dcid": "usc/PacificDivision",
+ "name": "Pacific Division",
+ "provenanceId": "dc/base/WikidataOtherIdGeos",
+ "types": ["CensusDivision"]
+ }, {
+ "dcid": "node3",
+ "types": ["City"]
+ }]
+ }
+ }
+ },
+ }
+ }
+ response = NodeResponse.model_validate(json_data)
+ result = response.extract_connected_dcids(
+ subject_dcid='geoId/06',
+ property_dcid='relatedPlaces',
+ connected_node_types=["Country", "City"])
+ assert result == ['country/USA', 'node3']
+
+
+def test_extract_connected_nodes_with_multiple_node_type_filter():
+ """Test that extract_connected_nodes returns only nodes with the corresponding
+ connected_node_types."""
+
+ json_data = {
+ "data": {
+ "geoId/06": {
+ "arcs": {
+ "relatedPlaces": {
+ "nodes": [{
+ "dcid": "country/USA",
+ "name": "United States",
+ "provenanceId": "dc/base/WikidataOtherIdGeos",
+ "types": ["Country"]
+ }, {
+ "dcid": "usc/PacificDivision",
+ "name": "Pacific Division",
+ "provenanceId": "dc/base/WikidataOtherIdGeos",
+ "types": ["CensusDivision"]
+ }, {
+ "dcid": "node3",
+ "types": ["City"]
+ }]
+ }
+ }
+ },
+ }
+ }
+ response = NodeResponse.model_validate(json_data)
+ result = response.extract_connected_nodes(
+ subject_dcid='geoId/06',
+ property_dcid='relatedPlaces',
+ connected_node_types=["Country", "City"])
+ assert result == [
+ Node(dcid="country/USA",
+ name="United States",
+ provenanceId="dc/base/WikidataOtherIdGeos",
+ types=["Country"]),
+ Node(dcid="node3", types=["City"])
+ ]
+
+
+### ----- Test Observation Response ----- ###
+
+
+def test_get_data_by_entity():
+ """Test that get_data_by_entity correctly extracts data grouped by entities."""
+ # Mocking ObservationResponse with byVariable data
+ mock_data = {
+ "variable1":
+ Variable(byEntity={
+ "entity1": {
+ "orderedFacets": []
+ },
+ "entity2": {
+ "orderedFacets": []
+ },
+ }),
+ "variable2":
+ Variable(byEntity={
+ "entity3": {
+ "orderedFacets": []
+ },
+ }),
+ }
+ response = ObservationResponse.model_validate({"byVariable": mock_data})
+
+ result = response.get_data_by_entity()
+
+ # Expected output
+ expected = {
+ "variable1": {
+ "entity1": {
+ "orderedFacets": []
+ },
+ "entity2": {
+ "orderedFacets": []
+ },
+ },
+ "variable2": {
+ "entity3": {
+ "orderedFacets": []
+ },
+ },
+ }
+
+ assert result.to_dict() == expected
+
+
+def test_observation_as_dict():
+ """Test that the ObservationResponse.json property returns the correct dictionary."""
+ json_data = {
+ "byVariable": {
+ "var1": {
+ "byEntity": {
+ "entity1": {
+ "orderedFacets": [{
+ "facetId": "facet1"
+ }]
+ }
+ }
+ }
+ },
+ "facets": {
+ "facet1": {
+ "unit": "GTQ",
+ "importName": "Import Name",
+ }
+ },
+ }
+
+ # Parsing JSON data
+ response = ObservationResponse.model_validate(json_data)
+
+ # Getting it back as a dictionary
+ result = response.to_dict()
+
+ assert "byVariable" in result
+ assert "facets" in result
+ assert "var1" in result["byVariable"]
+ assert "entity1" in result["byVariable"]["var1"]["byEntity"]
+
+
+def test_observation_as_dict_exclude_none():
+ """Test that the ObservationResponse.json property returns the correct dictionary."""
+ json_data = {
+ "byVariable": {
+ "var1": {
+ "byEntity": {
+ "entity1": {
+ "orderedFacets": [{
+ "facetId": "facet1"
+ }]
+ }
+ }
+ }
+ },
+ "facets": {
+ "facet1": {
+ "unit": "GTQ",
+ "importName": "Import Name",
+ }
+ },
+ }
+
+ # Parsing JSON data
+ response = ObservationResponse.model_validate(json_data)
+
+ # Getting it back as a dictionary
+ result = response.to_dict(exclude_none=True)
+
+ assert ("latestDate" not in result["byVariable"]["var1"]["byEntity"]
+ ["entity1"]["orderedFacets"][0])
+
+
+def test_observation_response_model_validation():
+ """Test that ObservationResponse.model_validate parses JSON correctly."""
+ json_data = {
+ "byVariable": {
+ "var1": {
+ "byEntity": {
+ "entity1": {
+ "orderedFacets": [{
+ "facetId": "facet1"
+ }]
+ }
+ }
+ }
+ },
+ "facets": {
+ "facet1": {
+ "unit": "GTQ",
+ "importName": "Import Name",
+ }
+ },
+ }
+
+ # Parsing JSON data
+ response = ObservationResponse.model_validate(json_data)
+
+ assert "var1" in response.byVariable
+ assert "entity1" in response.byVariable["var1"].byEntity
+ assert "facet1" in response.facets
+ assert response.facets["facet1"].unit == "GTQ"
+
+
+def test_get_data_by_entity_from_method():
+ """Test that get_data_by_entity correctly extracts data grouped by entities."""
+ # Mocking ObservationResponse with byVariable data
+ mock_data = {
+ "variable1":
+ Variable(byEntity={
+ "entity1": {
+ "orderedFacets": []
+ },
+ "entity2": {
+ "orderedFacets": []
+ },
+ }),
+ "variable2":
+ Variable(byEntity={
+ "entity3": {
+ "orderedFacets": []
+ },
+ }),
+ }
+ response = ObservationResponse.model_validate({"byVariable": mock_data})
+
+ result = response.get_data_by_entity()
+
+ # Expected output
+ expected = {
+ "variable1": {
+ "entity1": {
+ "orderedFacets": []
+ },
+ "entity2": {
+ "orderedFacets": []
+ },
+ },
+ "variable2": {
+ "entity3": {
+ "orderedFacets": []
+ },
+ },
+ }
+
+ assert result.to_dict() == expected
+
+
+def test_extract_observations():
+ """Test that extract_observations correctly processes observations with typed inputs."""
+ variable = "var1"
+ entity = "entity1"
+
+ # Mocking OrderedFacet and Observations
+ entity_data = OrderedFacets.model_validate({
+ "orderedFacets": [
+ OrderedFacet(
+ facetId="facet1",
+ earliestDate="2023-01-01",
+ latestDate="2023-01-31",
+ obsCount=2,
+ observations=[
+ Observation(date="2023-01-01", value=10.0),
+ Observation(date="2023-01-15", value=15.0),
+ ],
+ )
+ ]
+ })
+
+ # Mocking facet metadata
+ facet_metadata = {
+ "facet1":
+ Facet(
+ importName="Example Import",
+ measurementMethod="Example Method",
+ observationPeriod="P1D",
+ provenanceUrl="http://example.com",
+ unit="Example Unit",
+ )
+ }
+
+ # Extracting observations
+ result = extract_observations(variable=variable,
+ entity=entity,
+ entity_data=entity_data,
+ facet_metadata=facet_metadata)
+
+ # Assertions
+ assert len(result) == 2, "There should be two observation records."
+ assert result[0].date == "2023-01-01"
+ assert result[0].value == 10.0
+ assert result[0].facetId == "facet1"
+ assert result[0].importName == "Example Import"
+ assert result[1].date == "2023-01-15"
+ assert result[1].value == 15.0
+
+
+def test_get_observations_as_records():
+ """Test that get_observations_as_records correctly converts data into records."""
+ # Minimal input setup for byVariable and facets
+ mock_data = ByVariable.model_validate({
+ "variable1":
+ Variable(
+ byEntity={
+ "entity1": {
+ "orderedFacets": [
+ OrderedFacet(
+ facetId="facet1",
+ observations=[
+ Observation(date="2023-01-01", value=10.0),
+ Observation(date="2023-01-15", value=15.0),
+ ],
+ )
+ ]
+ }
+ })
+ })
+
+ mock_facets = {"facet1": Facet(importName="ImportName")}
+
+ # Create an ObservationResponse instance
+ response = ObservationResponse(byVariable=mock_data, facets=mock_facets)
+
+ # Call the method and get the result
+ result = response.to_observation_records()
+
+ # Expected output
+ expected = [
+ {
+ "date": "2023-01-01",
+ "entity": "entity1",
+ "variable": "variable1",
+ "value": 10.0,
+ "facetId": "facet1",
+ "importName": "ImportName",
+ },
+ {
+ "date": "2023-01-15",
+ "entity": "entity1",
+ "variable": "variable1",
+ "value": 15.0,
+ "facetId": "facet1",
+ "importName": "ImportName",
+ },
+ ]
+
+ # Assert the results
+ assert result.model_dump(exclude_none=True) == expected
+
+
+### ----- Test Resolve Response ----- ###
+
+
+def test_resolve_response_model_validation():
+ """Test that ResolveResponse.model_validate parses entities and candidates."""
+ # Mock input JSON
+ json_data = {
+ "entities": [
+ {
+ "node":
+ "entity1",
+ "candidates": [
+ {
+ "dcid": "dcid1",
+ "dominantType": "Type1"
+ },
+ {
+ "dcid": "dcid2",
+ "dominantType": None
+ },
+ ],
+ },
+ {
+ "node": "entity2",
+ "candidates": [{
+ "dcid": "dcid3",
+ "dominantType": "Type2"
+ },],
+ },
+ ]
+ }
+
+ # Parse the response
+ response = ResolveResponse.model_validate(json_data)
+
+ # Assert the number of entities
+ assert len(response.entities) == 2
+
+ # Validate the first entity
+ entity1 = response.entities[0]
+ assert entity1.node == "entity1"
+ assert len(entity1.candidates) == 2
+ assert entity1.candidates[0].dcid == "dcid1"
+ assert entity1.candidates[0].dominantType == "Type1"
+ assert entity1.candidates[1].dcid == "dcid2"
+ assert entity1.candidates[1].dominantType is None
+
+ # Validate the second entity
+ entity2 = response.entities[1]
+ assert entity2.node == "entity2"
+ assert len(entity2.candidates) == 1
+ assert entity2.candidates[0].dcid == "dcid3"
+ assert entity2.candidates[0].dominantType == "Type2"
+
+
+def test_resolve_response_dict():
+ """Test that ResolveResponse.to_dict and json are consistent."""
+ # Input dictionary
+ input_data = {
+ "entities": [
+ {
+ "node":
+ "entity1",
+ "candidates": [
+ {
+ "dcid": "dcid1",
+ "dominantType": "Type1",
+ "metadata": None,
+ "typeOf": None,
+ },
+ {
+ "dcid": "dcid2",
+ "dominantType": None,
+ "metadata": None,
+ "typeOf": None,
+ },
+ ],
+ },
+ {
+ "node":
+ "entity2",
+ "candidates": [{
+ "dcid": "dcid3",
+ "dominantType": "Type2",
+ "metadata": None,
+ "typeOf": None,
+ },],
+ },
+ ]
+ }
+
+ # Create ResolveResponse from the dictionary
+ response = ResolveResponse.model_validate(input_data)
+
+ # Convert back to dictionary using the json property
+ result = response.to_dict(exclude_none=False)
+
+ # Assert that the resulting dictionary matches the original input
+ assert result == input_data
+
+
+def test_resolve_response_dict_exclude_none():
+ """Test that ResolveResponse.to_dict and json are consistent."""
+ # Input dictionary
+ input_data = {
+ "entities": [
+ {
+ "node":
+ "entity1",
+ "candidates": [
+ {
+ "dcid": "dcid1",
+ "dominantType": "Type1"
+ },
+ {
+ "dcid": "dcid2",
+ "dominantType": None
+ },
+ ],
+ },
+ {
+ "node": "entity2",
+ "candidates": [{
+ "dcid": "dcid3",
+ "dominantType": "Type2"
+ },],
+ },
+ {
+ "node": "entity3",
+ "candidates": [],
+ },
+ ]
+ }
+
+ # Expected data
+ expected_data = {
+ "entities": [
+ {
+ "node":
+ "entity1",
+ "candidates": [
+ {
+ "dcid": "dcid1",
+ "dominantType": "Type1"
+ },
+ {
+ "dcid": "dcid2"
+ },
+ ],
+ },
+ {
+ "node": "entity2",
+ "candidates": [{
+ "dcid": "dcid3",
+ "dominantType": "Type2"
+ },],
+ },
+ {
+ "node": "entity3",
+ "candidates": [],
+ },
+ ]
+ }
+
+ # Create ResolveResponse from the dictionary
+ response = ResolveResponse.model_validate(input_data)
+
+ # Convert back to dictionary using the json property
+ result = response.to_dict(exclude_none=True)
+
+ # Assert that the resulting dictionary matches the original input
+ assert result == expected_data
+
+
+def test_resolve_response_json_string_exclude_none():
+ """Test that ResolveResponse.to_dict and json are consistent."""
+ # Input dictionary
+ input_data = {
+ "entities": [
+ {
+ "node":
+ "entity1",
+ "candidates": [
+ {
+ "dcid": "dcid1",
+ "dominantType": "Type1",
+ "metadata": None,
+ "typeOf": None,
+ },
+ {
+ "dcid": "dcid2",
+ "dominantType": None,
+ "metadata": None,
+ "typeOf": None,
+ },
+ ],
+ },
+ {
+ "node":
+ "entity2",
+ "candidates": [{
+ "dcid": "dcid3",
+ "dominantType": "Type2",
+ "metadata": None,
+ "typeOf": None,
+ },],
+ },
+ {
+ "node": "entity3",
+ "candidates": [],
+ },
+ ]
+ }
+
+ # Expected data
+ expected = json.dumps(input_data, indent=2)
+
+ # Create ResolveResponse from the dictionary
+ response = ResolveResponse.model_validate(input_data)
+
+ # Convert back to dictionary using the json property
+ result = response.to_json(exclude_none=False)
+
+ # Assert that the resulting dictionary matches the original input
+ assert result == expected
+
+
+def test_get_facets_metadata():
+ """Test that get_facets_metadata correctly extracts and structures facet metadata."""
+ payload = {
+ "byVariable": {
+ "variable1": {
+ "byEntity": {
+ "entity1": {
+ "orderedFacets": [{
+ "facetId": "facet1",
+ "earliestDate": "2023",
+ "latestDate": "2025",
+ "obsCount": 5,
+ "observations": []
+ }]
+ },
+ "entity2": {
+ "orderedFacets": [{
+ "facetId": "facet2",
+ "earliestDate": "2021",
+ "latestDate": "2021",
+ "obsCount": 3,
+ "observations": []
+ }]
+ },
+ }
+ },
+ "variable2": {
+ "byEntity": {
+ "entity3": {
+ "orderedFacets": [{
+ "facetId": "facet1",
+ "earliestDate": "2000",
+ "latestDate": "2013",
+ "obsCount": 7,
+ "observations": []
+ }]
+ }
+ }
+ },
+ },
+ "facets": {
+ "facet1": {
+ "unit": "USD",
+ "importName": "Import Source"
+ },
+ "facet2": {
+ "unit": "Year",
+ "importName": "Another Source"
+ },
+ },
+ }
+
+ # build ObservationResponse
+ response = ObservationResponse.model_validate(payload)
+
+ # Call the method
+ result = response.get_facets_metadata()
+
+ # Expected structure
+ expected = {
+ "variable1": {
+ "facet1": {
+ "earliestDate": {
+ "entity1": "2023"
+ },
+ "latestDate": {
+ "entity1": "2025"
+ },
+ "obsCount": {
+ "entity1": 5
+ },
+ "unit": "USD",
+ "importName": "Import Source",
+ "measurementMethod": None,
+ "observationPeriod": None,
+ "provenanceUrl": None,
+ },
+ "facet2": {
+ "earliestDate": {
+ "entity2": "2021"
+ },
+ "latestDate": {
+ "entity2": "2021"
+ },
+ "obsCount": {
+ "entity2": 3
+ },
+ "unit": "Year",
+ "importName": "Another Source",
+ "measurementMethod": None,
+ "observationPeriod": None,
+ "provenanceUrl": None,
+ },
+ },
+ "variable2": {
+ "facet1": {
+ "earliestDate": {
+ "entity3": "2000"
+ },
+ "latestDate": {
+ "entity3": "2013"
+ },
+ "obsCount": {
+ "entity3": 7
+ },
+ "unit": "USD",
+ "importName": "Import Source",
+ "measurementMethod": None,
+ "observationPeriod": None,
+ "provenanceUrl": None,
+ }
+ },
+ }
+
+ assert result == expected
+
+
+def test_find_matching_facet_id(monkeypatch):
+ """Tests that find_matching_facet_id correctly finds facet IDs matching a given property and value."""
+ mock_response = ObservationResponse(byVariable={}, facets={})
+ mock_metadata = {
+ "statvar1": {
+ "facet1": {
+ "measurementMethod": "Census"
+ },
+ "facet2": {
+ "measurementMethod": "Survey"
+ },
+ },
+ "statvar2": {
+ "facet3": {
+ "unit": "USD"
+ },
+ },
+ }
+ monkeypatch.setattr(
+ ObservationResponse,
+ "get_facets_metadata",
+ lambda self: mock_metadata,
+ )
+
+ result = mock_response.find_matching_facet_id("measurementMethod", "Census")
+ assert result == ["facet1"]
+
+ result = mock_response.find_matching_facet_id("measurementMethod",
+ ["Census", "Survey"])
+ assert result == ["facet1", "facet2"]
+
+ result = mock_response.find_matching_facet_id("unit", "USD")
+ assert result == ["facet3"]
+
+ result = mock_response.find_matching_facet_id("measurementMethod",
+ "Nonexistent")
+ assert result == []
diff --git a/datacommons_client/tests/models/test_node_models.py b/datacommons_client/tests/models/test_node_models.py
new file mode 100644
index 00000000..ce70f2a6
--- /dev/null
+++ b/datacommons_client/tests/models/test_node_models.py
@@ -0,0 +1,153 @@
+from datacommons_client.models.node import Arcs
+from datacommons_client.models.node import Node
+from datacommons_client.models.node import NodeGroup
+from datacommons_client.models.node import Properties
+from datacommons_client.models.node import StatVarConstraint
+from datacommons_client.models.node import StatVarConstraints
+
+
+def test_node_model_validation():
+ """Test that Node.model_validate parses data correctly."""
+ json_data = {
+ "dcid": "node123",
+ "name": "Test Node",
+ "provenanceId": "prov123",
+ "types": ["TypeA", "TypeB"],
+ "value": "42",
+ }
+ node = Node.model_validate(json_data)
+ assert node.dcid == "node123"
+ assert node.name == "Test Node"
+ assert node.provenanceId == "prov123"
+ assert node.types == ["TypeA", "TypeB"]
+ assert node.value == "42"
+
+
+def test_node_model_validation_partial():
+ """Test Node.model_validate with partial data."""
+ json_data = {
+ "dcid": "node123",
+ }
+ node = Node.model_validate(json_data)
+ assert node.dcid == "node123"
+ assert node.name is None
+ assert node.provenanceId is None
+ assert node.types is None
+ assert node.value is None
+
+
+def test_nodegroup_model_validation():
+ """Test that NodeGroup.model_validate parses data correctly."""
+ json_data = {
+ "nodes": [
+ {
+ "dcid": "node1",
+ "name": "Node 1"
+ },
+ {
+ "dcid": "node2",
+ "name": "Node 2"
+ },
+ ]
+ }
+ node_group = NodeGroup.model_validate(json_data)
+ assert len(node_group.nodes) == 2
+ assert node_group.nodes[0].dcid == "node1"
+ assert node_group.nodes[1].name == "Node 2"
+
+
+def test_nodegroup_model_validation_empty():
+ """Test NodeGroup.model_validate with empty data."""
+ json_data = {}
+ node_group = NodeGroup.model_validate(json_data)
+ assert len(node_group.nodes) == 0
+
+
+def test_arcs_model_validation():
+ """Test that Arcs.model_validate parses data correctly."""
+ json_data = {
+ "arcs": {
+ "label1": {
+ "nodes": [{
+ "dcid": "node1"
+ }, {
+ "dcid": "node2"
+ }]
+ },
+ "label2": {
+ "nodes": [{
+ "dcid": "node3"
+ }]
+ },
+ }
+ }
+ arcs = Arcs.model_validate(json_data)
+ assert len(arcs.arcs) == 2
+ assert "label1" in arcs.arcs
+ assert len(arcs.arcs["label1"].nodes) == 2
+ assert arcs.arcs["label1"].nodes[0].dcid == "node1"
+ assert len(arcs.arcs["label2"].nodes) == 1
+ assert arcs.arcs["label2"].nodes[0].dcid == "node3"
+
+
+def test_arcs_model_validation_empty():
+ """Test Arcs.model_validate with empty data."""
+ json_data = {}
+ arcs = Arcs.model_validate(json_data)
+ assert len(arcs.arcs) == 0
+
+
+def test_properties_model_validation():
+ """Test that Properties.model_validate parses data correctly."""
+ json_data = {"properties": ["prop1", "prop2", "prop3"]}
+ properties = Properties.model_validate(json_data)
+ assert len(properties.properties) == 3
+ assert properties.properties == ["prop1", "prop2", "prop3"]
+
+
+def test_properties_model_validation_empty():
+ """Test Properties.model_validate with empty data."""
+ json_data = {}
+ properties = Properties.model_validate(json_data)
+ assert properties.properties is None
+
+
+def test_statvarconstraint_model_validation():
+ """Test StatVarConstraint.model_validate parses data correctly."""
+ data = {
+ "constraintId": "DevelopmentFinanceScheme",
+ "constraintName": "Development Finance Scheme",
+ "valueId": "ODAGrants",
+ "valueName": "Official Development Assistance Grants",
+ }
+ constraint = StatVarConstraint.model_validate(data)
+
+ assert constraint.constraintId == "DevelopmentFinanceScheme"
+ assert constraint.constraintName == "Development Finance Scheme"
+ assert constraint.valueId == "ODAGrants"
+ assert constraint.valueName == "Official Development Assistance Grants"
+
+
+def test_statvarconstraints_model_validation():
+ """Test StatVarConstraints root model validates mapping properly."""
+ constraints = StatVarConstraints.model_validate({
+ "sv/1": [
+ {
+ "constraintId": "DevelopmentFinanceScheme",
+ "constraintName": "Development Finance Scheme",
+ "valueId": "ODAGrants",
+ "valueName": "Official Development Assistance Grants",
+ },
+ {
+ "constraintId": "DevelopmentFinanceRecipient",
+ "constraintName": "Development Finance Recipient",
+ "valueId": "country/GTM",
+ "valueName": "Guatemala",
+ },
+ ],
+ "sv/2": [],
+ })
+
+ assert "sv/1" in constraints and "sv/2" in constraints
+ assert len(constraints["sv/1"]) == 2
+ assert constraints["sv/2"] == []
diff --git a/datacommons_client/tests/models/test_observation_models.py b/datacommons_client/tests/models/test_observation_models.py
new file mode 100644
index 00000000..670f85bb
--- /dev/null
+++ b/datacommons_client/tests/models/test_observation_models.py
@@ -0,0 +1,157 @@
+import pytest
+
+from datacommons_client.models.observation import Facet
+from datacommons_client.models.observation import Observation
+from datacommons_client.models.observation import ObservationSelectList
+from datacommons_client.models.observation import OrderedFacet
+from datacommons_client.models.observation import Variable
+from datacommons_client.utils.error_handling import InvalidObservationSelectError
+
+
+def test_observation_model_validation():
+ """Test that Observation.model_validate parses data correctly."""
+ json_data = {"date": "2024-01-01", "value": 123.45}
+ observation = Observation.model_validate(json_data)
+ assert observation.date == "2024-01-01"
+ assert observation.value == 123.45
+ assert isinstance(observation.value, float)
+
+
+def test_observation_model_validation_partial():
+ """Test Observation.model_validate with missing data."""
+ json_data = {"date": "2024-01-01"}
+ observation = Observation.model_validate(json_data)
+ assert observation.date == "2024-01-01"
+ assert observation.value is None
+
+
+def test_ordered_facets_model_validation():
+ """Test that OrderedFacet.model_validate parses data correctly."""
+ json_data = {
+ "earliestDate":
+ "2023-01-01",
+ "facetId":
+ "facet123",
+ "latestDate":
+ "2024-01-01",
+ "obsCount":
+ 2,
+ "observations": [
+ {
+ "date": "2023-01-01",
+ "value": 100.0
+ },
+ {
+ "date": "2024-01-01",
+ "value": 200.0
+ },
+ ],
+ }
+ ordered_facets = OrderedFacet.model_validate(json_data)
+ assert ordered_facets.earliestDate == "2023-01-01"
+ assert ordered_facets.facetId == "facet123"
+ assert ordered_facets.latestDate == "2024-01-01"
+ assert ordered_facets.obsCount == 2
+ assert len(ordered_facets.observations) == 2
+ assert ordered_facets.observations[0].value == 100.0
+
+
+def test_ordered_facets_model_validation_empty_observations():
+ """Test OrderedFacet.model_validate with empty observations."""
+ json_data = {
+ "earliestDate": "2023-01-01",
+ "facetId": "facet123",
+ "latestDate": "2024-01-01",
+ "obsCount": 0,
+ "observations": [],
+ }
+ ordered_facets = OrderedFacet.model_validate(json_data)
+ assert len(ordered_facets.observations) == 0
+
+
+def test_variable_model_validation():
+ """Test that Variable.model_validate parses data correctly."""
+ json_data = {
+ "byEntity": {
+ "entity1": {
+ "orderedFacets": [{
+ "earliestDate":
+ "2023-01-01",
+ "facetId":
+ "facet1",
+ "latestDate":
+ "2023-12-31",
+ "obsCount":
+ 2,
+ "observations": [
+ {
+ "date": "2023-01-01",
+ "value": 50.0
+ },
+ {
+ "date": "2023-12-31",
+ "value": 75.0
+ },
+ ],
+ }]
+ }
+ }
+ }
+ variable = Variable.model_validate(json_data)
+ assert "entity1" in variable.byEntity
+ facets = variable.byEntity["entity1"].orderedFacets
+ assert len(facets) == 1
+ assert facets[0].facetId == "facet1"
+ assert facets[0].observations[0].value == 50.0
+
+
+def test_variable_model_validation_empty():
+ """Test Variable.model_validate with empty byEntity."""
+ json_data = {"byEntity": {}}
+ variable = Variable.model_validate(json_data)
+ assert len(variable.byEntity) == 0
+
+
+def test_facet_model_validation():
+ """Test that Facet.model_validate parses data correctly."""
+ json_data = {
+ "importName": "Import 1",
+ "measurementMethod": "Method A",
+ "observationPeriod": "2023",
+ "provenanceUrl": "http://example.com",
+ "unit": "usd",
+ }
+ facet = Facet.model_validate(json_data)
+ assert facet.importName == "Import 1"
+ assert facet.measurementMethod == "Method A"
+ assert facet.observationPeriod == "2023"
+ assert facet.provenanceUrl == "http://example.com"
+ assert facet.unit == "usd"
+
+
+def test_facet_model_validation_partial():
+ """Test Facet.model_validate with missing data."""
+ json_data = {"importName": "Import 1", "unit": "GTQ"}
+ facet = Facet.model_validate(json_data)
+ assert facet.importName == "Import 1"
+ assert facet.measurementMethod is None
+ assert facet.unit == "GTQ"
+ assert facet.provenanceUrl is None
+
+
+def test_observation_select_list_defaults():
+ """ObservationSelectList returns default selects when none provided."""
+ osl = ObservationSelectList.model_validate(None)
+ assert osl.select == ["date", "variable", "entity", "value"]
+
+
+def test_observation_select_list_custom():
+ """ObservationSelectList accepts custom select lists."""
+ osl = ObservationSelectList.model_validate(["variable", "entity", "facet"])
+ assert osl.select == ["variable", "entity", "facet"]
+
+
+def test_observation_select_list_missing_required():
+ """Missing required select entries raises InvalidObservationSelectError."""
+ with pytest.raises(InvalidObservationSelectError):
+ ObservationSelectList.model_validate(["date", "value"])
diff --git a/datacommons_client/tests/models/test_resolve_models.py b/datacommons_client/tests/models/test_resolve_models.py
new file mode 100644
index 00000000..7a5e205e
--- /dev/null
+++ b/datacommons_client/tests/models/test_resolve_models.py
@@ -0,0 +1,51 @@
+from datacommons_client.models.resolve import Candidate
+from datacommons_client.models.resolve import Entity
+
+
+def test_candidate_model_validation():
+ """Test that Candidate.model_validate parses full data correctly."""
+ json_data = {"dcid": "dcid123", "dominantType": "Place"}
+ candidate = Candidate.model_validate(json_data)
+ assert candidate.dcid == "dcid123"
+ assert candidate.dominantType == "Place"
+
+
+def test_candidate_model_validation_partial():
+ """Test Candidate.model_validate with missing optional dominantType."""
+ json_data = {"dcid": "dcid456"}
+ candidate = Candidate.model_validate(json_data)
+ assert candidate.dcid == "dcid456"
+ assert candidate.dominantType is None
+
+
+def test_entity_model_validation():
+ """Test that Entity.model_validate handles multiple candidates."""
+ json_data = {
+ "node":
+ "test_query",
+ "candidates": [
+ {
+ "dcid": "dcid123",
+ "dominantType": "Place"
+ },
+ {
+ "dcid": "dcid456",
+ "dominantType": "Event"
+ },
+ ],
+ }
+ entity = Entity.model_validate(json_data)
+ assert entity.node == "test_query"
+ assert len(entity.candidates) == 2
+ assert entity.candidates[0].dcid == "dcid123"
+ assert entity.candidates[0].dominantType == "Place"
+ assert entity.candidates[1].dcid == "dcid456"
+ assert entity.candidates[1].dominantType == "Event"
+
+
+def test_entity_model_validation_empty_candidates():
+ """Test Entity.model_validate with no candidates."""
+ json_data = {"node": "test_query", "candidates": []}
+ entity = Entity.model_validate(json_data)
+ assert entity.node == "test_query"
+ assert len(entity.candidates) == 0
diff --git a/datacommons_client/tests/test_client.py b/datacommons_client/tests/test_client.py
new file mode 100644
index 00000000..221befff
--- /dev/null
+++ b/datacommons_client/tests/test_client.py
@@ -0,0 +1,473 @@
+from unittest.mock import MagicMock
+from unittest.mock import patch
+
+import pandas as pd
+import pytest
+
+from datacommons_client import use_api_key
+from datacommons_client.client import DataCommonsClient
+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.models.node import Name
+from datacommons_client.models.node import StatVarConstraint
+from datacommons_client.models.node import StatVarConstraints
+from datacommons_client.models.observation import ObservationRecord
+from datacommons_client.models.observation import ObservationRecords
+from datacommons_client.utils.error_handling import NoDataForPropertyError
+
+
+@pytest.fixture
+def mock_client():
+ """Fixture to create a DataCommonsClient instance with a mocked API."""
+ with patch("datacommons_client.endpoints.base.API.post") as mock_post:
+ client = DataCommonsClient(api_key="test_key")
+ client.observation = MagicMock(spec=ObservationEndpoint)
+ return client
+
+
+@patch(
+ "datacommons_client.endpoints.base.resolve_instance_url",
+ return_value="https://datacommons.org",
+)
+@patch(
+ "datacommons_client.utils.request_handling.check_instance_is_valid",
+ return_value="https://datacommons.org",
+)
+def test_datacommons_client_initialization(mock_check_instance,
+ mock_resolve_instance_url):
+ """Tests that DataCommonsClient initializes correctly with API and endpoints, using a fake address."""
+ client = DataCommonsClient(api_key="test_key", dc_instance="test_instance")
+
+ assert isinstance(client.api, API)
+ assert client.api.headers == {
+ "Content-Type": "application/json",
+ "X-API-Key": "test_key",
+ "x-surface": "clientlib-python"
+ }
+
+ assert isinstance(client.node, NodeEndpoint)
+ assert isinstance(client.observation, ObservationEndpoint)
+ assert isinstance(client.resolve, ResolveEndpoint)
+
+ assert client.node.api is client.api
+ assert client.observation.api is client.api
+ assert client.resolve.api is client.api
+
+
+@patch(
+ "datacommons_client.endpoints.base.check_instance_is_valid",
+ return_value="https://test.url",
+)
+@patch(
+ "datacommons_client.endpoints.base.resolve_instance_url",
+ return_value="https://datacommons.org",
+)
+def test_datacommons_client_initialization_with_surface_header(
+ mock_check_instance, mock_resolve_instance_url):
+ """Tests that DataCommonsClient initializes correctly with a surface header value."""
+
+ client = DataCommonsClient(api_key="test_key",
+ dc_instance="test_instance",
+ surface_header_value="mcp-1.0")
+
+ assert isinstance(client.api, API)
+ assert client.api.headers == {
+ "Content-Type": "application/json",
+ "X-API-Key": "test_key",
+ "x-surface": "mcp-1.0"
+ }
+
+
+def test_datacommons_client_raises_error_when_both_url_and_instance_are_provided(
+):
+ """Tests that DataCommonsClient raises a ValueError when both `dc_instance` and `url` are given."""
+ with pytest.raises(ValueError,
+ match="Cannot provide both `dc_instance` and `url`."):
+ DataCommonsClient(api_key="test_key",
+ dc_instance="test_instance",
+ url="https://test.url")
+
+
+def test_observations_dataframe_raises_error_when_entities_all_but_no_entity_type(
+ mock_client,):
+ """Tests that ValueError is raised if 'entities' is 'all' but 'entity_type' is not specified."""
+ with pytest.raises(
+ ValueError,
+ match=
+ "When 'entity_dcids' is 'all', both 'parent_entity' and 'entity_type' must be specified.",
+ ):
+ mock_client.observations_dataframe(variable_dcids="var1",
+ date="2024",
+ entity_dcids="all",
+ parent_entity="africa")
+
+
+def test_observations_dataframe_raises_error_when_entities_all_but_no_parent_entity(
+ mock_client,):
+ """Tests that ValueError is raised if 'entities' is 'all' but 'entity_type' is not specified."""
+ with pytest.raises(
+ ValueError,
+ match=
+ "When 'entity_dcids' is 'all', both 'parent_entity' and 'entity_type' must be specified.",
+ ):
+ mock_client.observations_dataframe(variable_dcids="var1",
+ date="2024",
+ entity_dcids="all",
+ entity_type="Country")
+
+
+def test_observations_dataframe_raises_error_when_invalid_entity_type_usage(
+ mock_client,):
+ """Tests that ValueError is raised if 'entity_type' or 'parent_entity' is specified with specific entities."""
+ with pytest.raises(
+ ValueError,
+ match="Specify 'entity_type' and 'parent_entity'"
+ " only when 'entity_dcids' is 'all'.",
+ ):
+ mock_client.observations_dataframe(
+ variable_dcids="var1",
+ date="2024",
+ entity_dcids=["entity1"],
+ entity_type="Country",
+ )
+
+
+def test_observations_dataframe_calls_fetch_observations_by_entity_type(
+ mock_client):
+ """Tests that fetch_observations_by_entity_type is called with correct parameters."""
+ mock_client.observation.fetch_observations_by_entity_type.return_value.to_observation_records.return_value = (
+ ObservationRecords.model_validate([]))
+
+ df = mock_client.observations_dataframe(
+ variable_dcids=["var1", "var2"],
+ date="2024",
+ entity_dcids="all",
+ entity_type="Country",
+ parent_entity="Earth",
+ )
+
+ mock_client.observation.fetch_observations_by_entity_type.assert_called_once_with(
+ date="2024",
+ variable_dcids=["var1", "var2"],
+ entity_type="Country",
+ parent_entity="Earth",
+ filter_facet_ids=None,
+ )
+
+ assert isinstance(df, pd.DataFrame)
+ assert df.empty
+
+
+def test_observations_dataframe_calls_fetch_observations_by_entity(mock_client):
+ """Tests that fetch_observations_by_entity is called with correct parameters."""
+
+ mock_client.observation.fetch_observations_by_entity_dcid.return_value.to_observation_records.return_value = (
+ ObservationRecords([]))
+
+ df = mock_client.observations_dataframe(variable_dcids="var1",
+ date="latest",
+ entity_dcids=["entity1", "entity2"])
+
+ mock_client.observation.fetch_observations_by_entity_dcid.assert_called_once_with(
+ date="latest",
+ entity_dcids=["entity1", "entity2"],
+ variable_dcids="var1",
+ filter_facet_ids=None,
+ )
+
+ assert isinstance(df, pd.DataFrame)
+ assert df.empty
+
+
+def test_observations_dataframe_returns_dataframe_with_expected_columns(
+ mock_client):
+ """Tests that the method returns a DataFrame with expected columns."""
+ mock_client.observation.fetch_observations_by_entity_dcid.return_value.to_observation_records.return_value = ObservationRecords.model_validate(
+ [
+ {
+ "date": "2024",
+ "entity": "entity1",
+ "variable": "var1",
+ "value": 100,
+ "unit": "unit1",
+ },
+ {
+ "date": "2024",
+ "entity": "entity2",
+ "variable": "var2",
+ "value": 200,
+ "unit": "unit2",
+ },
+ ])
+
+ # Mock entity name lookup to prevent API calls
+ mock_client.node.fetch_entity_names = MagicMock(return_value={
+ "entity1": Name(value="Entity One", language="en", property="name"),
+ "entity2": Name(value="Entity Two", language="en", property="name"),
+ "var1": Name(value="Variable One", language="en", property="name"),
+ "var2": Name(value="Variable Two", language="en", property="name"),
+ },)
+
+ df = mock_client.observations_dataframe(variable_dcids="var1",
+ date="2024",
+ entity_dcids=["entity1", "entity2"])
+
+ assert isinstance(df, pd.DataFrame)
+ assert set(df.columns) == {
+ "date", "entity", "entity_name", "variable", "variable_name", "value",
+ "unit"
+ }
+ assert len(df) == 2
+ assert df.iloc[0]["entity"] == "entity1"
+ assert df.iloc[0]["entity_name"] == "Entity One"
+ assert df.iloc[1]["entity"] == "entity2"
+ assert df.iloc[1]["entity_name"] == "Entity Two"
+ assert df.iloc[0]["variable"] == "var1"
+ assert df.iloc[0]["variable_name"] == "Variable One"
+ assert df.iloc[0]["value"] == 100
+ assert df.iloc[0]["unit"] == "unit1"
+ assert df.iloc[1]["variable"] == "var2"
+ assert df.iloc[1]["variable_name"] == "Variable Two"
+ assert df.iloc[1]["value"] == 200
+ assert df.iloc[1]["unit"] == "unit2"
+
+
+def test_observations_dataframe_includes_constraints_metadata(mock_client):
+ """When include_constraints_metadata=True, DataFrame includes constraint columns."""
+ # Two observations with different variables
+ mock_client.observation.fetch_observations_by_entity_dcid.return_value.to_observation_records.return_value = ObservationRecords.model_validate(
+ [
+ {
+ "date": "2021",
+ "entity": "geo/1",
+ "variable": "sv/A",
+ "value": 1,
+ "unit": "Count",
+ },
+ {
+ "date": "2021",
+ "entity": "geo/2",
+ "variable": "sv/B",
+ "value": 2,
+ "unit": "Count",
+ },
+ ])
+
+ # Avoid name lookups
+ mock_client.node.fetch_entity_names = MagicMock(return_value={})
+
+ mock_client.node.fetch_statvar_constraints = MagicMock(
+ return_value=StatVarConstraints.model_validate({
+ "sv/A": [
+ StatVarConstraint(
+ constraintId="DevelopmentFinanceScheme",
+ constraintName="Development Finance Scheme",
+ valueId="ODAGrants",
+ valueName="Official Development Assistance Grants",
+ )
+ ],
+ "sv/B": [
+ StatVarConstraint(
+ constraintId="sex",
+ constraintName="Sex",
+ valueId="Female",
+ valueName="Female",
+ )
+ ],
+ }))
+
+ df = mock_client.observations_dataframe(
+ variable_dcids=["sv/A", "sv/B"],
+ date="2021",
+ entity_dcids=["geo/1", "geo/2"],
+ include_constraints_metadata=True,
+ )
+
+ # Check presence and correctness of constraint columns
+ assert "DevelopmentFinanceScheme" in df.columns
+ assert "DevelopmentFinanceScheme_name" in df.columns
+ assert "sex" in df.columns and "sex_name" in df.columns
+
+ row_a = df[df["variable"] == "sv/A"].iloc[0]
+ assert row_a["DevelopmentFinanceScheme"] == "ODAGrants"
+ assert (row_a["DevelopmentFinanceScheme_name"] ==
+ "Official Development Assistance Grants")
+
+ row_b = df[df["variable"] == "sv/B"].iloc[0]
+ assert row_b["sex"] == "Female"
+ assert row_b["sex_name"] == "Female"
+
+
+@patch(
+ "datacommons_client.endpoints.base.check_instance_is_valid",
+ return_value="https://test.url",
+)
+def test_dc_instance_is_ignored_when_url_is_provided(mock_check_instance):
+ """Tests that dc_instance is ignored when a fully resolved URL is provided."""
+
+ client = DataCommonsClient(api_key="test_key", url="https://test.url")
+
+ # Check that the API base_url is set to the fully resolved url
+ assert client.api.base_url == "https://test.url"
+
+
+def test_find_filter_facet_ids_returns_none_when_no_filters(mock_client):
+ """Tests that _find_filter_facet_ids returns None when no filters are provided."""
+ result = mock_client._find_filter_facet_ids(fetch_by="entity",
+ date="2024",
+ variable_dcids="var1",
+ property_filters=None)
+ assert result is None
+
+
+def test_find_filter_facet_ids_returns_facet_ids(mock_client):
+ """Tests that _find_filter_facet_ids correctly returns facet IDs when filters are provided."""
+ mock_client.observation.fetch_observations_by_entity_dcid.return_value.find_matching_facet_id.side_effect = [
+ ["213"], ["3243"]
+ ]
+
+ result = mock_client._find_filter_facet_ids(
+ fetch_by="entity",
+ date="2024",
+ variable_dcids="var1",
+ property_filters={
+ "measurementMethod": "Census",
+ "unit": "USD"
+ },
+ )
+
+ assert set(result) == {"213", "3243"}
+
+
+def test_observations_dataframe_filters_by_facet_ids(mock_client):
+ """Tests that observations_dataframe includes facet filtering when property_filters are used."""
+ mock_client._find_filter_facet_ids = MagicMock(
+ return_value=["facet_1", "facet_2"])
+
+ mock_client.observation.fetch_observations_by_entity_dcid.return_value.to_observation_records.return_value = (
+ ObservationRecords.model_validate([]))
+
+ df = mock_client.observations_dataframe(
+ variable_dcids="var1",
+ date="2024",
+ entity_dcids=["entity1"],
+ property_filters={"measurementMethod": "Census"},
+ )
+
+ mock_client.observation.fetch_observations_by_entity_dcid.assert_called_once_with(
+ variable_dcids="var1",
+ date="2024",
+ entity_dcids=["entity1"],
+ filter_facet_ids=["facet_1", "facet_2"],
+ )
+ assert isinstance(df, pd.DataFrame)
+
+
+def test_observations_dataframe_raises_error_when_no_facet_match(mock_client):
+ """Tests that observations_dataframe raises NoDataForPropertyError when no facets match the filters."""
+ mock_client._find_filter_facet_ids = MagicMock(return_value=None)
+
+ with pytest.raises(NoDataForPropertyError):
+ mock_client.observations_dataframe(
+ variable_dcids="var1",
+ date="2024",
+ entity_dcids=["entity1"],
+ property_filters={"measurementMethod": "Nonexistent"},
+ )
+
+ mock_client._find_filter_facet_ids = MagicMock(return_value=[])
+
+ with pytest.raises(NoDataForPropertyError):
+ mock_client.observations_dataframe(
+ variable_dcids="var2",
+ date="2024",
+ entity_dcids=["entity1"],
+ property_filters={"measurementMethodX": "Nonexistent"},
+ )
+
+
+@patch("datacommons_client.utils.request_handling.requests.post")
+@patch(
+ "datacommons_client.utils.request_handling.check_instance_is_valid",
+ return_value="https://datacommons.org",
+)
+def test_client_end_to_end_surface_header_propagation_observation(
+ mock_check_instance, mock_post):
+ """Tests that the surface_header_value is propagated from client to the final request via the observation endpoint."""
+
+ # Mock the response from requests.post
+ mock_response = MagicMock()
+ mock_response.status_code = 200
+ mock_response.json.return_value = {"byVariable": {}, "facets": {}}
+ mock_post.return_value = mock_response
+
+ # Initialize the client with a surface header value
+ client = DataCommonsClient(api_key="test_key",
+ surface_header_value="datagemma")
+
+ # Call a method on the observation endpoint that will trigger a post request
+ client.observation.fetch_observations_by_entity_dcid(
+ entity_dcids=["country/USA"],
+ variable_dcids=["Count_Person"],
+ date="2021")
+
+ # Check that requests.post was called with the correct headers
+ mock_post.assert_called_once()
+ _, kwargs = mock_post.call_args
+ headers = kwargs.get("headers")
+ assert headers is not None
+ assert headers.get("x-surface") == "datagemma"
+ assert headers.get("X-API-Key") == "test_key"
+
+
+@patch("datacommons_client.endpoints.base.post_request")
+def test_use_api_key_with_observation_fetch(mock_post_request):
+ """Test use_api_key override for observation fetches (non-threaded)."""
+
+ # Setup client with default key
+ client = DataCommonsClient(api_key="default-key")
+
+ # Configure mock to return valid response structure
+ mock_post_request.return_value = {"byVariable": {}, "facets": {}}
+
+ # Default usage
+ client.observation.fetch(variable_dcids="sv1", entity_dcids=["geo1"])
+ mock_post_request.assert_called()
+ _, kwargs = mock_post_request.call_args
+ assert kwargs["headers"]["X-API-Key"] == "default-key"
+
+ # Context override
+ with use_api_key("context-key"):
+ client.observation.fetch(variable_dcids="sv1", entity_dcids=["geo1"])
+ _, kwargs = mock_post_request.call_args
+ assert kwargs["headers"]["X-API-Key"] == "context-key"
+
+ # Back to default
+ client.observation.fetch(variable_dcids="sv1", entity_dcids=["geo1"])
+ _, kwargs = mock_post_request.call_args
+ assert kwargs["headers"]["X-API-Key"] == "default-key"
+
+
+@patch("datacommons_client.endpoints.base.post_request")
+def test_use_api_key_with_node_fetch_place_ancestors(mock_post_request):
+ """Test use_api_key propagation for node graph methods (threaded)."""
+
+ client = DataCommonsClient(api_key="default-key")
+
+ # Configure mock. fetch_place_ancestors expects a dict response or list of nodes.
+ # NodeResponse.data is a dict.
+ mock_post_request.return_value = {"data": {}}
+
+ # Default usage
+ client.node.fetch_place_ancestors(place_dcids=["geoId/06"])
+ _, kwargs = mock_post_request.call_args
+ assert kwargs["headers"]["X-API-Key"] == "default-key"
+
+ # Context override
+ with use_api_key("context-key"):
+ # Use a different DCID to avoid hitting fetch_relationship_lru cache
+ client.node.fetch_place_ancestors(place_dcids=["geoId/07"])
+ _, kwargs = mock_post_request.call_args
+ assert kwargs["headers"]["X-API-Key"] == "context-key"
diff --git a/datacommons_client/tests/test_context.py b/datacommons_client/tests/test_context.py
new file mode 100644
index 00000000..ef7117cb
--- /dev/null
+++ b/datacommons_client/tests/test_context.py
@@ -0,0 +1,43 @@
+# Copyright 2025 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from datacommons_client.utils.context import _API_KEY_CONTEXT_VAR
+from datacommons_client.utils.context import use_api_key
+
+
+def test_use_api_key_sets_var():
+ """Test that use_api_key sets the context variable."""
+ assert _API_KEY_CONTEXT_VAR.get() is None
+ with use_api_key("test-key"):
+ assert _API_KEY_CONTEXT_VAR.get() == "test-key"
+ assert _API_KEY_CONTEXT_VAR.get() is None
+
+
+def test_use_api_key_nested():
+ """Test nested usage of use_api_key."""
+ with use_api_key("outer"):
+ assert _API_KEY_CONTEXT_VAR.get() == "outer"
+ with use_api_key("inner"):
+ assert _API_KEY_CONTEXT_VAR.get() == "inner"
+ assert _API_KEY_CONTEXT_VAR.get() == "outer"
+ assert _API_KEY_CONTEXT_VAR.get() is None
+
+
+def test_use_api_key_none():
+ """Test that use_api_key with None/empty does not set the variable."""
+ assert _API_KEY_CONTEXT_VAR.get() is None
+ with use_api_key(None):
+ assert _API_KEY_CONTEXT_VAR.get() is None
+ with use_api_key(""):
+ assert _API_KEY_CONTEXT_VAR.get() is None
diff --git a/datacommons_client/tests/test_dataframes.py b/datacommons_client/tests/test_dataframes.py
new file mode 100644
index 00000000..076c1972
--- /dev/null
+++ b/datacommons_client/tests/test_dataframes.py
@@ -0,0 +1,88 @@
+from unittest.mock import MagicMock
+
+import pandas as pd
+
+from datacommons_client.endpoints.node import NodeEndpoint
+from datacommons_client.models.node import StatVarConstraint
+from datacommons_client.models.node import StatVarConstraints
+from datacommons_client.utils.dataframes import add_property_constraints_to_observations_dataframe
+
+
+def test_add_property_constraints_to_observations_dataframe_adds_columns():
+ """Adds constraint id and name columns based on statvar metadata."""
+ # Input observations
+ df = pd.DataFrame([
+ {
+ "date": "2020",
+ "entity": "geo/1",
+ "variable": "sv/A",
+ "value": 10,
+ "unit": "Count",
+ },
+ {
+ "date": "2020",
+ "entity": "geo/2",
+ "variable": "sv/B",
+ "value": 20,
+ "unit": "Count",
+ },
+ ])
+
+ endpoint = MagicMock(spec=NodeEndpoint)
+
+ endpoint.fetch_statvar_constraints.return_value = StatVarConstraints.model_validate(
+ {
+ "sv/A": [
+ StatVarConstraint(
+ constraintId="DevelopmentFinanceScheme",
+ constraintName="Development Finance Scheme",
+ valueId="ODAGrants",
+ valueName="Official Development Assistance Grants",
+ ),
+ StatVarConstraint(
+ constraintId="DevelopmentFinanceRecipient",
+ constraintName="Development Finance Recipient",
+ valueId="country/GTM",
+ valueName="Guatemala",
+ ),
+ ],
+ "sv/B": [
+ StatVarConstraint(
+ constraintId="sex",
+ constraintName="Sex",
+ valueId="Female",
+ valueName="Female",
+ )
+ ],
+ })
+
+ out = add_property_constraints_to_observations_dataframe(endpoint=endpoint,
+ observations_df=df)
+
+ # Columns for constraints should be present and filled per variable
+ assert "DevelopmentFinanceScheme" in out.columns
+ assert "DevelopmentFinanceScheme_name" in out.columns
+ assert ("DevelopmentFinanceRecipient" in out.columns and
+ "DevelopmentFinanceRecipient_name" in out.columns)
+ assert "sex" in out.columns and "sex_name" in out.columns
+
+ # Row-wise checks
+ row_a = out[out["variable"] == "sv/A"].iloc[0]
+ assert row_a["DevelopmentFinanceScheme"] == "ODAGrants"
+ assert row_a[
+ "DevelopmentFinanceScheme_name"] == "Official Development Assistance Grants"
+ assert row_a["DevelopmentFinanceRecipient"] == "country/GTM"
+ assert row_a["DevelopmentFinanceRecipient_name"] == "Guatemala"
+
+ row_b = out[out["variable"] == "sv/B"].iloc[0]
+ assert row_b["sex"] == "Female"
+ assert row_b["sex_name"] == "Female"
+
+
+def test_add_property_constraints_to_observations_dataframe_empty():
+ """Empty DataFrame returns unchanged."""
+ endpoint = MagicMock(spec=NodeEndpoint)
+ empty_df = pd.DataFrame([])
+ out = add_property_constraints_to_observations_dataframe(
+ endpoint=endpoint, observations_df=empty_df)
+ assert out.empty
diff --git a/datacommons_client/tests/test_decorators.py b/datacommons_client/tests/test_decorators.py
new file mode 100644
index 00000000..ed143143
--- /dev/null
+++ b/datacommons_client/tests/test_decorators.py
@@ -0,0 +1,46 @@
+from unittest import mock
+
+import pytest
+
+from datacommons_client.utils.decorators import requires_pandas
+
+try:
+ import pandas as pd
+
+ PANDAS_AVAILABLE = True
+except ImportError:
+ PANDAS_AVAILABLE = False
+
+
+@requires_pandas
+def function_requiring_pandas():
+ return "Pandas is available"
+
+
+def test_requires_pandas_with_pandas():
+ """Test that the function executes normally when Pandas is available."""
+ if PANDAS_AVAILABLE:
+ assert function_requiring_pandas() == "Pandas is available"
+
+
+def test_requires_pandas_without_pandas(monkeypatch):
+ """Test that the decorator raises ImportError when Pandas is not available."""
+ # Simulate Pandas being unavailable
+ monkeypatch.setattr("datacommons_client.utils.decorators.pd", None)
+ with pytest.raises(ImportError, match="Pandas is required for this method"):
+ function_requiring_pandas()
+
+
+def test_importerror_handling(monkeypatch):
+ """Test that the ImportError block is executed when Pandas is not installed."""
+
+ # Simulate pandas not being available
+ with mock.patch.dict("sys.modules", {"pandas": None}):
+ import importlib
+
+ # Reload the module so that a new check of Pandas is performed
+ import datacommons_client.utils.decorators
+ importlib.reload(datacommons_client.utils.decorators)
+
+ # Ensure pd is set to None
+ assert datacommons_client.utils.decorators.pd is None
diff --git a/datacommons_client/tests/test_names.py b/datacommons_client/tests/test_names.py
new file mode 100644
index 00000000..311b094a
--- /dev/null
+++ b/datacommons_client/tests/test_names.py
@@ -0,0 +1,72 @@
+from datacommons_client.models.node import Node
+from datacommons_client.utils.names import extract_name_from_english_name_property
+from datacommons_client.utils.names import extract_name_from_property_with_language
+
+
+def test_extract_name_from_english_name_property_with_list():
+ """Test extracting name from a list of Nodes."""
+ properties = [Node(value="Test Name")]
+ result = extract_name_from_english_name_property(properties)
+ assert result == "Test Name"
+
+
+def test_extract_name_from_english_empty_list():
+ """Test extracting name from an empty list."""
+ result = extract_name_from_english_name_property([])
+ assert result == ""
+
+
+def test_extract_name_from_english_not_list():
+ """Test extracting name from a single Node (not in a list)."""
+ property_node = Node(value="Single Node Name")
+ result = extract_name_from_english_name_property(property_node)
+ assert result == "Single Node Name"
+
+
+def test_extract_name_from_property_with_language_match():
+ """Test extracting name when desired language is present."""
+ properties = [
+ Node(value="Nombre@es"),
+ Node(value="Name@en"),
+ ]
+ result = extract_name_from_property_with_language(properties,
+ language="es",
+ fallback_language="en")
+ assert result[0] == "Nombre"
+ assert result[1] == "es"
+
+
+def test_extract_name_from_property_with_language_fallback():
+ """Test fallback to English when desired language is not found."""
+ properties = [
+ Node(value="Name@en"),
+ Node(value="Nom@fr"),
+ Node(value="Nome@it"),
+ ]
+ result = extract_name_from_property_with_language(properties,
+ language="de",
+ fallback_language="it")
+ assert result[0] == "Nome"
+ assert result[1] == "it"
+
+
+def test_extract_name_from_property_with_language_no_fallback():
+ """Test no result when language is not found and fallback is disabled."""
+ properties = [
+ Node(value="Name@en"),
+ Node(value="Nom@fr"),
+ ]
+ result = extract_name_from_property_with_language(properties, language="de")
+ assert result[0] is None
+ assert result[1] is None
+
+
+def test_extract_name_from_property_without_language_tags():
+ """Test that properties without language tags are skipped."""
+ properties = [
+ Node(value="Plain str"),
+ Node(value="Name@en"),
+ ]
+ result = extract_name_from_property_with_language(properties, language="en")
+ assert result[0] == "Name"
+ assert result[1] == "en"
diff --git a/datacommons_client/tests/test_utils.py b/datacommons_client/tests/test_utils.py
new file mode 100644
index 00000000..166b7ef7
--- /dev/null
+++ b/datacommons_client/tests/test_utils.py
@@ -0,0 +1,40 @@
+from datacommons_client.utils.data_processing import group_variables_by_entity
+
+
+def test_group_variables_by_entity_basic():
+ """Test grouping with simple variable-entity mapping."""
+ input_data = {
+ "var1": ["ent1", "ent2"],
+ "var2": ["ent2", "ent3"],
+ "var3": ["ent1"],
+ }
+ expected_output = {
+ "ent1": ["var1", "var3"],
+ "ent2": ["var1", "var2"],
+ "ent3": ["var2"],
+ }
+
+ result = group_variables_by_entity(input_data)
+ assert result == expected_output
+
+
+def test_group_variables_by_entity_duplicate_entities():
+ """Test grouping when a variable has duplicate entities."""
+ input_data = {
+ "var1": ["ent1", "ent1", "ent2"],
+ }
+ result = group_variables_by_entity(input_data)
+ assert result["ent1"].count("var1") == 2 # duplicates are preserved
+ assert "ent2" in result
+ assert result["ent2"] == ["var1"]
+
+
+def test_group_variables_by_entity_preserves_order():
+ """Test if the order of variables is preserved in the resulting entity lists."""
+ input_data = {
+ "var1": ["ent1"],
+ "var2": ["ent1"],
+ "var3": ["ent1"],
+ }
+ result = group_variables_by_entity(input_data)
+ assert result["ent1"] == ["var1", "var2", "var3"]
diff --git a/datacommons_client/tests/utils/test_graph.py b/datacommons_client/tests/utils/test_graph.py
new file mode 100644
index 00000000..0a922eff
--- /dev/null
+++ b/datacommons_client/tests/utils/test_graph.py
@@ -0,0 +1,273 @@
+from collections import defaultdict
+from unittest.mock import MagicMock
+
+from datacommons_client.models.node import Node
+from datacommons_client.utils.graph import _assemble_tree
+from datacommons_client.utils.graph import _fetch_relationship_uncached
+from datacommons_client.utils.graph import _postorder_nodes
+from datacommons_client.utils.graph import build_graph_map
+from datacommons_client.utils.graph import build_relationship_tree
+from datacommons_client.utils.graph import fetch_relationship_lru
+from datacommons_client.utils.graph import flatten_relationship
+
+
+def test_fetch_parents_uncached_returns_data():
+ """Test _fetch_parents_uncached delegates to endpoint correctly."""
+ endpoint = MagicMock()
+ endpoint.fetch_place_parents.return_value.get.return_value = [
+ Node(dcid="parent1", name="Parent 1", types=["Country"])
+ ]
+
+ result = _fetch_relationship_uncached(endpoint,
+ "test_dcid",
+ contained_type=None,
+ relationship="parents")
+ assert isinstance(result, list)
+ assert result[0].dcid == "parent1"
+ endpoint.fetch_place_parents.assert_called_once_with(
+ "test_dcid",
+ as_dict=False,
+ )
+
+
+def test_fetch_relationship_lru_caches_results():
+ """Test fetch_relationship_lru uses LRU cache and returns list."""
+ endpoint = MagicMock()
+ endpoint.fetch_place_parents.return_value.get.return_value = [
+ Node(dcid="parentX", name="Parent X", types=["Region"])
+ ]
+
+ result1 = fetch_relationship_lru(endpoint,
+ "nodeA",
+ contained_type=None,
+ relationship="parents")
+ result2 = fetch_relationship_lru(endpoint,
+ "nodeA",
+ contained_type=None,
+ relationship="parents")
+ fetch_relationship_lru(endpoint,
+ "nodeA",
+ contained_type=None,
+ relationship="parents")
+
+ assert isinstance(result1, list)
+ assert result1[0].dcid == "parentX"
+ assert result1 == result2
+ assert endpoint.fetch_place_parents.call_count == 1
+
+
+def test_build_ancestry_map_linear_tree():
+ """A -> B -> C"""
+
+ def fetch_mock(dcid):
+ return {
+ "C": [Node(dcid="B", name="Node B", types=["Type"])],
+ "B": [Node(dcid="A", name="Node A", types=["Type"])],
+ "A": [],
+ }.get(dcid, [])
+
+ root, graph = build_graph_map("C", fetch_mock, max_workers=2)
+
+ assert root == "C"
+ assert set(graph.keys()) == {"C", "B", "A"}
+ assert graph["C"][0].dcid == "B"
+ assert graph["B"][0].dcid == "A"
+ assert graph["A"] == []
+
+
+def test_build_ancestry_map_branching_graph():
+ r"""
+ Graph:
+ F
+ / \
+ D E
+ / \ /
+ B C
+ \/
+ A
+ """
+
+ def fetch_mock(dcid):
+ return {
+ "A": (Node(dcid="B", name="Node B",
+ types=["Type"]), Node(dcid="C",
+ name="Node C",
+ types=["Type"])),
+ "B": (Node(dcid="D", name="Node D", types=["Type"]),),
+ "C": (Node(dcid="D", name="Node D",
+ types=["Type"]), Node(dcid="E",
+ name="Node E",
+ types=["Type"])),
+ "D": (Node(dcid="F", name="Node F", types=["Type"]),),
+ "E": (Node(dcid="F", name="Node F", types=["Type"]),),
+ "F": tuple(),
+ }.get(dcid, tuple())
+
+ root, ancestry = build_graph_map("A", fetch_mock, max_workers=4)
+
+ assert root == "A"
+ assert set(ancestry.keys()) == {"A", "B", "C", "D", "E", "F"}
+ assert [p.dcid for p in ancestry["A"]] == ["B", "C"] # A has two parents
+ assert [p.dcid for p in ancestry["B"]] == ["D"] # B has one parent
+ assert [p.dcid for p in ancestry["C"]] == ["D", "E"] # C has two parents
+ assert [p.dcid for p in ancestry["D"]] == ["F"] # D has one parent
+ assert [p.dcid for p in ancestry["E"]] == ["F"] # E has one parent
+ assert ancestry["F"] == [] # F has no parents
+
+
+def test_build_ancestry_map_cycle_detection():
+ """
+ Graph with a cycle:
+ A -> B -> C -> A
+ (Should not loop infinitely)
+ """
+
+ call_count = defaultdict(int)
+
+ def fetch_mock(dcid):
+ call_count[dcid] += 1
+ return {
+ "A": (Node(dcid="B", name="B", types=["Type"]),),
+ "B": (Node(dcid="C", name="C", types=["Type"]),),
+ "C": (Node(dcid="A", name="A", types=["Type"]),), # Cycle back to A
+ }.get(dcid, tuple())
+
+ root, ancestry = build_graph_map("A", fetch_mock, max_workers=2)
+
+ assert root == "A" # Since we start from A
+ assert set(ancestry.keys()) == {"A", "B", "C"}
+ assert [p.dcid for p in ancestry["A"]] == ["B"] # A points to B
+ assert [p.dcid for p in ancestry["B"]] == ["C"] # B points to C
+ assert [p.dcid for p in ancestry["C"]] == ["A"
+ ] # C points back to A but it's ok
+
+ # Check that each node was fetched only once (particularly for A to avoid infinite loop)
+ assert call_count["A"] == 1
+ assert call_count["B"] == 1
+ assert call_count["C"] == 1
+
+
+def test_postorder_nodes_simple_graph():
+ """Test postorder traversal on a simple graph."""
+ ancestry = {
+ "C": [Node(dcid="B", name="B", types=["Type"])],
+ "B": [Node(dcid="A", name="A", types=["Type"])],
+ "A": [],
+ }
+
+ order = _postorder_nodes("C", ancestry)
+ assert order == ["A", "B", "C"]
+
+ new_order = _postorder_nodes("B", ancestry)
+ assert new_order == ["A", "B"]
+
+
+def test_postorder_nodes_ignores_disconnected():
+ """
+ Graph:
+ A <- B <- C
+ D (disconnected)
+ """
+ graph = {
+ "A": [Node(dcid="B", name="B", types=["Type"])],
+ "B": [Node(dcid="C", name="C", types=["Type"])],
+ "C": [],
+ "D": [Node(dcid="Z", name="Z", types=["Type"])],
+ }
+ order = _postorder_nodes("A", graph)
+ assert order == ["C", "B", "A"]
+ assert "D" not in order
+
+
+def test_assemble_tree_creates_nested_structure():
+ """Test _assemble_tree creates a nested structure."""
+ ancestry = {
+ "C": [Node(dcid="B", name="Node B", types=["Type"])],
+ "B": [Node(dcid="A", name="Node A", types=["Type"])],
+ "A": [],
+ }
+ postorder = ["A", "B", "C"]
+ tree = _assemble_tree(postorder, ancestry, relationship_key="parents")
+
+ assert tree["dcid"] == "C"
+ assert tree["parents"][0]["dcid"] == "B"
+ assert tree["parents"][0]["parents"][0]["dcid"] == "A"
+
+
+def test_postorder_nodes_ignores_unreachable_nodes():
+ """
+ Graph:
+ A → B → C
+ Ancestry map also includes D (unconnected)
+ """
+ ancestry = {
+ "A": [Node(dcid="B", name="B", types=["Type"])],
+ "B": [Node(dcid="C", name="C", types=["Type"])],
+ "C": [],
+ "D": [Node(dcid="X", name="X", types=["Type"])],
+ }
+
+ postorder = _postorder_nodes("A", ancestry)
+
+ # Only nodes reachable from A should be included
+ assert postorder == ["C", "B", "A"]
+ assert "D" not in postorder
+
+
+def test_assemble_tree_shared_parent_not_duplicated():
+ """
+ Structure:
+ A → C
+ B → C
+ Both A and B have same parent C
+ """
+
+ ancestry = {
+ "A": [Node(dcid="C", name="C name", types=["City"])],
+ "B": [Node(dcid="C", name="C name", types=["City"])],
+ "C": [],
+ }
+
+ postorder = ["C", "A", "B"] # C first to allow bottom-up build
+ tree = _assemble_tree(postorder, ancestry, relationship_key="parents")
+
+ assert tree["dcid"] == "B"
+ assert len(tree["parents"]) == 1
+ assert tree["parents"][0]["dcid"] == "C"
+
+ # Confirm C only appears once
+ assert tree["parents"][0] is not None
+ assert tree["parents"][0]["name"] == "C name"
+
+
+def test_build_ancestry_tree_nested_output():
+ """Test build_ancestry_tree creates a nested structure."""
+ ancestry = {
+ "C": [Node(dcid="B", name="B", types=["Type"])],
+ "B": [Node(dcid="A", name="A", types=["Type"])],
+ "A": [],
+ }
+
+ tree = build_relationship_tree("C", ancestry, relationship_key="parents")
+
+ assert tree["dcid"] == "C"
+ assert tree["parents"][0]["dcid"] == "B"
+ assert tree["parents"][0]["parents"][0]["dcid"] == "A"
+
+
+def test_flatten_ancestry_deduplicates():
+ """Test flatten_ancestry deduplicates parents."""
+
+ ancestry = {
+ "X": [Node(dcid="A", name="A", types=["Country"])],
+ "Y": [
+ Node(dcid="A", name="A", types=["Country"]),
+ Node(dcid="B", name="B", types=["City"])
+ ],
+ }
+
+ flat = flatten_relationship(ancestry)
+
+ assert {"dcid": "A", "name": "A", "types": ["Country"]} in flat
+ assert {"dcid": "B", "name": "B", "types": ["City"]} in flat
+ assert len(flat) == 2
diff --git a/datacommons_client/utils/__init__.py b/datacommons_client/utils/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/datacommons_client/utils/context.py b/datacommons_client/utils/context.py
new file mode 100644
index 00000000..c76944c5
--- /dev/null
+++ b/datacommons_client/utils/context.py
@@ -0,0 +1,56 @@
+# Copyright 2025 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from contextlib import contextmanager
+from contextvars import ContextVar
+from typing import Generator
+
+_API_KEY_CONTEXT_VAR: ContextVar[str | None] = ContextVar("api_key",
+ default=None)
+
+
+@contextmanager
+def use_api_key(api_key: str | None) -> Generator[None, None, None]:
+ """Context manager to set the API key for the current execution context.
+
+ If api_key is None or empty, this context manager does nothing, allowing
+ the underlying client to use its default API key.
+
+ Args:
+ api_key: The API key to use. If None or empty, no change is made.
+
+ Example:
+ from datacommons_client import use_api_key
+ # ...
+ client = DataCommonsClient(api_key="default-key")
+
+ # Uses "default-key"
+ client.observation.fetch(...)
+
+ with use_api_key("temp-key"):
+ # Uses "temp-key"
+ client.observation.fetch(...)
+
+ # Back to "default-key"
+ client.observation.fetch(...)
+ """
+ if not api_key:
+ yield
+ return
+
+ token = _API_KEY_CONTEXT_VAR.set(api_key)
+ try:
+ yield
+ finally:
+ _API_KEY_CONTEXT_VAR.reset(token)
diff --git a/datacommons_client/utils/data_processing.py b/datacommons_client/utils/data_processing.py
new file mode 100644
index 00000000..5e1f6bf3
--- /dev/null
+++ b/datacommons_client/utils/data_processing.py
@@ -0,0 +1,198 @@
+from dataclasses import asdict
+import json
+from typing import Any, Dict, List
+
+from datacommons_client.models.base import ArcLabel
+from datacommons_client.models.base import facetID
+from datacommons_client.models.base import NodeDCID
+from datacommons_client.models.base import Property
+from datacommons_client.models.node import Arcs
+from datacommons_client.models.node import FlattenedArcsMapping
+from datacommons_client.models.node import FlattenedPropertiesMapping
+from datacommons_client.models.node import Name
+from datacommons_client.models.node import Node
+from datacommons_client.models.node import NodeGroup
+from datacommons_client.models.node import Properties
+from datacommons_client.models.observation import Facet
+from datacommons_client.models.observation import ObservationRecord
+from datacommons_client.models.observation import ObservationRecords
+from datacommons_client.models.observation import OrderedFacets
+from datacommons_client.models.observation import VariableByEntity
+
+
+def unpack_arcs(arcs: Dict[ArcLabel, NodeGroup]) -> dict[Property, list[Node]]:
+ """Simplify the 'arcs' structure."""
+ # Return dictionary of property nodes
+ return {
+ prop: getattr(arc_data, "nodes", []) for prop, arc_data in arcs.items()
+ }
+
+
+def flatten_properties(
+ data: Dict[NodeDCID, Arcs | Properties]
+) -> FlattenedPropertiesMapping | FlattenedArcsMapping:
+ """
+ Flatten the properties of a node response.
+
+ Processes a dictionary of node responses, extracting and
+ simplifying their properties and arcs into a flattened dictionary.
+
+ Args:
+ data (Dict[NodeDCID, Arcs | Properties]):
+ The input dictionary containing node responses. Each node maps to
+ a dictionary with potential "arcs" and "properties" keys.
+
+ Returns:
+ FlattenedPropertiesMapping | FlattenedArcsMapping:
+ A flattened dictionary where keys are node identifiers, and values
+ are the simplified properties or nodes.
+ """
+ if not data:
+ return FlattenedPropertiesMapping.model_validate({})
+
+ first_node = next(iter(data.values()))
+ is_properties = isinstance(first_node, Properties)
+ mapping_cls = FlattenedPropertiesMapping if is_properties else FlattenedArcsMapping
+
+ # Store simplified properties
+ items = {}
+ for node_id, node_data in data.items():
+ if is_properties:
+ props = getattr(node_data, "properties", None)
+ if props:
+ items[node_id] = props
+ else:
+ arcs = getattr(node_data, "arcs", None)
+ if arcs:
+ items[node_id] = unpack_arcs(arcs)
+
+ return mapping_cls.model_validate(items)
+
+
+def extract_observations(
+ variable: str, entity: str, entity_data: OrderedFacets,
+ facet_metadata: dict[facetID, Facet]) -> list[ObservationRecord]:
+ """
+ Extracts observations for a given variable, entity, and its data.
+
+ Args:
+ variable (str): The variable name.
+ entity (str): The entity name.
+ entity_data (OrderedFacets): Data for the entity, including ordered facets.
+ facet_metadata (dict[facetID, Facet]): Metadata for facets.
+
+ Returns:
+ list[dict]: A list of observation records.
+ """
+ observations = []
+ for facet in entity_data.orderedFacets:
+ for observation in facet.observations:
+ observations.append(
+ ObservationRecord.model_validate({
+ "date": observation.date,
+ "entity": entity,
+ "variable": variable,
+ "value": observation.value,
+ "facetId": facet.facetId,
+ **facet_metadata.get(facet.facetId, Facet()).to_dict(),
+ }))
+
+ return observations
+
+
+def observations_as_records(data: VariableByEntity,
+ facets: dict[facetID, Facet]) -> ObservationRecords:
+ """
+ Converts observation data into a list of records.
+
+ Args:
+ data (VariableByEntity): A mapping of variables to entities and their data.
+ facets (dict): Facet metadata for the observations.
+
+ Returns:
+ ObservationRecords: A flattened list of observation records.
+ """
+
+ records = []
+ for variable, entities in data.items():
+ for entity, entity_data in entities.items():
+ for record in extract_observations(
+ variable=variable,
+ entity=entity,
+ entity_data=entity_data,
+ facet_metadata=facets,
+ ):
+ records.append(record)
+
+ return ObservationRecords.model_validate(records)
+
+
+def group_variables_by_entity(
+ data: dict[str, list[str]]) -> dict[str, list[str]]:
+ """Groups variables by the entities they are associated with.
+ Takes a dictionary mapping statistical variable DCIDs to a list of entity DCIDs,
+ and returns a new dictionary mapping each entity DCID to a list of statistical
+ variables available for that entity.
+ Args:
+ data: A dictionary where each key is a variable DCID and the value is a list
+ of entity DCIDs that have observations for that variable.
+ Returns:
+ A dictionary where each key is an entity DCID and the value is a list of
+ variable DCIDs available for that entity.
+ """
+ result: dict[str, list[str]] = {}
+ for variable, entities in data.items():
+ for entity in entities:
+ result.setdefault(entity, []).append(variable)
+ return result
+
+
+class SerializableMixin:
+ """Provides serialization methods for the Response dataclasses."""
+
+ def to_dict(self, exclude_none: bool = True) -> Dict[str, Any]:
+ """Converts the instance to a dictionary.
+
+ Args:
+ exclude_none: If True, only include non-empty values in the response.
+
+ Returns:
+ Dict[str, Any]: The dictionary representation of the instance.
+ """
+
+ def _remove_none(data: Any) -> Any:
+ """Recursively removes None or empty values from a dictionary or list."""
+ if isinstance(data, dict):
+ return {k: _remove_none(v) for k, v in data.items() if v is not None}
+ elif isinstance(data, list):
+ return [_remove_none(item) for item in data]
+ return data
+
+ result = asdict(self)
+ return _remove_none(result) if exclude_none else result
+
+ def to_json(self, exclude_none: bool = True) -> str:
+ """Converts the instance to a JSON string.
+
+ Args:
+ exclude_none: If True, only include non-empty values in the response.
+
+ Returns:
+ str: The JSON string representation of the instance.
+ """
+ return json.dumps(self.to_dict(exclude_none=exclude_none), indent=2)
+
+
+def flatten_names_dictionary(names_dict: dict[str, Name]) -> dict[str, str]:
+ """
+ Flattens a dictionary which contains Name objects into a flattened dictionary
+ with DCIDs as keys and names as values.
+
+ Args:
+ names_dict (dict[str, Name]): The input dictionary to flatten.
+
+ Returns:
+ dict[str, str]: A flattened dictionary with DCIDs as keys and names as values.
+ """
+
+ return {dcid: name.to_dict()['value'] for dcid, name in names_dict.items()}
diff --git a/datacommons_client/utils/dataframes.py b/datacommons_client/utils/dataframes.py
new file mode 100644
index 00000000..3eb634f3
--- /dev/null
+++ b/datacommons_client/utils/dataframes.py
@@ -0,0 +1,95 @@
+from datacommons_client.endpoints.node import NodeEndpoint
+from datacommons_client.utils.data_processing import flatten_names_dictionary
+
+try:
+ import pandas as pd
+except ImportError:
+ pd = None
+
+from datacommons_client.utils.decorators import requires_pandas
+
+
+@requires_pandas
+def add_entity_names_to_observations_dataframe(
+ endpoint: NodeEndpoint,
+ observations_df: "pd.DataFrame", # type: ignore[reportInvalidTypeForm]
+ entity_columns: str | list[str],
+) -> "pd.DataFrame": # type: ignore[reportInvalidTypeForm]
+ """
+ Adds entity names to the observations DataFrame.
+
+ Args:
+ endpoint (NodeEndpoint): The NodeEndpoint instance for fetching entity names.
+ observations_df (dict): The DataFrame containing observations.
+ entity_columns (str | list[str]): The column(s) containing entity DCIDs.
+ """
+
+ # Guard against empty DataFrame
+ if observations_df.empty:
+ return observations_df
+
+ if not isinstance(entity_columns, list):
+ entity_columns = [entity_columns]
+
+ for entity_column in entity_columns:
+ if entity_column not in observations_df.columns:
+ raise ValueError(
+ "The specified entity column does not exist in the DataFrame.")
+
+ # Get unique entity DCIDs from the DataFrame
+ unique_values = observations_df[entity_column].dropna().unique().tolist()
+
+ # Guard against empty unique values
+ if not unique_values:
+ continue
+
+ # Fetch entity names from the endpoint
+ response = endpoint.fetch_entity_names(entity_dcids=unique_values)
+
+ # Flatten the response to get a dictionary of names
+ names = flatten_names_dictionary(response)
+
+ # Insert the names into a column next to the entity column
+ name_column = f"{entity_column}_name"
+ if name_column not in observations_df.columns:
+ observations_df.insert(
+ loc=observations_df.columns.get_loc(entity_column) + 1,
+ column=name_column,
+ value=observations_df[entity_column].map(names),
+ )
+
+ return observations_df
+
+
+@requires_pandas
+def add_property_constraints_to_observations_dataframe(
+ endpoint: NodeEndpoint,
+ observations_df: "pd.DataFrame", # type: ignore[reportInvalidTypeForm]
+) -> "pd.DataFrame": # type: ignore[reportInvalidTypeForm]
+ """
+ Adds property constraint dcids and names to the observations DataFrame.
+
+ Args:
+ endpoint (NodeEndpoint): The NodeEndpoint instance for fetching entity names.
+ observations_df (dict): The DataFrame containing observations.
+ """
+
+ # Guard against empty DataFrame
+ if observations_df.empty:
+ return observations_df
+
+ # Get constraints
+ constraints_data = endpoint.fetch_statvar_constraints(
+ variable_dcids=observations_df.variable.unique().tolist())
+
+ for statvar, constraints in constraints_data.items():
+ for constraint in constraints:
+ # Fill the columns with the corresponding values
+ observations_df.loc[observations_df.variable == statvar,
+ constraint.constraintId] = constraint.valueId
+
+ observations_df.loc[observations_df.variable == statvar,
+ constraint.constraintId +
+ "_name"] = constraint.valueName
+
+ return observations_df
diff --git a/datacommons_client/utils/decorators.py b/datacommons_client/utils/decorators.py
new file mode 100644
index 00000000..d27da444
--- /dev/null
+++ b/datacommons_client/utils/decorators.py
@@ -0,0 +1,18 @@
+from functools import wraps
+
+try:
+ import pandas as pd
+except ImportError:
+ pd = None
+
+
+def requires_pandas(func):
+ """Decorator to check if Pandas is available before executing a method."""
+
+ @wraps(func)
+ def wrapper(*args, **kwargs):
+ if pd is None:
+ raise ImportError("Pandas is required for this method")
+ return func(*args, **kwargs)
+
+ return wrapper
diff --git a/datacommons_client/utils/error_handling.py b/datacommons_client/utils/error_handling.py
new file mode 100644
index 00000000..4a7f89ce
--- /dev/null
+++ b/datacommons_client/utils/error_handling.py
@@ -0,0 +1,89 @@
+from typing import Optional
+
+from requests import Response
+
+
+class DataCommonsError(Exception):
+ """Base exception for all Data Commons-related errors."""
+
+ default_message = "An error occurred getting data from Data Commons API."
+
+ def __init__(self, message: Optional[str] = None):
+ """Initializes a DataCommonsError with a default or custom message."""
+ super().__init__(message or self.default_message)
+
+
+class APIError(DataCommonsError):
+ """Represents an error interacting with Data Commons API."""
+
+ default_message = "An API error occurred."
+
+ def __init__(
+ self,
+ response: Optional[Response] = None,
+ message: Optional[str] = None,
+ ):
+ """Initializes an APIError.
+
+ Args:
+ response (Optional[Response]): The response, if available.
+ message (Optional[str]): A descriptive error message.
+ """
+ super().__init__(message or self.default_message)
+ self.response = response
+ self.request = getattr(response, "request", None)
+ self.status_code = getattr(response, "status_code", None)
+
+ def __str__(self) -> str:
+ """Returns a detailed string representation of the error.
+
+ Returns:
+ str: A string describing the error, including the request URL if available.
+ """
+
+ details = f"\n{self.args[0]}"
+ if self.status_code:
+ details += f"\nStatus Code: {self.status_code}"
+ if getattr(self.request, "url", None):
+ details += f"\nRequest URL: {self.request.url}"
+ if getattr(self.response, "text", None):
+ details += f"\nResponse: {self.response.text}"
+
+ return details
+
+
+class DCConnectionError(APIError):
+ """Raised for network-related errors in the Data Commons API."""
+
+ default_message = (
+ "A network error occurred while connecting to the Data Commons API.")
+
+
+class DCStatusError(APIError):
+ """Raised for non-2xx HTTP status code errors in the Data Commons API."""
+
+ default_message = "The Data Commons API returned a non-2xx status code."
+
+
+class DCAuthenticationError(APIError):
+ """Raised for 401 Unauthorized errors in the Data Commons API."""
+
+ default_message = "Authentication failed. Please check your API key."
+
+
+class InvalidDCInstanceError(DataCommonsError):
+ """Raised when an invalid Data Commons instance is provided."""
+
+ default_message = "The specified Data Commons instance is invalid."
+
+
+class InvalidObservationSelectError(DataCommonsError):
+ """Raised when an invalid ObservationSelect field is provided."""
+
+ default_message = "The ObservationSelect field is invalid."
+
+
+class NoDataForPropertyError(DataCommonsError):
+ """Raised when there is no data that meets the specified property filters."""
+
+ default_message = "No available data for the specified property filters."
diff --git a/datacommons_client/utils/graph.py b/datacommons_client/utils/graph.py
new file mode 100644
index 00000000..db636b5d
--- /dev/null
+++ b/datacommons_client/utils/graph.py
@@ -0,0 +1,266 @@
+from collections import deque
+from concurrent.futures import FIRST_COMPLETED
+from concurrent.futures import Future
+from concurrent.futures import ThreadPoolExecutor
+from concurrent.futures import wait
+import contextvars
+from functools import lru_cache
+from typing import Callable, Literal, Optional, TypeAlias
+
+from datacommons_client.models.node import Node
+
+GRAPH_MAX_WORKERS = 10
+
+RelationMap: TypeAlias = dict[str, list[Node]]
+AncestorsMap = RelationMap
+DescendantsMap = RelationMap
+
+# -- -- Fetch tools -- --
+
+
+def _fetch_relationship_uncached(
+ endpoint,
+ dcid: str,
+ contained_type: str | None,
+ relationship: Literal["parents", "children"],
+) -> list[Node]:
+ """Fetches the immediate parents/children of a given DCID from the endpoint, without caching.
+
+ This function performs a direct, uncached call to the API. It exists
+ primarily to serve as the internal, cache-free fetch use by functions with lru.
+
+ By isolating the pure fetch logic here, we ensure that caching is handled separately
+ and cleanly via `@lru_cache`, which requires its wrapped
+ function to be deterministic and side-effect free.
+
+ Args:
+ endpoint: A client object with a `fetch_entity_parents` and `fetch_entity_children` method.
+ dcid (str): The entity ID for which to fetch parents.
+ contained_type (str): The type of the entity to be fetched.
+ relationship (str): The type of relationship to fetch, either "parents" or "children".
+ Returns:
+ A list of Node objects.
+ """
+
+ if relationship == "parents":
+ result = endpoint.fetch_place_parents(dcid, as_dict=False).get(dcid, [])
+
+ else:
+ result = endpoint.fetch_place_children(dcid,
+ children_type=contained_type,
+ as_dict=False).get(dcid, [])
+
+ return result if isinstance(result, list) else [result]
+
+
+@lru_cache(maxsize=512)
+def fetch_relationship_lru(
+ endpoint,
+ dcid: str,
+ contained_type: str | None,
+ relationship: Literal["parents", "children"],
+) -> list[Node]:
+ """Fetches parents of a DCID using an LRU cache for improved performance.
+ Args:
+ endpoint: A Node client object.
+ dcid (str): The entity ID to fetch parents/children for.
+ contained_type (str): The type of the entity to be fetched.
+ relationship (str): The type of relationship to fetch, either "parents" or "children".
+ Returns:
+ A list of `Node` objects corresponding to the entity's parents or children.
+ """
+ return _fetch_relationship_uncached(
+ endpoint=endpoint,
+ dcid=dcid,
+ contained_type=contained_type,
+ relationship=relationship,
+ )
+
+
+# -- -- Ancestry tools -- --
+
+
+def build_graph_map(
+ root: str,
+ fetch_fn: Callable[..., tuple[Node, ...]],
+ *,
+ max_workers: Optional[int] = GRAPH_MAX_WORKERS,
+) -> tuple[str, RelationMap]:
+ """Constructs a complete ancestry/descendancy map for the root node using parallel
+ Breadth-First Search (BFS).
+
+ Traverses the graph from the root node, discovering all parent/children
+ relationships (depending on the fetch_fn) by fetching in parallel.
+
+ Args:
+ root (str): The DCID of the root entity to start from.
+ fetch_fn (Callable): A function that takes a DCID and returns Node tuples.
+ max_workers (Optional[int]): Max number of threads to use for parallel fetching.
+ Optional, defaults to `PARENTS_MAX_WORKERS`.
+
+ Returns:
+ A tuple containing:
+ - The original root DCID.
+ - A dictionary mapping each DCID to a Node list.
+ """
+ graph_map: RelationMap = {}
+ visited: set[str] = set()
+ in_progress: dict[str, Future] = {}
+
+ original_root = root
+
+ ctx = contextvars.copy_context()
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
+ queue = deque([root])
+
+ # Standard BFS loop, but fetches are executed in parallel threads
+ while queue or in_progress:
+ # Submit fetch tasks for all nodes in the queue
+ while queue:
+ dcid = queue.popleft()
+ # Check if the node has already been visited or is in progress
+ if dcid not in visited and dcid not in in_progress:
+ # Submit the fetch task
+ in_progress[dcid] = executor.submit(ctx.run, fetch_fn, dcid=dcid)
+
+ # Check if any futures are still in progress
+ if not in_progress:
+ continue
+
+ # Wait for at least one future to complete
+ done_futures, _ = wait(in_progress.values(), return_when=FIRST_COMPLETED)
+
+ # Find which DCIDs have completed
+ completed_dcids = [
+ dcid for dcid, future in in_progress.items() if future in done_futures
+ ]
+
+ # Process completed fetches and enqueue any unseen parents
+ for dcid in completed_dcids:
+ future = in_progress.pop(dcid)
+ nodes = list(future.result())
+ graph_map[dcid] = nodes
+ visited.add(dcid)
+
+ for node in nodes:
+ if (node and node.dcid not in visited and
+ node.dcid not in in_progress):
+ queue.append(node.dcid)
+
+ return original_root, graph_map
+
+
+def _postorder_nodes(root: str, graph: RelationMap) -> list[str]:
+ """Generates a postorder list of all nodes reachable from the root.
+
+ Postorder ensures children are processed before their parents. That way the tree
+ is built bottom-up.
+
+ Args:
+ root (str): The root DCID to start traversal from.
+ graph (RelationMap): The ancestry/descendancy map.
+ Returns:
+ A list of DCIDs in postorder (i.e children before parents).
+ """
+ # Initialize stack and postorder list
+ stack, postorder, seen = [root], [], set()
+
+ # Traverse the graph using a stack
+ while stack:
+ node = stack.pop()
+ # Skip if already seen
+ if node in seen:
+ continue
+ seen.add(node)
+ postorder.append(node)
+ # Push all unvisited Nodes onto the stack
+ for relation in graph.get(node, []):
+ if not relation:
+ continue
+ relation_dcid = relation.dcid
+ if relation_dcid not in seen:
+ stack.append(relation_dcid)
+
+ # Reverse to get postorder relative to the adjacency direction
+ return list(reversed(postorder))
+
+
+def _assemble_tree(postorder: list[str], ancestry: RelationMap,
+ relationship_key: str) -> dict:
+ """Builds a nested dictionary tree from a Node list and RelationMa[.
+ Constructs a nested representation of the graph, ensuring that parents/children
+ are embedded after their root Node (which is enabled by postorder).
+ Args:
+ postorder (list[str]): List of node DCIDs in postorder.
+ ancestry (RelationMap): Map from DCID to list of Node objects.
+ relationship_key (str): The key to use for the relationship in the tree.
+ Returns:
+ A nested dictionary representing the ancestry tree rooted at the last postorder node.
+ """
+ tree_cache: dict[str, dict] = {}
+
+ for node in postorder:
+ # Initialize the node dictionary.
+ node_dict = {"dcid": node, "name": None, "type": None, relationship_key: []}
+
+ # For each relationship of the current node, fetch its details and add it to the node_dict.
+ for relationship in ancestry.get(node, []):
+ if not relationship:
+ continue
+ relationship_dcid = relationship.dcid
+ name = relationship.name
+ entity_type = relationship.types
+
+ # If the node is not already in the cache, add it.
+ if relationship_dcid not in tree_cache:
+ tree_cache[relationship_dcid] = {
+ "dcid": relationship_dcid,
+ "name": name,
+ "type": entity_type,
+ relationship_key: [],
+ }
+
+ relationship_node = tree_cache[relationship_dcid]
+
+ # Ensure name/type are up to date (in case of duplicates)
+ relationship_node["name"] = name
+ relationship_node["type"] = entity_type
+ node_dict[relationship_key].append(relationship_node)
+
+ tree_cache[node] = node_dict
+
+ # The root node is the last one in postorder, that's what gets returned
+ return tree_cache[postorder[-1]]
+
+
+def build_relationship_tree(root: str, graph: RelationMap,
+ relationship_key: str) -> dict:
+ """Builds a nested ancestry tree from an ancestry map.
+ Args:
+ root (str): The DCID of the root node.
+ graph (RelationMap): A dictionary mapping DCIDs to lists of Node objects.
+ relationship_key (str): The key to use for the relationship in the tree.
+ Returns:
+ A nested dictionary tree rooted at the specified DCID.
+ """
+ postorder = _postorder_nodes(root, graph)
+ return _assemble_tree(postorder, graph, relationship_key=relationship_key)
+
+
+def flatten_relationship(graph: RelationMap) -> list[dict[str, str]]:
+ """Flattens the RelationMap into a deduplicated list of parent/child records.
+ Args:
+ graph (GraphMap): mapping of DCIDs to lists of Node objects.
+ Returns:
+ A list of dictionaries with keys 'dcid', 'name', and 'type', containing
+ each unique parent/child in the graph.
+ """
+
+ flat: list = []
+ seen: set[str] = set()
+ for relationships in graph.values():
+ for relationship in relationships:
+ if relationship and relationship.dcid not in seen:
+ seen.add(relationship.dcid)
+ flat.append(relationship.to_dict())
+ return flat
diff --git a/datacommons_client/utils/names.py b/datacommons_client/utils/names.py
new file mode 100644
index 00000000..490ae480
--- /dev/null
+++ b/datacommons_client/utils/names.py
@@ -0,0 +1,62 @@
+from typing import Optional
+
+from datacommons_client.models.node import Node
+
+DEFAULT_NAME_PROPERTY: str = "name"
+NAME_WITH_LANGUAGE_PROPERTY: str = "nameWithLanguage"
+DEFAULT_NAME_LANGUAGE: str = "en"
+
+
+def extract_name_from_english_name_property(properties: list | Node) -> str:
+ """
+ Extracts the name from a list of properties with English names.
+ Args:
+ properties (list): A list of properties with English names.
+ Returns:
+ str: The extracted name.
+ """
+ if not properties:
+ return ''
+
+ if isinstance(properties, Node):
+ properties = [properties]
+
+ return properties[0].value
+
+
+def extract_name_from_property_with_language(
+ properties: list,
+ language: str,
+ fallback_language: Optional[str] = None) -> tuple[str | None, str | None]:
+ """
+ Extracts the name from a list of properties with language tags.
+ Args:
+ properties (list): A list of properties with language tags.
+ language (str): The desired language code.
+ fallback_language: If provided, this language will be used as a fallback if the requested
+ language is not available. If not provided, no fallback will be used.
+
+ Returns:
+ tuple[str,str]: A tuple containing the extracted name and its language.
+ """
+ # If a non-English language is requested, unpack the response to get it.
+ fallback_name = None
+
+ # Iterate through the properties to find the name in the specified language
+ for candidate in properties:
+ # If no language is specified, skip the candidate
+ if "@" not in candidate.value:
+ continue
+
+ # Split the candidate value into name and language
+ name, lang = candidate.value.rsplit("@", 1)
+
+ # If the language matches, add the name to the dictionary.
+ if lang == language:
+ return name, lang
+ # If language is 'en', store the name as a fallback
+ if fallback_language and (lang == fallback_language):
+ fallback_name = name
+
+ # If no name was found in the specified language, use the fallback name (if available)
+ return fallback_name, fallback_language if fallback_language else None
diff --git a/datacommons_client/utils/request_handling.py b/datacommons_client/utils/request_handling.py
new file mode 100644
index 00000000..79f2cb80
--- /dev/null
+++ b/datacommons_client/utils/request_handling.py
@@ -0,0 +1,281 @@
+from typing import Any, Dict, Optional
+
+import requests
+from requests import exceptions
+from requests import Response
+
+from datacommons_client.utils.error_handling import APIError
+from datacommons_client.utils.error_handling import DCAuthenticationError
+from datacommons_client.utils.error_handling import DCConnectionError
+from datacommons_client.utils.error_handling import DCStatusError
+from datacommons_client.utils.error_handling import InvalidDCInstanceError
+
+BASE_DC_V2: str = "https://api.datacommons.org/v2"
+CUSTOM_DC_V2: str = "/core/api/v2"
+
+
+def check_instance_is_valid(instance_url: str,
+ api_key: str | None = None) -> str:
+ """Check that the given instance URL points to a valid Data Commons instance.
+
+ This function attempts a GET request against a known node in Data Commons to
+ validate the given instance URL. If the node is found and the response has the
+ expected data, the URL is considered valid.
+
+ If an api_key is provided, it will be included in the request headers.
+
+ Args:
+ instance_url: The Data Commons instance URL to validate.
+ api_key: Optional API key for authentication.
+
+ Returns:
+ The validated instance URL.
+
+ Raises:
+ InvalidDCInstanceError: If the instance URL does not seem to be a valid
+ Data Commons instance.
+ """
+ # Test URL for a known node in Data Commons
+ test_url = f"{instance_url}/node?nodes=country%2FGTM&property=->name"
+
+ headers = {}
+ if api_key:
+ headers["X-API-Key"] = api_key
+
+ try:
+ response = requests.get(test_url, headers=headers)
+ response.raise_for_status()
+ except requests.exceptions.RequestException as exc:
+ raise InvalidDCInstanceError(exc.response) from exc
+
+ data = response.json()
+ if "data" not in data or "country/GTM" not in data["data"]:
+ raise InvalidDCInstanceError(
+ f"{instance_url} is not a valid Data Commons instance.")
+
+ return instance_url
+
+
+def resolve_instance_url(dc_instance: str) -> str:
+ """Resolve the base API URL for a given Data Commons instance.
+
+ If the instance is `datacommons.org`, the default URL is returned. Otherwise,
+ the custom URL is validated via `_check_instance_is_valid`.
+
+ Args:
+ dc_instance: The identifier or domain of the Data Commons instance.
+
+ Returns:
+ The resolved base API URL.
+ """
+ # if https or http included in the string, remove it
+ dc_instance = dc_instance.replace("https://", "").replace("http://", "")
+
+ # If the instance is the default, return the base URL
+ if dc_instance == "datacommons.org":
+ return BASE_DC_V2
+
+ # Otherwise, validate the custom instance URL
+ url = f"https://{dc_instance}{CUSTOM_DC_V2}"
+ return check_instance_is_valid(url)
+
+
+def _send_post_request(url: str, payload: dict[str, Any],
+ headers: dict[str, str]) -> Response:
+ """Send a POST request and handle common HTTP errors with custom exceptions.
+
+ Args:
+ url: The target endpoint URL.
+ payload: The JSON payload to send with the request.
+ headers: The request headers, including authentication.
+
+ Returns:
+ The successful Response object.
+
+ Raises:
+ DCConnectionError: If a network-related error occurs.
+ DCAuthenticationError: If a 401 Unauthorized error is received.
+ DCStatusError: for 500-level HTTP errors.
+ APIError: For other HTTP errors.
+ """
+ try:
+ response = requests.post(url, json=payload, headers=headers)
+ response.raise_for_status()
+ return response
+
+ # if ConnectionError
+ except exceptions.ConnectionError as exc:
+ raise DCConnectionError(exc.response) from exc
+
+ # if HTTPError
+ except exceptions.HTTPError as exc:
+ status_code = (exc.response.status_code
+ if exc.response is not None else None)
+
+ # if 401 Unauthorized
+ if status_code == 401:
+ raise DCAuthenticationError(exc.response) from exc
+
+ # if 500-level HTTP errors
+ if status_code >= 500:
+ raise DCStatusError(exc.response) from exc
+
+ # for other HTTP errors
+ raise APIError(exc.response) from exc
+
+
+def _recursively_merge_dicts(
+ base: dict[str, Any],
+ new: dict[str, Any],
+ keys_to_skip: Optional[set[str]] = None,
+) -> dict[str, Any]:
+ """Recursively merge two dictionaries, skipping specified keys.
+
+ Uses `_merge_values` to handle nested structures and different types.
+
+ Args:
+ base: The base dictionary.
+ new: The dictionary to merge into `base`.
+ keys_to_skip: A set of keys to skip during merging.
+
+ Returns:
+ A new dictionary that is the result of merging `new` into `base`.
+ """
+ if keys_to_skip is None:
+ keys_to_skip = {"nextToken"}
+
+ result = dict(base)
+ for k, v in new.items():
+ # Skip keys that should be excluded from merging
+ if k in keys_to_skip:
+ continue
+ # If the key is already present, merge the values
+ if k in result:
+ result[k] = _merge_values(result[k], v)
+ # Otherwise, add the new key-value pair
+ else:
+ result[k] = v
+ return result
+
+
+def _merge_values(base: Any, new: Any) -> Any:
+ """Merge two values based on their structure using pattern matching.
+
+ - If both are dicts, recursively merge them.
+ - If both are lists, concatenate them.
+ - Otherwise, if they differ, combine into a list. If they are equal, return one of them.
+
+ Args:
+ base: Existing value in the data structure.
+ new: New value to merge in.
+
+ Returns:
+ The merged value.
+ """
+ match base, new:
+ case dict(), dict():
+ return _recursively_merge_dicts(base, new)
+ case list(), list():
+ # Merge two lists by concatenation
+ return base + new
+ case _:
+ # If they are the same, return one. Otherwise, combine into a list.
+ return base if base == new else [base, new]
+
+
+def _fetch_with_pagination(
+ url: str,
+ payload: dict[str, Any],
+ headers: dict[str, str],
+ all_pages: bool = True,
+ next_token: Optional[str] = None,
+) -> dict[str, Any]:
+ """Fetch and (if necessary) merge paginated results from an API.
+
+ Continues fetching pages until `nextToken` is not found or `max_pages` is reached.
+
+ Args:
+ url: The API endpoint URL.
+ payload: The request payload (JSON-serializable).
+ headers: The request headers.
+ all_pages: Whether to fetch all available pages. Defaults to True.
+ next_token: Optionally, the token to fetch the next page of results. Defaults to None.
+
+ Returns:
+ A dictionary containing all merged results from all fetched pages.
+ """
+ combined_results: dict[str, Any] = {}
+
+ # If a next token is provided, use it to fetch the next page
+ if next_token:
+ payload["nextToken"] = next_token
+
+ while True:
+ # Send a POST request and parse the JSON response
+ response = _send_post_request(url, payload, headers)
+
+ try:
+ page_data = response.json()
+ except ValueError:
+ raise APIError(response)
+
+ # Merge current page data into combined results
+ combined_results = _recursively_merge_dicts(combined_results, page_data)
+
+ # Update the payload with the next token
+ next_token = page_data.get("nextToken")
+
+ # Update the payload with the next token
+ payload["nextToken"] = next_token
+
+ # Stop if the user only wants one page or if there is no next token
+ if not all_pages or not next_token:
+ break
+
+ # Add the final next token to the response
+ combined_results["nextToken"] = next_token
+
+ return combined_results
+
+
+def post_request(
+ url: str,
+ payload: dict[str, Any],
+ headers: dict[str, str],
+ *,
+ all_pages: bool = True,
+ next_token: Optional[str] = None,
+) -> Dict[str, Any]:
+ """Send a POST request with optional pagination support and return a DCResponse.
+
+ Args:
+ url: The target endpoint URL.
+ payload: The payload to send with the request.
+ headers: The request headers, including authentication.
+ all_pages: Fetch all available pages automatically. Defaults to True. Set to
+ False to only fetch the first page. In that case, a `nextToken` key in the
+ response will indicate if more pages are available. That token can be used
+ to fetch the next page.
+ next_token: Optionally, the token to fetch the next page of results. Defaults to None.
+ If combined with `all_pages=False`, this token will be used to fetch only the next page.
+ If combined with `all_pages=True`, all remaining pages will be fetched starting from this token.
+
+
+ Returns:
+ A dictionary containing the aggregated API response data.
+
+ Raises:
+ ValueError: If `json` is not a dictionary.
+ """
+ if not isinstance(payload, dict):
+ raise ValueError("Payload must be a dictionary.")
+
+ # Fetch and merge paginated results
+ combined_results = _fetch_with_pagination(url=url,
+ payload=payload,
+ headers=headers,
+ all_pages=all_pages,
+ next_token=next_token)
+
+ # Return the combined results as a dictionary
+ return combined_results
diff --git a/datacommons_pandas/CHANGELOG.md b/datacommons_pandas/CHANGELOG.md
index 42bedc43..9b776927 100644
--- a/datacommons_pandas/CHANGELOG.md
+++ b/datacommons_pandas/CHANGELOG.md
@@ -1,5 +1,15 @@
# Changelog
+## 0.0.4
+
+**Date** - 01/12/2026
+
+**Release Tag** - [pd.0.0.4](https://github.com/datacommonsorg/api-python/releases/tag/pd0.0.4)
+
+**Release Status** - Current head of branch [`master`](https://github.com/datacommonsorg/api-python/tree/master)
+
+Added deprecation notice to legacy `datacommons_pandas` package.
+
## 0.0.3
**Date** - 11/10/2020
diff --git a/datacommons_pandas/README.md b/datacommons_pandas/README.md
index 7db05ea9..218c00d4 100644
--- a/datacommons_pandas/README.md
+++ b/datacommons_pandas/README.md
@@ -1,46 +1 @@
-# Data Commons Pandas API
-
-This is a Python library for creating pandas objects with data in the
-Data Commons Graph.
-
-To get started, install this package from pip.
-
- pip install datacommons_pandas
-
-Once the package is installed, import `datacommons_pandas`.
-
- import datacommons_pandas as dcpd
-
-For more detail on getting started with the API, please visit our
-[API Overview](http://docs.datacommons.org/api/).
-
-When you are ready to use the API, you can refer to `datacommons_pandas/examples` for
-examples on how to use this package to perform various tasks. More tutorials and
-documentation can be found on our [tutorials page](https://datacommons.org/colab)!
-
-## About Data Commons
-
-[Data Commons](https://datacommons.org/) is an open knowledge repository that
-provides a unified view across multiple public data sets and statistics. You can
-view what [datasets](https://datacommons.org/datasets) are currently ingested
-and browse the graph using our [browser](https://browser.datacommons.org/).
-
-## License
-
-Apache 2.0
-
-## Development
-
-Please follow the [Development instructions](../README.md#development).
-
-## Release
-
-Please follow the [Release instructions](../README.md#release).
-
-## Support
-
-For general questions or issues about the API, please open an issue on our
-[issues](https://github.com/datacommonsorg/api-python/issues) page. For all other
-questions, please send an email to `support@datacommons.org`.
-
-**Note** - This is not an officially supported Google product.
+**DEPRECATED: This library has been deprecated. Please migrate to the [datacommons_client](https://pypi.org/project/datacommons-client/) library. For help on translating your requests, see the [Migration guide](https://docs.datacommons.org/api/python/v2/migration.html).**
diff --git a/datacommons_pandas/__init__.py b/datacommons_pandas/__init__.py
deleted file mode 100644
index 353c395d..00000000
--- a/datacommons_pandas/__init__.py
+++ /dev/null
@@ -1,33 +0,0 @@
-# Copyright 2020 Google Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-from datacommons_pandas.df_builder import build_time_series, build_time_series_dataframe, build_multivariate_dataframe
-
-################################ SYMLINK FILES ################################
-# We include symlinks to all user-facing functions from the datacommons pkg. #
-# This is so that users do not need to import both libraries for pd support. #
-# Please keep the below in sync with the __init__.py in the datacommons/ dir #
-# TODO: enforce this. https://github.com/datacommonsorg/api-python/issues/149 #
-##############################################@################################
-# Data Commons SPARQL query support
-from datacommons_pandas.query import query
-
-# Data Commons Python API
-from datacommons_pandas.core import get_property_labels, get_property_values, get_triples
-from datacommons_pandas.places import get_places_in, get_related_places, get_stats
-from datacommons_pandas.populations import get_populations, get_observations, get_pop_obs, get_place_obs
-from datacommons_pandas.stat_vars import get_stat_value, get_stat_series, get_stat_all
-
-# Other utilities
-from datacommons_pandas.utils import set_api_key
diff --git a/datacommons_pandas/core.py b/datacommons_pandas/core.py
deleted file mode 120000
index 15f455cf..00000000
--- a/datacommons_pandas/core.py
+++ /dev/null
@@ -1 +0,0 @@
-../datacommons/core.py
\ No newline at end of file
diff --git a/datacommons_pandas/df_builder.py b/datacommons_pandas/df_builder.py
deleted file mode 100644
index 44dcb69f..00000000
--- a/datacommons_pandas/df_builder.py
+++ /dev/null
@@ -1,317 +0,0 @@
-# Copyright 2020 Google Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-"""Data Commons Pandas API DataFrame Builder Module.
-
-Provides functions for building pandas DataFrames using the Data Commons Graph.
-"""
-
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
-import collections
-import pandas as pd
-import six
-
-import datacommons_pandas.stat_vars as dc
-
-
-def build_time_series(place,
- stat_var,
- measurement_method=None,
- observation_period=None,
- unit=None,
- scaling_factor=None):
- """Constructs a pandas Series with `dates` as the index and corresponding `stat_var` statistics as values.
-
- Args:
- place (`str`): The dcid of Place to query for.
- stat_var (`str`): The dcid of the StatisticalVariable.
- measurement_method (`str`): Optional, the dcid of the preferred
- `measurementMethod` value.
- observation_period (`str`): Optional, the preferred
- `observationPeriod` value.
- unit (`str`): Optional, the dcid of the preferred `unit` value.
- scaling_factor (`int`): Optional, the preferred `scalingFactor` value.
- Returns:
- A pandas Series with Place IDs as the index and observed statistics as
- values, representing a time series satisfying all optional args.
- """
- return pd.Series(
- dc.get_stat_series(place, stat_var, measurement_method,
- observation_period, unit, scaling_factor))
-
-
-def _group_stat_all_by_obs_options(places, stat_vars, keep_series=True):
- """Groups the result of `get_stat_all` by StatVarObservation options for time series or multivariates.
-
- Note that this function does not preserve `(place, stat_var)` pairs that
- yield no data `from get_stat_all`. In the extreme case that there is no
- data for any pairs, raise a ValueError instead of returning an empty dict.
-
- Args:
- places (`str` or `iterable` of `str`): The dcids of Places to query for.
- stat_vars (`Iterable` of `str`): The dcids of the StatisticalVariables.
- keep_series (`boolean`): if True, output time series grouped by
- StatVarObservation options; if False, output latest statistics grouped
- by StatVarObservation options.
- Returns:
- A nested dict mapping each StatisticalVariable in `stat_vars` to its
- StatVarObservation options. In turn, each StatVarObservation option
- maps to a list of rows, one per place, with the place id and stat data.
-
- Raises:
- ValueError: If the payload returned by the Data Commons REST API is
- malformed, or if there is no data for any (Place, StatisticalVariables)
- pair.
- """
- if keep_series:
- if len(stat_vars) != 1:
- raise ValueError(
- 'When `keep_series` is set, only one StatisticalVariable for `stat_vars` is allowed.'
- )
- res = collections.defaultdict(list)
- else:
- res = collections.defaultdict(lambda: collections.defaultdict(list))
-
- stat_all = dc.get_stat_all(places, stat_vars)
- for place, place_data in stat_all.items():
- if not place_data:
- continue
- for stat_var, stat_var_data in place_data.items():
- if not stat_var_data:
- continue
- for source_series in stat_var_data['sourceSeries']:
- series = source_series['val']
- # Convert dict of SVO options into nested tuple (hashable key).
- obs_options = (('measurementMethod',
- source_series.get('measurementMethod')),
- ('observationPeriod',
- source_series.get('observationPeriod')),
- ('unit', source_series.get('unit')),
- ('scalingFactor',
- source_series.get('scalingFactor')))
- if keep_series:
- res[obs_options].append(dict({'place': place}, **series))
- else:
- date = max(series)
- res[stat_var][obs_options].append({
- 'place': place,
- 'date': date,
- 'val': series[date]
- })
- if not res:
- raise ValueError(
- 'No data for any of specified Places and StatisticalVariables.')
- if keep_series:
- return dict(res)
- else:
- return {k: dict(v) for k, v in res.items()}
-
-
-def _time_series_pd_input(places, stat_var):
- """Returns a `list` of `dict` per element of `places` based on the `stat_var`.
-
- Data Commons will pick a set of StatVarObservation options that covers the
- maximum number of queried places. Among ties, Data Commons selects an option
- set with the latest Observation.
-
- Args:
- places (`str` or `iterable` of `str`): The dcids of Places to query for.
- stat_var (`str`): The dcid of the StatisticalVariable.
- Returns:
- A `list` of `dict`, one per element of `places`. Each `dict` consists of
- the time series and place identifier.
-
- Examples:
- >>> _time_series_pd_input(["geoId/29", "geoId/33"], "Count_Person")
- [
- {'2020-03-07': 20, '2020-03-08': 40, 'place': 'geoId/29'},
- {'2020-08-21': 428, '2020-08-22': 429, 'place': 'geoId/33'}
- ]
- """
-
- rows_dict = _group_stat_all_by_obs_options(places, [stat_var],
- keep_series=True)
- most_geos = []
- max_geo_count_so_far = 0
- latest_date = []
- latest_date_so_far = ''
- for options, rows in rows_dict.items():
- current_geos = len(rows)
- if current_geos > max_geo_count_so_far:
- max_geo_count_so_far = current_geos
- most_geos = [options]
- # Reset tiebreaker stats. Recompute after this if-else block.
- latest_date = []
- latest_date_so_far = ''
- elif current_geos == max_geo_count_so_far:
- most_geos.append(options)
- else:
- # Do not compute tiebreaker stats if no change to most_geos.
- # Skip to top of the for loop.
- continue
-
- for row in rows:
- dates = set(row.keys())
- dates.remove('place')
- row_max_date = max(dates)
- if row_max_date > latest_date_so_far:
- latest_date_so_far = row_max_date
- latest_date = [options]
- elif row_max_date == latest_date_so_far:
- latest_date.append(options)
- for options in most_geos:
- if options in latest_date:
- return rows_dict[options]
-
-
-def build_time_series_dataframe(places, stat_var, desc_col=False):
- """Constructs a pandas DataFrame with `places` as the index and dates of the time series as the columns.
-
- To ensure statistics are comparable across all Places, when multiple
- StatVarObservations options are available for Place and StatVar combos,
- Data Commons selects the StatVarObservation options that covers the most
- Places, and breaks ties using the StatVarObservation options that yield
- the latest Observation for any Place.
-
- Args:
- places (`str` or `iterable` of `str`): The dcids of Places to query for.
- stat_var (`str`): The dcid of the StatisticalVariable.
- desc_col: Whether to order columns in descending order.
- Returns:
- A pandas DataFrame with Place IDs as the index, and sorted dates as columns.
- """
- try:
- if isinstance(places, six.string_types):
- places = [places]
- else:
- places = list(places)
- assert all(isinstance(place, six.string_types) for place in places)
- except:
- raise ValueError(
- 'Parameter `places` must be a string object or list-like object of string.'
- )
- if not isinstance(stat_var, six.string_types):
- raise ValueError('Parameter `stat_var` must be a string.')
-
- df = pd.DataFrame.from_records(_time_series_pd_input(places, stat_var))
- df.set_index('place', inplace=True)
- df.sort_index(inplace=True)
- return df[sorted(df.columns, reverse=desc_col)]
-
-
-def _multivariate_pd_input(places, stat_vars):
- """Returns a `list` of `dict` per element of `places` based on the `stat_var`.
-
- Data Commons will pick a set of StatVarObservation options that covers the
- maximum number of queried places. Among ties, Data Commons selects an option
- set with the latest Observation.
-
- Args:
- places (`str` or `iterable` of `str`): The dcids of Places to query for.
- stat_vars (`Iterable` of `str`): The dcids of the StatisticalVariables.
- Returns:
- A `list` of `dict`, one per element of `places`. Each `dict` consists of
- the time series and place identifier.
-
- Examples:
- >>> _multivariate_pd_input(["geoId/29", "geoId/33"],
- ["Count_Person", "Median_Income_Person"])
- [
- {'Count_Person': 20, 'Median_Income_Person': 40, 'place': 'geoId/29'},
- {'Count_Person': 428, 'Median_Income_Person': 429, 'place': 'geoId/33'}
- ]
- """
-
- rows_dict = _group_stat_all_by_obs_options(places,
- stat_vars,
- keep_series=False)
- place2cov = collections.defaultdict(dict) # {geo: {var1: 3, var2: 33}}
-
- for stat_var, candidates_dict in rows_dict.items():
- selected_rows = None
- most_geos = []
- max_geo_count_so_far = 0
- latest_date = []
- latest_date_so_far = ''
- for options, rows in candidates_dict.items():
- current_geos = len(rows)
- if current_geos > max_geo_count_so_far:
- max_geo_count_so_far = current_geos
- most_geos = [options]
- # Reset tiebreaker stats. Recompute after this if-else block.
- latest_date = []
- latest_date_so_far = ''
- elif current_geos == max_geo_count_so_far:
- most_geos.append(options)
- else:
- # Do not compute tiebreaker stats if not in most_geos.
- continue
-
- for row in rows:
- row_date = row['date']
- if row_date > latest_date_so_far:
- latest_date_so_far = row_date
- latest_date = [options]
- elif row_date == latest_date_so_far:
- latest_date.append(options)
- for options in most_geos:
- if options in latest_date:
- selected_rows = candidates_dict[options]
-
- for row in selected_rows:
- place2cov[row['place']][stat_var] = row['val']
- return [
- dict({'place': place}, **multivariates)
- for place, multivariates in place2cov.items()
- ]
-
-
-def build_multivariate_dataframe(places, stat_vars):
- """Constructs a pandas DataFrame with `places` as the index and `stat_vars` as the columns.
-
- To ensure statistics are comparable across all Places, when multiple
- StatVarObservations options are available for Place and StatVar combos,
- Data Commons selects the StatVarObservation options that covers the most
- Places, and breaks ties using the StatVarObservation options that yield
- the latest Observation for any Place.
-
- Args:
- places (`str` or `iterable` of `str`): The dcids of Places to query for.
- stat_vars (`Iterable` of `str`): The dcids of the StatisticalVariables.
- Returns:
- A pandas DataFrame with Place IDs as the index and `stat_vars` as columns.
- """
- try:
- if isinstance(places, six.string_types):
- places = [places]
- else:
- places = list(places)
- assert all(isinstance(place, six.string_types) for place in places)
- if isinstance(stat_vars, six.string_types):
- stat_vars = [stat_vars]
- else:
- stat_vars = list(stat_vars)
- assert all(
- isinstance(stat_var, six.string_types)
- for stat_var in stat_vars)
- except:
- raise ValueError(
- 'Parameter `places` and `stat_vars` must be string object or list-like object.'
- )
- df = pd.DataFrame.from_records(_multivariate_pd_input(places, stat_vars))
- df.set_index('place', inplace=True)
- df.sort_index(inplace=True)
- return df
diff --git a/datacommons_pandas/examples/__init__.py b/datacommons_pandas/examples/__init__.py
deleted file mode 100644
index 2c79033c..00000000
--- a/datacommons_pandas/examples/__init__.py
+++ /dev/null
@@ -1,13 +0,0 @@
-# Copyright 2020 Google Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
diff --git a/datacommons_pandas/examples/df_builder.py b/datacommons_pandas/examples/df_builder.py
deleted file mode 100644
index 6dff26bf..00000000
--- a/datacommons_pandas/examples/df_builder.py
+++ /dev/null
@@ -1,138 +0,0 @@
-# Copyright 2020 Google Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-"""Basic examples for building pandas objects using the Data Commons Pandas API."""
-
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
-import datacommons_pandas as dcpd
-
-
-def build_time_series_example():
-
- print("""
-# Build a pd.Series of time series for one variable and one place.
-$ dcpd.build_time_series('country/CAN', 'Count_WildlandFireEvent')
-{}""".format(dcpd.build_time_series('country/CAN', 'Count_WildlandFireEvent')))
-
- print("""
-# Build a pd.Series of time series for one variable and one place and optional args.
-$ dcpd.build_time_series('country/USA', 'Count_Person', 'CensusPEPSurvey')
-{}""".format(
- dcpd.build_time_series('country/USA', 'Count_Person',
- 'CensusPEPSurvey')))
-
-
-def build_time_series_dataframe_example():
-
- def demonstrate_build_time_series_dataframe(intro_str,
- places,
- stat_var,
- desc_col=False):
- arg_str = "{}, '{}'".format(places, stat_var)
- if desc_col:
- arg_str += ", desc_col=True"
- print("""
- # {}
- $ dcpd.build_time_series_dataframe({})
- {}""".format(intro_str, arg_str,
- dcpd.build_time_series_dataframe(places, stat_var, desc_col)))
-
- build_time_series_dataframe_params = [{
- 'intro_str':
- 'Build a DataFrame of time series for one variable in multiple places.',
- 'places': ['geoId/33', 'geoId/29', 'country/USA'],
- 'stat_var':
- 'Median_Income_Person'
- }, {
- 'intro_str':
- 'Build a DataFrame of time series with columns sorted in descending order.',
- 'places': ['country/USA'],
- 'stat_var':
- 'Median_Income_Person',
- 'desc_col':
- True
- }]
-
- for param_set in build_time_series_dataframe_params:
- demonstrate_build_time_series_dataframe(**param_set)
-
-
-def build_multivariate_dataframe_example():
-
- def demonstrate_build_multivariate_dataframe(intro_str, places, stat_vars):
- print("""
- # {}
- $ dcpd.build_multivariate_dataframe({}, {})
- {}""".format(intro_str, places, stat_vars,
- dcpd.build_multivariate_dataframe(places, stat_vars)))
-
- build_multivariate_dataframe_params = [{
- 'intro_str':
- 'Build a DataFrame of latest observations for multiple variables in multiple places.',
- 'places': ['geoId/06', 'country/FRA'],
- 'stat_vars': ['Median_Age_Person', 'Count_Person', 'Count_Household']
- }]
-
- for param_set in build_multivariate_dataframe_params:
- demonstrate_build_multivariate_dataframe(**param_set)
-
-
-def expect_err_examples():
-
- print("\n\nExpect 6 errors, starting HERE:")
- try:
- dcpd.build_time_series_dataframe(
- ['geoId/33'], ['Median_Income_Person', 'Count_Person'])
- except ValueError as e:
- print("Successfully errored on: ", e)
- try:
- dcpd.build_time_series_dataframe(24, ['Median_Income_Person'])
- except ValueError as e:
- print("Successfully errored on: ", e)
- try:
- dcpd.build_multivariate_dataframe(
- [3], ['Median_Income_Person', 'Count_Person'])
- except ValueError as e:
- print("Successfully errored on: ", e)
- try:
- dcpd.build_multivariate_dataframe('country/USA', True)
- except ValueError as e:
- print("Successfully errored on: ", e)
- # If the following two do not error due to the addition of
- # Median_Income_Person statistics for NUTS geos, then please
- # replace either the places or the StatVar.
- try:
- dcpd.build_time_series_dataframe(['nuts/HU2', 'nuts/HU22'],
- 'Median_Income_Person')
- except ValueError as e:
- print("Successfully errored on: ", e)
- try:
- dcpd.build_multivariate_dataframe(['nuts/HU2', 'nuts/HU22'],
- ['Median_Income_Person'])
- except ValueError as e:
- print("Successfully errored on: ", e)
- print("until HERE.")
-
-
-def main():
- build_time_series_example()
- build_time_series_dataframe_example()
- build_multivariate_dataframe_example()
- expect_err_examples()
-
-
-if __name__ == '__main__':
- main()
diff --git a/datacommons_pandas/places.py b/datacommons_pandas/places.py
deleted file mode 120000
index 7206307a..00000000
--- a/datacommons_pandas/places.py
+++ /dev/null
@@ -1 +0,0 @@
-../datacommons/places.py
\ No newline at end of file
diff --git a/datacommons_pandas/populations.py b/datacommons_pandas/populations.py
deleted file mode 120000
index 3e74c37b..00000000
--- a/datacommons_pandas/populations.py
+++ /dev/null
@@ -1 +0,0 @@
-../datacommons/populations.py
\ No newline at end of file
diff --git a/datacommons_pandas/query.py b/datacommons_pandas/query.py
deleted file mode 120000
index d7db3c39..00000000
--- a/datacommons_pandas/query.py
+++ /dev/null
@@ -1 +0,0 @@
-../datacommons/query.py
\ No newline at end of file
diff --git a/datacommons_pandas/stat_vars.py b/datacommons_pandas/stat_vars.py
deleted file mode 120000
index ab7359b6..00000000
--- a/datacommons_pandas/stat_vars.py
+++ /dev/null
@@ -1 +0,0 @@
-../datacommons/stat_vars.py
\ No newline at end of file
diff --git a/datacommons_pandas/test/__init__.py b/datacommons_pandas/test/__init__.py
deleted file mode 100644
index 2c79033c..00000000
--- a/datacommons_pandas/test/__init__.py
+++ /dev/null
@@ -1,13 +0,0 @@
-# Copyright 2020 Google Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
diff --git a/datacommons_pandas/test/df_builder_test.py b/datacommons_pandas/test/df_builder_test.py
deleted file mode 100644
index e686b16d..00000000
--- a/datacommons_pandas/test/df_builder_test.py
+++ /dev/null
@@ -1,295 +0,0 @@
-# Copyright 2020 Google Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-""" Data Commons Python API unit tests.
-
-Unit tests for StatVar methods in the Data Commons Pandas API.
-"""
-
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
-try:
- from unittest.mock import patch
-except ImportError:
- from mock import patch
-
-import datacommons_pandas.df_builder as dcpd
-import datacommons_pandas.utils as utils
-import json
-import unittest
-import six
-import six.moves.urllib as urllib
-
-# Reusable parts of REST API /stat/all response.
-CA_COUNT_PERSON = {
- "isDcAggregate":
- "true",
- "sourceSeries": [{
- "val": {
- "1990": 23640,
- "1991": 24100,
- "1993": 25090,
- },
- "observationPeriod": "P1Y",
- "importName": "WorldDevelopmentIndicators",
- "provenanceDomain": "worldbank.org"
- }, {
- "val": {
- "1790": 3929214,
- "1800": 5308483,
- "1810": 7239881,
- },
- "measurementMethod": "WikidataPopulation",
- "importName": "WikidataPopulation",
- "provenanceDomain": "wikidata.org"
- }, {
- "val": {
- "1890": 28360,
- "1891": 24910,
- "1892": 25070,
- },
- "measurementMethod": "OECDRegionalStatistics",
- "observationPeriod": "P1Y",
- "importName": "OECDRegionalDemography",
- "provenanceDomain": "oecd.org"
- }]
-}
-
-HU22_COUNT_PERSON = {
- "sourceSeries": [{
- "val": {
- "1990": 2360,
- "1991": 2410,
- "1992": 2500,
- },
- "measurementMethod": "OECDRegionalStatistics",
- "observationPeriod": "P1Y",
- "importName": "OECDRegionalDemography",
- "provenanceDomain": "oecd.org"
- }]
-}
-
-CA_MEDIAN_AGE_PERSON = {
- "sourceSeries": [{
- "val": {
- "1990": 12,
- "1991": 24,
- "1992": 24,
- },
- "measurementMethod": "WikidataPopulation",
- "importName": "WikidataPopulation",
- "provenanceDomain": "wikidata.org"
- }]
-}
-
-
-def request_mock(*args, **kwargs):
- """A mock urlopen requests sent in the requests package."""
-
- # Create the mock response object.
- class MockResponse:
-
- def __init__(self, json_data):
- self.json_data = json_data
-
- def read(self):
- return self.json_data
-
- req = args[0]
-
- stat_value_url_base = utils._API_ROOT + utils._API_ENDPOINTS[
- 'get_stat_value']
- stat_series_url_base = utils._API_ROOT + utils._API_ENDPOINTS[
- 'get_stat_series']
- stat_all_url_base = utils._API_ROOT + utils._API_ENDPOINTS['get_stat_all']
-
- # Mock responses for urlopen requests to get_stat_series.
- if req.get_full_url(
- ) == stat_series_url_base + '?place=geoId/06&stat_var=Count_Person':
- # Response returned when querying with basic args.
- return MockResponse(json.dumps({"series": {"2000": 1, "2001": 2}}))
- if (req.get_full_url() == stat_series_url_base +
- '?place=geoId/06&stat_var=Count_Person&' +
- 'measurement_method=CensusPEPSurvey&observation_period=P1Y&' +
- 'unit=RealPeople&scaling_factor=100'):
-
- # Response returned when querying with above optional params.
- return MockResponse(json.dumps({"series": {"2000": 3, "2001": 42}}))
- if (req.get_full_url() == stat_series_url_base +
- '?place=geoId/06&stat_var=Count_Person&' +
- 'measurement_method=DNE'):
-
- # Response returned when data not available for optional parameters.
- # /stat/series?place=geoId/06&stat_var=Count_Person&measurement_method=DNE
- return MockResponse(json.dumps({"series": {}}))
-
- # Mock responses for urlopen requests to get_stat_all.
- if req.get_full_url() == stat_all_url_base:
- data = json.loads(req.data)
-
- if (data['places'] == ['geoId/06', 'nuts/HU22'] and
- data['stat_vars'] == ['Count_Person', 'Median_Age_Person']):
- # Response returned when querying with above params.
- # Median Age missing for HU22.
- resp = {
- "placeData": {
- "geoId/06": {
- "statVarData": {
- "Count_Person": CA_COUNT_PERSON,
- "Median_Age_Person": CA_MEDIAN_AGE_PERSON
- }
- },
- "nuts/HU22": {
- "statVarData": {
- "Count_Person": HU22_COUNT_PERSON,
- "Median_Age_Person": {}
- }
- }
- }
- }
- return MockResponse(json.dumps(resp))
-
- if (data['places'] == ['geoId/06', 'nuts/HU22'] and
- data['stat_vars'] == ['Count_Person']):
- # Response returned when querying with above params.
- resp = {
- "placeData": {
- "geoId/06": {
- "statVarData": {
- "Count_Person": CA_COUNT_PERSON,
- }
- },
- "nuts/HU22": {
- "statVarData": {
- "Count_Person": HU22_COUNT_PERSON,
- }
- }
- }
- }
- return MockResponse(json.dumps(resp))
-
- if (data['places'] == ['geoId/06'] and
- data['stat_vars'] == ['Count_Person']):
- # Response returned when querying with above params.
- resp = {
- "placeData": {
- "geoId/06": {
- "statVarData": {
- "Count_Person": CA_COUNT_PERSON,
- }
- }
- }
- }
- return MockResponse(json.dumps(resp))
-
- if (data['places'] == ['geoId/06', 'nuts/HU22'] and
- data['stat_vars'] == ['Count_Person', 'Median_Age_Person']):
- # Response returned when querying with above params.
- # Median Age missing for HU22.
- resp = {
- "placeData": {
- "geoId/06": {
- "statVarData": {
- "Count_Person": CA_COUNT_PERSON,
- "Median_Age_Person": CA_MEDIAN_AGE_PERSON
- }
- },
- "nuts/HU22": {
- "statVarData": {
- "Count_Person": HU22_COUNT_PERSON,
- "Median_Age_Person": {}
- }
- }
- }
- }
- return MockResponse(json.dumps(resp))
- # Otherwise, return an empty response and a 404.
- return urllib.error.HTTPError(None, 404, None, None, None)
-
-
-class TestPdTimeSeries(unittest.TestCase):
- """Unit tests for _time_series_pd_input."""
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_basic(self, urlopen):
- """Calling _time_series_pd_input with proper args."""
- rows = dcpd._time_series_pd_input(['geoId/06', 'nuts/HU22'],
- 'Count_Person')
- exp = [{
- "1890": 28360,
- "1891": 24910,
- "1892": 25070,
- "place": "geoId/06"
- }, {
- "1991": 2410,
- "1990": 2360,
- "1992": 2500,
- "place": "nuts/HU22"
- }]
- six.assertCountEqual(self, rows, exp)
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_one_place(self, urlopen):
- """Calling _time_series_pd_input with single place."""
- rows = dcpd._time_series_pd_input(['geoId/06'], 'Count_Person')
- exp = [{
- "1990": 23640,
- "1991": 24100,
- "1993": 25090,
- "place": "geoId/06"
- }]
- self.assertEqual(rows, exp)
-
-
-class TestPdMultivariates(unittest.TestCase):
- """Unit tests for _multivariate_pd_input."""
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_basic(self, urlopen):
- """Calling _multivariate_pd_input with proper args."""
- rows = dcpd._multivariate_pd_input(
- ['geoId/06', 'nuts/HU22'], ['Count_Person', 'Median_Age_Person'])
- exp = [{
- "place": "geoId/06",
- "Median_Age_Person": 24,
- "Count_Person": 25070
- }, {
- "place": "nuts/HU22",
- "Count_Person": 2500
- }]
- six.assertCountEqual(self, rows, exp)
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_one_each(self, urlopen):
- """Calling _multivariate_pd_input with single place and var."""
- rows = dcpd._multivariate_pd_input(['geoId/06'], ['Count_Person'])
- exp = [{"place": "geoId/06", "Count_Person": 25090}]
- self.assertEqual(rows, exp)
-
- @patch('six.moves.urllib.request.urlopen', side_effect=request_mock)
- def test_no_data(self, urlopen):
- """Error if there is no data."""
- with self.assertRaises(ValueError):
- dcpd._group_stat_all_by_obs_options(
- ['FOO/100'], ['Count_Person', 'Median_Age_Person'])
- with self.assertRaises(ValueError):
- dcpd._time_series_pd_input(['FOO/100', 'BAR/200'], ['Count_Person'])
- with self.assertRaises(ValueError):
- dcpd._multivariate_pd_input(['FOO/100', 'BAR/200'],
- ['Count_Person', 'Median_Age_Person'])
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/datacommons_pandas/utils.py b/datacommons_pandas/utils.py
deleted file mode 120000
index 06c545f5..00000000
--- a/datacommons_pandas/utils.py
+++ /dev/null
@@ -1 +0,0 @@
-../datacommons/utils.py
\ No newline at end of file
diff --git a/docs/development.md b/docs/development.md
new file mode 100644
index 00000000..03071c10
--- /dev/null
+++ b/docs/development.md
@@ -0,0 +1,55 @@
+# Python API Development
+
+This client library supports `python>=3.10`.
+
+## Set up
+If you haven't already, clone this repository.
+
+```bash
+git clone https://github.com/datacommonsorg/api-python.git
+cd api-python
+```
+
+To set up the Python environment for development, run:
+
+```bash
+./run_test.sh -s
+```
+
+This will install `hatch`, which is the main tool used to manage the
+environment, dependencies, and development tools. You can also manually install
+`hatch` and create a virtual environment.
+
+```bash
+pip install hatch
+hatch env create
+```
+
+## Code style and linting
+We use `isort` and `yapf` for code formatting. Check formatting with:
+
+```bash
+hatch run lint:check
+```
+
+To automatically fix formatting run:
+
+```bash
+hatch run lint:format
+```
+
+## Running tests
+
+To test, run:
+
+```bash
+hatch run test:all
+```
+
+To debug the continuous integration tests, run:
+
+```bash
+gcloud builds submit . --project=datcom-ci --config=cloudbuild.yaml
+```
+
+Both commands will run the same set of tests.
\ No newline at end of file
diff --git a/docs/release.md b/docs/release.md
new file mode 100644
index 00000000..96425cb4
--- /dev/null
+++ b/docs/release.md
@@ -0,0 +1,32 @@
+# Python API Release
+
+## Releasing the `datacommons_client` package
+Support for V2 of the Data Commons API is being released as a new client library
+called `datacommons_client`.
+
+To release:
+1. Update [CHANGELOG.md](../CHANGELOG.md) with relevant changes.
+2. Bump the version by running `hatch version` followed by `patch`, `minor`, `major`, a
+specific version number, or `--pre beta` for a beta version, for example.
+3. Build the package
+```bash
+hatch build
+```
+4. (optionally) Test the deployment process locally
+```bash
+hatch run release:localtest
+```
+5. Test the deployment process on Test PyPi
+```bash
+hatch run release:testpypi
+```
+
+6. Once verified, upload to PyPI:
+```bash
+hatch run release:pypi
+```
+
+7. Create a version tag on Git:
+```bash
+hatch run release:tag
+```
diff --git a/notebooks/COVID_19_Feature_Exploration_Analysis_with_Data_Commons.ipynb b/notebooks/COVID_19_Feature_Exploration_Analysis_with_Data_Commons.ipynb
deleted file mode 100644
index 4f736f8c..00000000
--- a/notebooks/COVID_19_Feature_Exploration_Analysis_with_Data_Commons.ipynb
+++ /dev/null
@@ -1,3728 +0,0 @@
-{
- "nbformat": 4,
- "nbformat_minor": 0,
- "metadata": {
- "colab": {
- "name": "COVID-19 Feature Exploration Analysis with Data Commons",
- "provenance": [],
- "collapsed_sections": [],
- "toc_visible": true,
- "include_colab_link": true
- },
- "kernelspec": {
- "name": "python3",
- "display_name": "Python 3"
- }
- },
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {
- "id": "view-in-github",
- "colab_type": "text"
- },
- "source": [
- ""
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "id": "8f_bICClsb5p",
- "colab_type": "text"
- },
- "source": [
- "Copyright 2020 Google LLC.\n",
- "SPDX-License-Identifier: Apache-2.0\n",
- "\n",
- "**Authors**: Yi Gao, Xianzhi Helen Wang"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "id": "CKiH1L7wHYlX",
- "colab_type": "text"
- },
- "source": [
- "# Welcome!\n",
- "\n",
- "Welcome to this Colab notebook! We will walk you through two examples of exploration analyses with the Data Commons dataset here:\n",
- "\n",
- "1. How do the COVID-19 cases trends differ across different counties?\n",
- "2. What features are potentially correlated with COVID-19 mortality rate? \n",
- "\n",
- "We hope the examples can help you better understand what data are available on Data Commons and how to use them, so that further research or analysis can be conducted easily.\n",
- "\n",
- "**More about the dataset used in this Colab**\n",
- "\n",
- "The analyses here use a frozen version of the data set which was generated around mid July, 2020. \n",
- "* For COVID-19 case data, we fix the analysis period as 2020-02-26 to 2020-07-10 so that the results in this Colab are reproducible. (The start date is chosen since it's the time of the first instance of community spread as reported [here](https://www.cdc.gov/media/releases/2020/s0226-COVID-19-spread.html).)\n",
- "* For context data, they are the most recent values at the time when this data set was generated (2020-07-15). More details about the context features' definition can be found on Data Commons ([link](http://docs.datacommons.org/statistical_variables.html)).\n",
- "\n",
- "You can find live versions of the data sets in the Appendix section of this Colab.\n",
- "\n",
- "Hope you enjoy the exploratory analysis here!"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "id": "4GQojUtPY05J",
- "colab_type": "text"
- },
- "source": [
- "# Prepare"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "id": "oFqaXbHiMp1f",
- "colab_type": "text"
- },
- "source": [
- "## Load Packages"
- ]
- },
- {
- "cell_type": "code",
- "metadata": {
- "id": "iLO0z5aRGKjr",
- "colab_type": "code",
- "colab": {
- "base_uri": "https://localhost:8080/",
- "height": 303
- },
- "outputId": "a364b400-e7ce-4b40-d831-9c31b4a8559a"
- },
- "source": [
- "!pip install -U plotly\n",
- "\n",
- "import json\n",
- "import matplotlib.pyplot as plt\n",
- "import numpy as np\n",
- "import pandas as pd\n",
- "import plotly.express as px\n",
- "import random\n",
- "import seaborn as sns\n",
- "import statsmodels.api as sm\n",
- "\n",
- "from collections import defaultdict\n",
- "from datetime import datetime\n",
- "from sklearn.cluster import KMeans\n",
- "from sklearn.decomposition import PCA\n",
- "from sklearn.ensemble import RandomForestClassifier\n",
- "from sklearn.ensemble import RandomForestRegressor\n",
- "from sklearn.metrics import silhouette_score\n",
- "from urllib.request import urlopen\n",
- "\n",
- "# This is used for map plots.\n",
- "counties_geo_path = 'https://raw.githubusercontent.com/plotly/datasets/master/geojson-counties-fips.json'\n",
- "with urlopen(counties_geo_path) as response:\n",
- " COUNTIES_GEO = json.load(response)\n",
- "\n",
- "pd.set_option('max_colwidth', 400)\n"
- ],
- "execution_count": 1,
- "outputs": [
- {
- "output_type": "stream",
- "text": [
- "Collecting plotly\n",
- "\u001b[?25l Downloading https://files.pythonhosted.org/packages/04/20/c2d77eef33dbd40c5e3263a9a8763ffca610a4c3d2b2da21c5601e5fc5d8/plotly-4.10.0-py2.py3-none-any.whl (13.0MB)\n",
- "\u001b[K |████████████████████████████████| 13.0MB 323kB/s \n",
- "\u001b[?25hRequirement already satisfied, skipping upgrade: six in /usr/local/lib/python3.6/dist-packages (from plotly) (1.15.0)\n",
- "Requirement already satisfied, skipping upgrade: retrying>=1.3.3 in /usr/local/lib/python3.6/dist-packages (from plotly) (1.3.3)\n",
- "Installing collected packages: plotly\n",
- " Found existing installation: plotly 4.4.1\n",
- " Uninstalling plotly-4.4.1:\n",
- " Successfully uninstalled plotly-4.4.1\n",
- "Successfully installed plotly-4.10.0\n"
- ],
- "name": "stdout"
- },
- {
- "output_type": "stream",
- "text": [
- "/usr/local/lib/python3.6/dist-packages/statsmodels/tools/_testing.py:19: FutureWarning:\n",
- "\n",
- "pandas.util.testing is deprecated. Use the functions in the public API at pandas.testing instead.\n",
- "\n"
- ],
- "name": "stderr"
- }
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "id": "s-CG75mgMtxM",
- "colab_type": "text"
- },
- "source": [
- "## Load and join data sets"
- ]
- },
- {
- "cell_type": "code",
- "metadata": {
- "id": "ATtiFNuIGWtJ",
- "colab_type": "code",
- "cellView": "code",
- "colab": {}
- },
- "source": [
- "# The Google files are in this Google Drive Folder:\n",
- "# https://drive.google.com/drive/u/1/folders/1mM1scQxsD38tlSk7IcSKJrN7Ez5-J22F\n",
- "\n",
- "analysis_data_start = datetime.strptime('2020-02-26', '%Y-%m-%d')\n",
- "analysis_data_end = datetime.strptime('2020-07-10', '%Y-%m-%d')\n",
- "date_range = pd.date_range(analysis_data_start, analysis_data_end).format(\n",
- " formatter=lambda x: x.strftime('%Y-%m-%d'))\n",
- "count_data_columns = ['County', 'Name'] + date_range\n",
- "\n",
- "STATE_COVARIATE_PATH = 'https://docs.google.com/spreadsheets/d/e/2PACX-1vR12v_MRKdDa6u2mdD0aPFlDilV0SJtbeYyrNbqI3qZDuImw_RnDcpNAmwbJkRP7O7fAC-9EYsoV4Dm/pub?output=csv'\n",
- "df_state_context = pd.read_csv(STATE_COVARIATE_PATH)\n",
- "COUNTY_COVARIATE_PATH = 'https://docs.google.com/spreadsheets/d/e/2PACX-1vSSeYEVT9wH-F6X7qxnhyXke1JoudkpqAJqVdZWlkTLF0Ah5529VDB4gLLICrqpczbvl-qi_P7Eyte7/pub?output=csv'\n",
- "df_county_context = pd.read_csv(COUNTY_COVARIATE_PATH)\n",
- "\n",
- "COUNTY_POSITIVE_COUNT_PATH = 'https://docs.google.com/spreadsheets/d/e/2PACX-1vSv2PsEqsv4wyVUG9F3SCBMgMm9vXQg05a_Hyjc7xw5uOmlNUFYSg75JxvRQ3yuHodoZMFcK_FB1J3H/pub?output=csv'\n",
- "df_county_case = pd.read_csv(COUNTY_POSITIVE_COUNT_PATH)\n",
- "df_county_case = df_county_case[count_data_columns]\n",
- "\n",
- "COUNTY_DEATH_COUNT_PATH = 'https://docs.google.com/spreadsheets/d/e/2PACX-1vRqhlJ7yAGMEJtnS8-8BISwB7ZfB9WyHFtOpbGe5v_hVmUHXvkXyJCXCDX-jp8u6KC-SdbVBs4c78aI/pub?output=csv'\n",
- "df_county_death = pd.read_csv(COUNTY_DEATH_COUNT_PATH)\n",
- "df_county_death = df_county_death[count_data_columns]"
- ],
- "execution_count": 2,
- "outputs": []
- },
- {
- "cell_type": "code",
- "metadata": {
- "id": "pEll5ECdQzLY",
- "colab_type": "code",
- "colab": {}
- },
- "source": [
- "# Create geoid - county / state name mapping.\n",
- "df_county_name = df_county_context[['County', 'Name']].copy()\n",
- "df_county_name['State'] = df_county_name['County'].apply(lambda x: x[:-3])\n",
- "df_county_name = pd.merge(df_county_name, df_state_context[['State', 'Name']],\n",
- " on='State', suffixes=('_county', '_state'))\n",
- "df_county_name['full_name'] = df_county_name['Name_county'] + ', '+ \\\n",
- " df_county_name['Name_state']\n",
- "\n",
- "county_name_mapping = defaultdict(str)\n",
- "for _, row in df_county_name.iterrows():\n",
- " county_name_mapping[row['County']] = row['full_name']\n",
- "\n",
- "def get_name_by_geoid(geoid):\n",
- " return county_name_mapping[geoid]"
- ],
- "execution_count": 3,
- "outputs": []
- },
- {
- "cell_type": "code",
- "metadata": {
- "id": "2H_19sCyydGG",
- "colab_type": "code",
- "colab": {}
- },
- "source": [
- "# Merge the county level DataFrames.\n",
- "df_county_joined = pd.merge(df_county_case, df_county_death, how='outer', \n",
- " suffixes=('_case', '_death'), \n",
- " on=['County', 'Name']).pipe(\n",
- " pd.merge,right=df_county_context, how='outer')\n",
- "df_county_joined = df_county_joined.set_index('County')\n",
- "df_county_joined = df_county_joined.join(df_county_name)\n",
- "\n",
- "case_columns = [date + '_case' for date in date_range]\n",
- "death_columns = [date + '_death' for date in date_range]"
- ],
- "execution_count": 4,
- "outputs": []
- },
- {
- "cell_type": "code",
- "metadata": {
- "id": "iV3voC4IuUet",
- "colab_type": "code",
- "colab": {
- "base_uri": "https://localhost:8080/",
- "height": 112
- },
- "outputId": "ee9e669b-9266-4b3f-c4b0-426a314af326"
- },
- "source": [
- "# Check the shapes of the loaded DataFrames\n",
- "print(f'The shape of df_state_context is {df_state_context.shape}.')\n",
- "print(f'The shape of df_county_context is {df_county_context.shape}.')\n",
- "print(f'The shape of df_county_case is {df_county_case.shape}.')\n",
- "print(f'The shape of df_county_death is {df_county_death.shape}.')\n",
- "print(f'The shape of df_county_joined is {df_county_joined.shape}.')"
- ],
- "execution_count": 5,
- "outputs": [
- {
- "output_type": "stream",
- "text": [
- "The shape of df_state_context is (52, 192).\n",
- "The shape of df_county_context is (3190, 182).\n",
- "The shape of df_county_case is (3076, 138).\n",
- "The shape of df_county_death is (3138, 138).\n",
- "The shape of df_county_joined is (3258, 458).\n"
- ],
- "name": "stdout"
- }
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "id": "ZKZEyb51ZHpC",
- "colab_type": "text"
- },
- "source": [
- "# How do COVID-19 cases trends differ across different counties?\n",
- "\n",
- "In this section, we give an example of discovering different case trends across counties. \n",
- "\n",
- "Here are the main steps in this example:\n",
- "1. Calculate normalized case trends.\n",
- "2. Cluster the counties of similar case growth patterns.\n",
- "3. Visualize the main trend of each cluster.\n",
- "4. Visualize the clustering result on the U.S. map.\n",
- "\n"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "id": "K6OCxuXDZZNz",
- "colab_type": "text"
- },
- "source": [
- "## Calculate normalized trends\n",
- "\n",
- "The positive case counts available in DataCommons are cumulative counts. In this part, we calculate daily incremental case counts and normalize them by population, so that trends can be compared between different counties. \n",
- "\n",
- "**Technical detail**\n",
- "\n",
- "We will cluster the counties by their smoothed trends of incremental positive cases normalized by population: \n",
- "\n",
- "$$\\frac{\\#\\ of\\ incremental\\ positive\\ cases}{county\\ population}.$$\n",
- "\n",
- "The $\\#\\ of\\ incremental\\ positive\\ cases$ is the number of new cases diagnosed with COVID-19 on each day in each county. We want to look at incremental changes instead of the cumulative trends because it's easier to distinguish / interpret incremental change patterns (which can go up / down) compared to cumulative trend patterns (which all go up).\n",
- "\n",
- "The incremental cases are normalized by population as we expect to see higher case numbers in counties with more people. Simply clustering the counties using the incremental case numbers will possibly generate clusters just reflecting county populations. After the normalization, we can have the numbers that are roughly comparable among the counties, regardless of their population, so that the clustering results can better reflect the severity of the counties.\n",
- "\n",
- "In addition, the trends are smoothed with centered 7-day moving average to adjust for weekday effects.\n",
- "\n",
- "We will see what are the main trend patterns during the analysis period and how they are distributed in the U.S."
- ]
- },
- {
- "cell_type": "code",
- "metadata": {
- "id": "4WgG4ZfxMSN8",
- "colab_type": "code",
- "cellView": "both",
- "colab": {}
- },
- "source": [
- "def obtain_incremental_trend(df, count_columns, smooth=True, clip=True):\n",
- " \"\"\"Obtains the normalized incremental changes.\n",
- "\n",
- " The input df contains cumulative counts of interest, and this function\n",
- " calculates normalized incremental changes from that df.\n",
- "\n",
- " Args:\n",
- " df: A data frame with joined data.\n",
- " count_columns: Columns in the joined data dataframe specifying the columns\n",
- " of cumulative counts. They should have the format YYYY-MM-DD_[case|death].\n",
- " smooth: Whether to take rolling averages of the counts. If True, the output\n",
- " will be 7-day centered rolling averages of incremental changes in each\n",
- " county. Otherwise the raw incremental trends will be returned. Note that\n",
- " first 3 days and last 3 days will be removed if smooth=True.\n",
- " clip: Whether to replace negative incremental changes as 0.\n",
- " \n",
- " Returns:\n",
- " A dataframe with dates as index and county geoId as columns names. Each\n",
- " column is a series of incremental changes in the county.\n",
- " \"\"\"\n",
- " trends = df[count_columns].apply(lambda x: x / df['Count_Person'])\n",
- " trends = trends.transpose()\n",
- " trends.index = pd.to_datetime([idx.split('_')[0] for idx in trends.index])\n",
- " trends = trends.dropna(axis=1, how='all')\n",
- " trend_changes = trends.sort_index().diff()\n",
- " # Some incremental changes of cumulative positive cases are negative, and this\n",
- " # is an issue existing in the source data (JHU dataset) we are using. We \n",
- " # simply replace the negative ones as 0 here.\n",
- " if clip:\n",
- " trend_changes = trend_changes.clip(lower=0.)\n",
- " if smooth:\n",
- " trend_changes = trend_changes.apply(\n",
- " lambda x: x.rolling(7,center=True).mean())\n",
- " return trend_changes.dropna()\n",
- "\n",
- "case_incremental_trends = obtain_incremental_trend(df_county_joined,\n",
- " case_columns)"
- ],
- "execution_count": 6,
- "outputs": []
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "id": "bppHEz5HaHgw",
- "colab_type": "text"
- },
- "source": [
- "## Cluster the counties\n",
- "\n",
- "After preparing the trend data, we run a K-means algorithm to cluster the counties into 9 clusters. The cluster labels are ordered so that smaller labels indicate larger cluster sizes.\n",
- "\n",
- "**Technical detail**\n",
- "\n",
- "The number of clusters, 9, is decided in this Appendix section."
- ]
- },
- {
- "cell_type": "code",
- "metadata": {
- "id": "zBBcLfaHtFri",
- "colab_type": "code",
- "colab": {}
- },
- "source": [
- "def get_cluster_labels(trend_df, n_cluster, **kmeans_kwargs):\n",
- " \"\"\"Generates cluster labels for data containing incremental trends.\n",
- " \n",
- " Args:\n",
- " trend_df: A dataframe containing incremental trends generated from the \n",
- " function 'obtain_incremental_trend'.\n",
- " n_cluster: The number of clusters desired.\n",
- " random_seed: A random seed to fix the results of the k-means algorithm.\n",
- " **kmeans_kwargs: Named arguments passed to the KMeans function.\n",
- "\n",
- " Returns:\n",
- " A data frame with two columns. The first column 'County' contains the\n",
- " geoIds and the second column 'Cluster' contains the cluster labels. The\n",
- " cluster labels are ordered so that clusters with smaller labels are larger\n",
- " clusters containing more conties.\n",
- " \"\"\"\n",
- " cluster_data_df = trend_df.transpose()\n",
- " km_model = KMeans(n_clusters=n_cluster, **kmeans_kwargs).fit(cluster_data_df)\n",
- " cluster_labels = pd.DataFrame({\n",
- " 'County': trend_df.columns,\n",
- " 'Cluster': km_model.predict(cluster_data_df)})\n",
- " cluster_counts = cluster_labels['Cluster'].value_counts().sort_values(\n",
- " ascending=False)\n",
- " new_cluster_mapping = {cluster_counts.index[i]: i for\n",
- " i in range(len(cluster_counts))}\n",
- " cluster_labels['Cluster'] = cluster_labels['Cluster'].apply(\n",
- " lambda x: new_cluster_mapping[x])\n",
- " return cluster_labels\n",
- "\n",
- "case_cluster_labels = get_cluster_labels(case_incremental_trends, 9,\n",
- " random_state=42, n_init=1000)\n"
- ],
- "execution_count": 7,
- "outputs": []
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "id": "jupmVpx-tRiS",
- "colab_type": "text"
- },
- "source": [
- "## Check the main trend of each cluster\n",
- "\n",
- "The median trend values of the clusters are shown below. (The shadow area is specified by 2.5%-tile and 97.5%-tile values in each cluster on each day.) A few clusters are very small because they are essentially 'outliers' in the data set. The first few clusters are of special interest:\n",
- "\n",
- "* Cluster '0' is the counties with low case ratio over the whole analysis period.\n",
- "* Cluster '1' is the counties where new case ratio started to increase after June.\n",
- "* Cluster '2' is the severe counties during April and May.\n",
- "* Cluster '3' is the very severe counties where new cases peaked in early June and then dropped.\n",
- "* Cluster '4' is the very severe counties where new cases peaked in early May and then dropped.\n",
- "* Cluster 5-8 are basically some 'outliers'."
- ]
- },
- {
- "cell_type": "code",
- "metadata": {
- "id": "-QoYKcG-YCvE",
- "colab_type": "code",
- "colab": {
- "base_uri": "https://localhost:8080/",
- "height": 351
- },
- "outputId": "9ecd5066-167f-42d4-c72f-ff7524984338"
- },
- "source": [
- "def plot_cluster_medians(rate_df, label_df, nc=5, ylim=None):\n",
- " \"\"\"Plots cluster medians with bounds of 2.5 and 97.5 percentiles.\n",
- "\n",
- " Args:\n",
- " rate_df: The dataframe containing incremental rates.\n",
- " label_df: The dataframe containing cluster labels of each county.\n",
- " nc: The number of columns in the output plot.\n",
- " ylim: The y-limits of the plots.\n",
- "\n",
- " Returns:\n",
- " A dataframe containing the median trends of the clusters. A group of trend\n",
- " plots with median trends with bounds of 2.5 and 97.5 percentiles is plotted.\n",
- " \"\"\"\n",
- " n_cluster = label_df['Cluster'].nunique()\n",
- " nr = int(np.ceil(n_cluster / nc))\n",
- " fig, ax = plt.subplots(nr, nc, sharex=True, sharey=True, figsize=[20, 3*nr])\n",
- " ax = ax.flatten()\n",
- " all_trend = pd.DataFrame()\n",
- " if ylim is None:\n",
- " ylim = [0, rate_df.max().max()]\n",
- " for i, cluster in enumerate(sorted(label_df['Cluster'].unique())):\n",
- " temp_county = label_df[label_df['Cluster'] == cluster]['County'].values\n",
- " temp_rates = rate_df[temp_county]\n",
- " temp_stats = temp_rates.transpose().describe(percentiles=[0.025, 0.975])\n",
- " up_bound = temp_stats.loc['97.5%']\n",
- " low_bound = temp_stats.loc['2.5%']\n",
- " median_trend = temp_stats.loc['50%']\n",
- " all_trend[f'cluster_{i}'] = median_trend\n",
- " median_trend.plot(ax=ax[i], label='median')\n",
- " ax[i].set_title(f'cluster_{i} (n={len(temp_county)})')\n",
- " ax[i].fill_between(median_trend.index, low_bound.values, up_bound.values, \n",
- " alpha=0.3, label='[2.5%, 97.75%]-tiles')\n",
- " ax[i].set_ylim(ylim)\n",
- " if i==0:\n",
- " ax[i].legend()\n",
- " return all_trend\n",
- "\n",
- "median_case_trends = plot_cluster_medians(case_incremental_trends,\n",
- " case_cluster_labels, ylim=[0, 0.002])"
- ],
- "execution_count": 8,
- "outputs": [
- {
- "output_type": "display_data",
- "data": {
- "image/png": "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FO2YqdSUe/yqJ3sAUt4GBga48sorufXWW2ltbR21/dRTT2XXrl00NzfzwAMPcPnll7Nt2zZmzJjBr371K8Drh/S5z32Oe++9l3e96110d3dz/fXX86pXvSrj695444184AMfYM2aNZxyyimsXbsWn883vD0cDnPffffx2c9+dszx33///Zx11lkjyt0ef/xxjjzySNrb23n961/PSSedxLnnnsuHPvSh4Qyqd77zndx888185zvf4aGHHmLVqlV8/OMfz+tnl8lDDz3EQw89xNq1awHvZ7xt2zbOOeccrr/+ej784Q9zySWXcM455xTl9URERESkvJRzyVsgnHuJkXrQSLJixkl7ByMj/lsKqb3DJH/KUJKMIpEIV155JW95y1v467/+67T7tLa2Djebvvjii4lEIqNKvD796U/zsY99jDvvvJOzzz6b73//+3zyk58c87VbW1v57ne/y4YNG/jBD35AR0cHxx133PD2X//615x66qksWLBgzOOky2I68sgjAZg/fz5XXHEF69atG7H9mWeewTnHiSeeyP/+7/9y991389JLL7Ft27ZRx7/wwgtZs2YN73znO8ccRzLnHB/5yEeGezxt376da6+9lhNOOIGnn36aU045hY9//OPDDc5FREREZHops3jSiAvrXMvdQKtkyUjFCpQ65+gJlD6gFI3r93e8FFCStJxzXHvttSxfvpwPfvCDGfc7cOAALvGHY926dcTjcebMmTO8fdu2bbS1tXHeeecRCASoqqrCzBgcHATg61//Ol//+tdHHbenp4dw2Etz/M53vsO55547IkMql75Ivb29/OEPf+Cyyy4bfszv99Pf3z/89UMPPTRqxbabbrqJT3/600QiEWIx7w5NVVXViGbbQx588EE2bNjAd77zHcArnRs6fiYXXnghd9xxBwMDAwDs3buX9vZ29u3bR2NjI3//93/PDTfcwNNPPz3mcURERERkaiq3krfk8eTakBsgFFFTbjmsWAGlgVB0uAyzr5QBJWUojZtK3srU61aMnXlTan/605/44Q9/yCmnnMKaNWsAb+W1iy++mP/6r/8C4D3veQ8//elP+da3vkV1dTUNDQ3cddddmNnwcT72sY9xyy23AHDNNddw+eWX87nPfW44+2br1q2cddZZo15/y5YtvP3tb8fMWLlyJbfffvvwNr/fz8MPP8y3v/3t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\n",
- "text/plain": [
- ""
- ]
- },
- "metadata": {
- "tags": [],
- "needs_background": "light"
- }
- }
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "id": "VV6meXkEaZiC",
- "colab_type": "text"
- },
- "source": [
- "## Geographical distribution of the clusters\n",
- "\n",
- "Plot the heatmap of the distribution of the different trend patterns."
- ]
- },
- {
- "cell_type": "code",
- "metadata": {
- "id": "ygdTyEMmYt0D",
- "colab_type": "code",
- "colab": {
- "base_uri": "https://localhost:8080/",
- "height": 542
- },
- "outputId": "16f4746c-270b-45da-d8be-5bf6f596bcf4"
- },
- "source": [
- "def norm_by_range(x):\n",
- " \"\"\"Normalizes an array by it's range.\n",
- "\n",
- " Args:\n",
- " x: an array or a pd.Series.\n",
- "\n",
- " Returns:\n",
- " A normalized array or pd.Series whose values are normalized to the range\n",
- " between 0 and 1.\n",
- " \n",
- " \"\"\"\n",
- " return (x - x.min()) / (x.max() - x.min())\n",
- "\n",
- "def show_values_on_map(values, title, adjust_cmap=False, **kwargs):\n",
- " \"\"\"Shows the values associated with counties on a U.S. map.\n",
- "\n",
- " Args:\n",
- " values: A pd.Series containing the values of interest indexed by county geo\n",
- " IDs. The values can either be strings or numbers. Strings will be\n",
- " interpreted as groups and numbers will be used as continuous values.\n",
- " title: The title of the plot.\n",
- " adjust_cmap: Whether to adjust the color map. Only set this as True for\n",
- " numerical values. It will adjust the color scale so that the the plot is\n",
- " not influenced by severe outliers.\n",
- " **kwargs: Other key-value arguments passed to px.choropleth.\n",
- " \n",
- " Returns:\n",
- " A plotly.graph_objs._figure.Figure project.\n",
- " \"\"\"\n",
- " plot_df = pd.DataFrame({\n",
- " 'county': values.index,\n",
- " 'value': values.values\n",
- " })\n",
- " plot_df['full_name'] = plot_df['county'].apply(get_name_by_geoid)\n",
- " plot_df['fips'] = plot_df['county'].apply(lambda x: x.split('/')[1])\n",
- " if adjust_cmap:\n",
- " value_describe = plot_df['value'].describe(\n",
- " percentiles=[0.01, 0.99])[['min','1%', '50%', '99%', 'max']]\n",
- " value_describe_norm = norm_by_range(value_describe)\n",
- " new_color_scale = [(0, 'green'), \n",
- " (value_describe_norm['1%'], 'green'),\n",
- " (value_describe_norm['50%'], 'white'),\n",
- " (value_describe_norm['99%'], 'orange'),\n",
- " (1, 'orange')]\n",
- " fig = px.choropleth(plot_df, locations='fips', geojson=COUNTIES_GEO, \n",
- " color='value', scope=\"usa\",\n",
- " hover_data=['full_name'],\n",
- " color_continuous_scale=new_color_scale,\n",
- " title=title, **kwargs)\n",
- " else:\n",
- " fig = px.choropleth(plot_df, locations='fips', geojson=COUNTIES_GEO, \n",
- " color='value', scope=\"usa\",\n",
- " hover_data=['full_name'],\n",
- " title=title, **kwargs)\n",
- " return fig\n",
- "\n",
- "str_cluster_labels = case_cluster_labels.sort_values(\n",
- " 'Cluster').set_index('County')['Cluster'].apply(lambda x: f'cluster_{x}')\n",
- "color_sequence = ['#2ca02c', '#ff7f0e', '#9467bd', '#8c564b', '#1f77b4',\n",
- " '#d62728', '#17becf', '#bcbd22', '#7f7f7f']\n",
- "show_values_on_map(str_cluster_labels, 'Case trend cluster distribution',\n",
- " color_discrete_sequence = color_sequence)"
- ],
- "execution_count": 9,
- "outputs": [
- {
- "output_type": "display_data",
- "data": {
- "text/html": [
- "\n",
- "\n",
- "\n",
- "