Skip to content

Commit 7c27493

Browse files
committed
New HDX state class
1 parent 2098d9f commit 7c27493

5 files changed

Lines changed: 232 additions & 4 deletions

File tree

requirements.txt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ click==8.1.8
3232
# typer
3333
colorama==0.4.6
3434
# via mkdocs-material
35-
coverage==7.8.0
35+
coverage==7.8.1
3636
# via pytest-cov
3737
defopt==6.4.0
3838
# via hdx-python-api (pyproject.toml)
@@ -56,7 +56,7 @@ frictionless==5.18.1
5656
# via hdx-python-utilities
5757
ghp-import==2.1.0
5858
# via mkdocs
59-
google-auth==2.40.1
59+
google-auth==2.40.2
6060
# via
6161
# google-auth-oauthlib
6262
# gspread
@@ -250,7 +250,7 @@ rfc3986==2.0.0
250250
# via frictionless
251251
rich==14.0.0
252252
# via typer
253-
rpds-py==0.25.0
253+
rpds-py==0.25.1
254254
# via
255255
# jsonschema
256256
# referencing
@@ -296,7 +296,7 @@ typing-extensions==4.13.2
296296
# typeguard
297297
# typer
298298
# typing-inspection
299-
typing-inspection==0.4.0
299+
typing-inspection==0.4.1
300300
# via pydantic
301301
unidecode==1.4.0
302302
# via

