Skip to content

Commit 846026a

Browse files
[ENH] V1 → V2 API Migration - setups
1 parent 5762185 commit 846026a

5 files changed

Lines changed: 238 additions & 117 deletions

File tree

openml/_api/resources/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from openml._api.resources.datasets import DatasetsV1, DatasetsV2
2+
from openml._api.resources.setups import SetupsV1, SetupsV2
23
from openml._api.resources.tasks import TasksV1, TasksV2
34

4-
__all__ = ["DatasetsV1", "DatasetsV2", "TasksV1", "TasksV2"]
5+
__all__ = ["DatasetsV1", "DatasetsV2", "SetupsV1", "SetupsV2", "TasksV1", "TasksV2"]

openml/_api/resources/base.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
from __future__ import annotations
22

33
from abc import ABC, abstractmethod
4+
from collections.abc import Iterable
45
from typing import TYPE_CHECKING
56

67
if TYPE_CHECKING:
78
from requests import Response
89

910
from openml._api.http import HTTPClient
1011
from openml.datasets.dataset import OpenMLDataset
12+
from openml.setups.setup import OpenMLSetup
1113
from openml.tasks.task import OpenMLTask
1214

1315

@@ -29,3 +31,25 @@ def get(
2931
*,
3032
return_response: bool = False,
3133
) -> OpenMLTask | tuple[OpenMLTask, Response]: ...
34+
35+
36+
class SetupsAPI(ResourceAPI, ABC):
37+
@abstractmethod
38+
def list(
39+
self,
40+
limit: int,
41+
offset: int,
42+
*,
43+
setup: Iterable[int] | None = None,
44+
flow: int | None = None,
45+
tag: str | None = None,
46+
) -> list[OpenMLSetup]: ...
47+
48+
@abstractmethod
49+
def _create_setup(self, result_dict: dict) -> OpenMLSetup: ...
50+
51+
@abstractmethod
52+
def get(self, setup_id: int) -> OpenMLSetup: ...
53+
54+
@abstractmethod
55+
def exists(self) -> int: ...

openml/_api/resources/setups.py

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
from __future__ import annotations
2+
3+
from collections.abc import Iterable
4+
5+
import xmltodict
6+
7+
from openml._api.resources.base import SetupsAPI
8+
from openml.setups.setup import OpenMLParameter, OpenMLSetup
9+
10+
11+
class SetupsV1(SetupsAPI):
12+
"""V1 XML API implementation for setups."""
13+
14+
def list(
15+
self,
16+
limit: int,
17+
offset: int,
18+
*,
19+
setup: Iterable[int] | None = None,
20+
flow: int | None = None,
21+
tag: str | None = None,
22+
) -> list[OpenMLSetup]:
23+
"""Perform API call `/setup/list/{filters}`
24+
25+
Parameters
26+
----------
27+
The setup argument that is a list is separated from the single value
28+
filters which are put into the kwargs.
29+
30+
limit : int
31+
offset : int
32+
setup : list(int), optional
33+
flow : int, optional
34+
tag : str, optional
35+
36+
Returns
37+
-------
38+
list
39+
setups that match the filters, going from id to the OpenMLSetup object.
40+
"""
41+
api_call = self._build_url(limit, offset, setup=setup, flow=flow, tag=tag)
42+
setup_response = self._http.get(api_call)
43+
xml_content = setup_response.text
44+
45+
return self._parse_list_xml(xml_content)
46+
47+
def _build_url(
48+
self,
49+
limit: int,
50+
offset: int,
51+
*,
52+
setup: Iterable[int] | None = None,
53+
flow: int | None = None,
54+
tag: str | None = None,
55+
) -> str:
56+
"""Construct an OpenML Setup API URL with filtering parameters.
57+
58+
Parameters
59+
----------
60+
The setup argument that is a list is separated from the single value
61+
filters which are put into the kwargs.
62+
63+
limit : int
64+
offset : int
65+
setup : list(int), optional
66+
flow : int, optional
67+
tag : str, optional
68+
69+
Returns
70+
-------
71+
str
72+
A relative API path suitable for an OpenML HTTP request.
73+
"""
74+
api_call = "setup/list"
75+
if limit is not None:
76+
api_call += f"/limit/{limit}"
77+
if offset is not None:
78+
api_call += f"/offset/{offset}"
79+
if setup is not None:
80+
api_call += f"/setup/{','.join([str(int(i)) for i in setup])}"
81+
if flow is not None:
82+
api_call += f"/flow/{flow}"
83+
if tag is not None:
84+
api_call += f"/tag/{tag}"
85+
86+
return api_call
87+
88+
def _parse_list_xml(self, xml_content: str) -> list[OpenMLSetup]:
89+
"""Helper function to parse API calls which are lists of setups"""
90+
setups_dict = xmltodict.parse(xml_content, force_list=("oml:setup",))
91+
openml_uri = "http://openml.org/openml"
92+
# Minimalistic check if the XML is useful
93+
if "oml:setups" not in setups_dict:
94+
raise ValueError(
95+
f'Error in return XML, does not contain "oml:setups": {setups_dict!s}',
96+
)
97+
98+
if "@xmlns:oml" not in setups_dict["oml:setups"]:
99+
raise ValueError(
100+
f'Error in return XML, does not contain "oml:setups"/@xmlns:oml: {setups_dict!s}',
101+
)
102+
103+
if setups_dict["oml:setups"]["@xmlns:oml"] != openml_uri:
104+
raise ValueError(
105+
"Error in return XML, value of "
106+
'"oml:seyups"/@xmlns:oml is not '
107+
f'"{openml_uri}": {setups_dict!s}',
108+
)
109+
110+
assert isinstance(setups_dict["oml:setups"]["oml:setup"], list), type(
111+
setups_dict["oml:setups"]
112+
)
113+
114+
return [
115+
self._create_setup({"oml:setup_parameters": setup_})
116+
for setup_ in setups_dict["oml:setups"]["oml:setup"]
117+
]
118+
119+
def _create_setup(self, result_dict: dict) -> OpenMLSetup:
120+
"""Turns an API xml result into a OpenMLSetup object (or dict)"""
121+
setup_id = int(result_dict["oml:setup_parameters"]["oml:setup_id"])
122+
flow_id = int(result_dict["oml:setup_parameters"]["oml:flow_id"])
123+
124+
if "oml:parameter" not in result_dict["oml:setup_parameters"]:
125+
return OpenMLSetup(setup_id, flow_id, parameters=None)
126+
127+
xml_parameters = result_dict["oml:setup_parameters"]["oml:parameter"]
128+
if isinstance(xml_parameters, dict):
129+
parameters = {
130+
int(xml_parameters["oml:id"]): self._create_setup_parameter_from_xml(
131+
xml_parameters
132+
),
133+
}
134+
elif isinstance(xml_parameters, list):
135+
parameters = {
136+
int(xml_parameter["oml:id"]): self._create_setup_parameter_from_xml(xml_parameter)
137+
for xml_parameter in xml_parameters
138+
}
139+
else:
140+
raise ValueError(
141+
f"Expected None, list or dict, received something else: {type(xml_parameters)!s}",
142+
)
143+
144+
return OpenMLSetup(setup_id, flow_id, parameters)
145+
146+
def _create_setup_parameter_from_xml(self, result_dict: dict[str, str]) -> OpenMLParameter:
147+
"""Create an OpenMLParameter object or a dictionary from an API xml result."""
148+
return OpenMLParameter(
149+
input_id=int(result_dict["oml:id"]),
150+
flow_id=int(result_dict["oml:flow_id"]),
151+
flow_name=result_dict["oml:flow_name"],
152+
full_name=result_dict["oml:full_name"],
153+
parameter_name=result_dict["oml:parameter_name"],
154+
data_type=result_dict["oml:data_type"],
155+
default_value=result_dict["oml:default_value"],
156+
value=result_dict["oml:value"],
157+
)
158+
159+
def get(self, setup_id: int) -> OpenMLSetup:
160+
"""
161+
Downloads the setup (configuration) description from OpenML
162+
and returns a structured object
163+
164+
Parameters
165+
----------
166+
setup_id : int
167+
The Openml setup_id
168+
169+
Returns
170+
-------
171+
OpenMLSetup (an initialized openml setup object)
172+
"""
173+
174+
def exists(self) -> int:
175+
pass
176+
177+
178+
class SetupsV2(SetupsAPI):
179+
"""V2 JSoN API implementation for setups."""
180+
181+
def list(
182+
self,
183+
limit: int,
184+
offset: int,
185+
*,
186+
setup: Iterable[int] | None = None,
187+
flow: int | None = None,
188+
tag: str | None = None,
189+
) -> list[OpenMLSetup]:
190+
raise NotImplementedError("V2 API implementation is not yet available")
191+
192+
def _create_setup(self, result_dict: dict) -> OpenMLSetup:
193+
raise NotImplementedError("V2 API implementation is not yet available")
194+
195+
def get(self, setup_id: int) -> OpenMLSetup:
196+
raise NotImplementedError("V2 API implementation is not yet available")
197+
198+
def exists(self) -> int:
199+
raise NotImplementedError("V2 API implementation is not yet available")

openml/_api/runtime/core.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,21 @@
77
from openml._api.resources import (
88
DatasetsV1,
99
DatasetsV2,
10+
SetupsV1,
11+
SetupsV2,
1012
TasksV1,
1113
TasksV2,
1214
)
1315

1416
if TYPE_CHECKING:
15-
from openml._api.resources.base import DatasetsAPI, TasksAPI
17+
from openml._api.resources.base import DatasetsAPI, SetupsAPI, TasksAPI
1618

1719

1820
class APIBackend:
19-
def __init__(self, *, datasets: DatasetsAPI, tasks: TasksAPI):
21+
def __init__(self, *, datasets: DatasetsAPI, tasks: TasksAPI, setups: SetupsAPI):
2022
self.datasets = datasets
2123
self.tasks = tasks
24+
self.setups = setups
2225

2326

2427
def build_backend(version: str, *, strict: bool) -> APIBackend:
@@ -28,15 +31,13 @@ def build_backend(version: str, *, strict: bool) -> APIBackend:
2831
v1 = APIBackend(
2932
datasets=DatasetsV1(v1_http),
3033
tasks=TasksV1(v1_http),
34+
setups=SetupsV1(v1_http),
3135
)
3236

3337
if version == "v1":
3438
return v1
3539

36-
v2 = APIBackend(
37-
datasets=DatasetsV2(v2_http),
38-
tasks=TasksV2(v2_http),
39-
)
40+
v2 = APIBackend(datasets=DatasetsV2(v2_http), tasks=TasksV2(v2_http), setups=SetupsV2(v2_http))
4041

4142
if strict:
4243
return v2

0 commit comments

Comments
 (0)