src/hdx/api/utilities/hdx_state.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""Utility to save state to a dataset and read it back."""
2+
3+
import logging
4+
from os.path import join
5+
from typing import Any, Callable
6+
7+
from hdx.data.dataset import Dataset
8+
from hdx.utilities.loader import load_text
9+
from hdx.utilities.saver import save_text
10+
from hdx.utilities.state import State
11+
12+
logger = logging.getLogger(__name__)
13+
14+
15+
class HDXState(State):
16+
"""State class that allows the reading and writing of state to a given HDX
17+
dataset. Input and output state transformations can be supplied in read_fn and
18+
write_fn respectively. The input state transformation takes in a string
19+
while the output transformation outputs a string.
20+
21+
Args:
22+
dataset_name_or_id (str): Dataset name or ID
23+
path (str): Path to temporary folder for state
24+
read_fn (Callable[[str], Any]): Input state transformation. Defaults to lambda x: x.
25+
write_fn: Callable[[Any], str]: Output state transformation. Defaults to lambda x: x.
26+
"""
27+
28+
def __init__(
29+
self,
30+
dataset_name_or_id: str,
31+
path: str,
32+
read_fn: Callable[[str], Any] = lambda x: x,
33+
write_fn: Callable[[Any], str] = lambda x: x,
34+
) -> None:
35+
self.dataset_name_or_id = dataset_name_or_id
36+
self.resource = None
37+
super().__init__(path, read_fn, write_fn)
38+
39+
def read(self) -> Any:
40+
"""Read state from HDX dataset
41+
42+
Returns:
43+
Any: State
44+
"""
45+
dataset = Dataset.read_from_hdx(self.dataset_name_or_id)
46+
self.resource = dataset.get_resource()
47+
_, path = self.resource.download()
48+
value = self.read_fn(load_text(path))
49+
logger.info(f"State read from {self.dataset_name_or_id} = {value}")
50+
return value
51+
52+
def write(self) -> None:
53+
"""Write state to HDX dataset
54+
55+
Returns:
56+
None
57+
"""
58+
logger.info(f"State written to {self.dataset_name_or_id} = {self.state}")
59+
filename = self.resource["name"]
60+
file_to_upload = join(self.path, filename)
61+
save_text(self.write_fn(self.state), file_to_upload)
62+
self.resource.set_file_to_upload(file_to_upload)
63+
self.resource.update_in_hdx()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DEFAULT=2020-09-23
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
2020-09-23
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
"""HDXState Utility Tests"""
2+
3+
import json
4+
from copy import deepcopy
5+
from datetime import datetime, timezone
6+
from os import makedirs
7+
from os.path import exists, join
8+
from shutil import copyfile, rmtree
9+
from tempfile import gettempdir
10+
11+
import pytest
12+
13+
from tests.hdx.data import MockResponse, dataset_resultdict
14+
from tests.hdx.data.test_resource import resultdict
15+
16+
from hdx.api.configuration import Configuration
17+
from hdx.api.utilities.hdx_state import HDXState
18+
from hdx.utilities.dateparse import iso_string_from_datetime, parse_date
19+
20+
21+
class TestState:
22+
@pytest.fixture(scope="class")
23+
def tempfolder(self):
24+
return join(gettempdir(), "test_state")
25+
26+
@pytest.fixture(scope="class")
27+
def statefolder(self, fixturesfolder):
28+
return join(fixturesfolder, "state")
29+
30+
@pytest.fixture(scope="class")
31+
def statefile(self):
32+
return "last_build_date.txt"
33+
34+
@pytest.fixture(scope="class")
35+
def multidatestatefile(self):
36+
return "analysis_dates.txt"
37+
38+
@pytest.fixture(scope="class")
39+
def date1(self):
40+
return datetime(2020, 9, 23, 0, 0, tzinfo=timezone.utc)
41+
42+
@pytest.fixture(scope="class")
43+
def date2(self):
44+
return datetime(2022, 5, 12, 10, 15, tzinfo=timezone.utc)
45+
46+
@pytest.fixture(scope="function")
47+
def do_state(self, tempfolder, statefile):
48+
class MockSession:
49+
@staticmethod
50+
def post(url, data, headers, files, allow_redirects, auth=None):
51+
if "resource" in url:
52+
result = json.dumps(resultdict)
53+
return MockResponse(
54+
200,
55+
'{"success": true, "result": %s, "help": "http://test-data.humdata.org/api/3/action/help_show?name=resource_show"}'
56+
% result,
57+
)
58+
else:
59+
myresultdict = deepcopy(dataset_resultdict)
60+
resource = myresultdict["resources"][0]
61+
resource["name"] = statefile
62+
resource["url"] = join(tempfolder, statefile)
63+
result = json.dumps(myresultdict)
64+
return MockResponse(
65+
200,
66+
'{"success": true, "result": %s, "help": "http://test-data.humdata.org/api/3/action/help_show?name=package_show"}'
67+
% result,
68+
)
69+
70+
Configuration.read().remoteckan().session = MockSession()
71+
72+
@pytest.fixture(scope="function")
73+
def do_state_multi(self, tempfolder, multidatestatefile):
74+
class MockSession:
75+
@staticmethod
76+
def post(url, data, headers, files, allow_redirects, auth=None):
77+
if "resource" in url:
78+
result = json.dumps(resultdict)
79+
return MockResponse(
80+
200,
81+
'{"success": true, "result": %s, "help": "http://test-data.humdata.org/api/3/action/help_show?name=resource_show"}'
82+
% result,
83+
)
84+
else:
85+
myresultdict = deepcopy(dataset_resultdict)
86+
resource = myresultdict["resources"][0]
87+
resource["name"] = multidatestatefile
88+
resource["url"] = join(tempfolder, multidatestatefile)
89+
result = json.dumps(myresultdict)
90+
return MockResponse(
91+
200,
92+
'{"success": true, "result": %s, "help": "http://test-data.humdata.org/api/3/action/help_show?name=package_show"}'
93+
% result,
94+
)
95+
96+
Configuration.read().remoteckan().session = MockSession()
97+
98+
def test_state(
99+
self, tempfolder, statefolder, statefile, date1, date2, configuration, do_state
100+
):
101+
if not exists(tempfolder):
102+
makedirs(tempfolder)
103+
statepath = join(tempfolder, statefile)
104+
copyfile(join(statefolder, statefile), statepath)
105+
with HDXState(
106+
"test_dataset", tempfolder, parse_date, iso_string_from_datetime
107+
) as state:
108+
assert state.get() == date1
109+
with HDXState(
110+
"test_dataset", tempfolder, parse_date, iso_string_from_datetime
111+
) as state:
112+
assert state.get() == date1
113+
state.set(date2)
114+
with HDXState(
115+
"test_dataset", tempfolder, parse_date, iso_string_from_datetime
116+
) as state:
117+
assert state.get() == date2.replace(hour=0, minute=0)
118+
rmtree(tempfolder)
119+
120+
def test_multi_date_state(
121+
self,
122+
tempfolder,
123+
statefolder,
124+
multidatestatefile,
125+
date1,
126+
date2,
127+
configuration,
128+
do_state_multi,
129+
):
130+
if not exists(tempfolder):
131+
makedirs(tempfolder)
132+
statepath = join(tempfolder, multidatestatefile)
133+
copyfile(join(statefolder, multidatestatefile), statepath)
134+
with HDXState(
135+
"test_dataset",
136+
tempfolder,
137+
HDXState.dates_str_to_country_date_dict,
138+
HDXState.country_date_dict_to_dates_str,
139+
) as state:
140+
state_dict = state.get()
141+
assert state_dict == {"DEFAULT": date1}
142+
with HDXState(
143+
"test_dataset",
144+
tempfolder,
145+
HDXState.dates_str_to_country_date_dict,
146+
HDXState.country_date_dict_to_dates_str,
147+
) as state:
148+
state_dict = state.get()
149+
assert state_dict == {"DEFAULT": date1}
150+
state_dict["AFG"] = date2
151+
state.set(state_dict)
152+
with HDXState(
153+
"test_dataset",
154+
tempfolder,
155+
HDXState.dates_str_to_country_date_dict,
156+
HDXState.country_date_dict_to_dates_str,
157+
) as state:
158+
state_dict = state.get()
159+
assert state_dict == {
160+
"DEFAULT": date1,
161+
"AFG": date2.replace(hour=0, minute=0),
162+
}
163+
rmtree(tempfolder)

0 commit comments

Comments
 (0)