Skip to content

Commit 0af278a

Browse files
committed
Merge branch 'runs-migration-stacked' of https://github.com/Omswastik-11/openml-python into runs-migration-stacked
2 parents 79db213 + 855973e commit 0af278a

17 files changed

Lines changed: 724 additions & 307 deletions

File tree

openml/_api/clients/http.py

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,26 @@ def _key_to_path(self, key: str) -> Path:
8989
"""
9090
return self.path.joinpath(key)
9191

92+
def _get_body_filename_from_response(self, response: Response) -> str:
93+
content_type = response.headers.get("Content-Type", "").lower()
94+
95+
if "application/json" in content_type:
96+
return "body.json"
97+
98+
if "text/xml" in content_type:
99+
return "body.xml"
100+
101+
return "body.txt"
102+
103+
def _get_body_filename_from_path(self, path: Path) -> str:
104+
if (path / "body.json").exists():
105+
return "body.json"
106+
107+
if (path / "body.xml").exists():
108+
return "body.xml"
109+
110+
return "body.txt"
111+
92112
def load(self, key: str) -> Response:
93113
"""
94114
Load a cached HTTP response from disk.
@@ -112,31 +132,26 @@ def load(self, key: str) -> Response:
112132
"""
113133
path = self._key_to_path(key)
114134

115-
if not path.exists():
116-
raise FileNotFoundError(f"Cache entry not found: {path}")
117-
118135
meta_path = path / "meta.json"
119-
headers_path = path / "headers.json"
120-
body_path = path / "body.bin"
136+
meta_raw = meta_path.read_bytes() if meta_path.exists() else "{}"
137+
meta = json.loads(meta_raw)
121138

122-
if not (meta_path.exists() and headers_path.exists() and body_path.exists()):
123-
raise FileNotFoundError(f"Incomplete cache at {path}")
124-
125-
with meta_path.open("r", encoding="utf-8") as f:
126-
meta = json.load(f)
127-
128-
with headers_path.open("r", encoding="utf-8") as f:
129-
headers = json.load(f)
139+
headers_path = path / "headers.json"
140+
headers_raw = headers_path.read_bytes() if headers_path.exists() else "{}"
141+
headers = json.loads(headers_raw)
130142

143+
body_path = path / self._get_body_filename_from_path(path)
144+
if not body_path.exists():
145+
raise FileNotFoundError(f"Incomplete cache at {body_path}")
131146
body = body_path.read_bytes()
132147

133148
response = Response()
134-
response.status_code = meta["status_code"]
135-
response.url = meta["url"]
136-
response.reason = meta["reason"]
137149
response.headers = headers
138150
response._content = body
139-
response.encoding = meta["encoding"]
151+
response.status_code = meta.get("status_code")
152+
response.url = meta.get("url")
153+
response.reason = meta.get("reason")
154+
response.encoding = meta.get("encoding")
140155

141156
return response
142157

@@ -160,7 +175,9 @@ def save(self, key: str, response: Response) -> None:
160175
path = self._key_to_path(key)
161176
path.mkdir(parents=True, exist_ok=True)
162177

163-
(path / "body.bin").write_bytes(response.content)
178+
body_filename = self._get_body_filename_from_response(response)
179+
with (path / body_filename).open("wb") as f:
180+
f.write(response.content)
164181

165182
with (path / "headers.json").open("w", encoding="utf-8") as f:
166183
json.dump(dict(response.headers), f)

openml/_api/resources/base/resources.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import builtins
44
from abc import abstractmethod
5+
from collections.abc import Iterable
56
from typing import TYPE_CHECKING, Any
67

78
from openml.enums import ResourceType
@@ -11,8 +12,11 @@
1112
if TYPE_CHECKING:
1213
import pandas as pd
1314

15+
from openml.estimation_procedures import OpenMLEstimationProcedure
1416
from openml.evaluations import OpenMLEvaluation
17+
from openml.flows.flow import OpenMLFlow
1518
from openml.runs.run import OpenMLRun
19+
from openml.setups.setup import OpenMLSetup
1620
from openml.tasks.task import TaskType
1721

1822

@@ -33,12 +37,18 @@ class EvaluationMeasureAPI(ResourceAPI):
3337

3438
resource_type: ResourceType = ResourceType.EVALUATION_MEASURE
3539

40+
@abstractmethod
41+
def list(self) -> list[str]: ...
42+
3643

3744
class EstimationProcedureAPI(ResourceAPI):
3845
"""Abstract API interface for estimation procedure resources."""
3946

4047
resource_type: ResourceType = ResourceType.ESTIMATION_PROCEDURE
4148

49+
@abstractmethod
50+
def list(self) -> list[OpenMLEstimationProcedure]: ...
51+
4252

4353
class EvaluationAPI(ResourceAPI):
4454
"""Abstract API interface for evaluation resources."""
@@ -109,3 +119,24 @@ class SetupAPI(ResourceAPI):
109119
"""Abstract API interface for setup resources."""
110120

111121
resource_type: ResourceType = ResourceType.SETUP
122+
123+
@abstractmethod
124+
def list(
125+
self,
126+
limit: int,
127+
offset: int,
128+
*,
129+
setup: Iterable[int] | None = None,
130+
flow: int | None = None,
131+
tag: str | None = None,
132+
) -> list[OpenMLSetup]: ...
133+
134+
@abstractmethod
135+
def get(self, setup_id: int) -> OpenMLSetup: ...
136+
137+
@abstractmethod
138+
def exists(
139+
self,
140+
flow: OpenMLFlow,
141+
param_settings: builtins.list[dict[str, Any]],
142+
) -> int | bool: ...
Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,84 @@
11
from __future__ import annotations
22

3+
import warnings
4+
5+
import xmltodict
6+
7+
from openml.estimation_procedures.estimation_procedure import OpenMLEstimationProcedure
8+
from openml.tasks.task import TaskType
9+
310
from .base import EstimationProcedureAPI, ResourceV1API, ResourceV2API
411

512

613
class EstimationProcedureV1API(ResourceV1API, EstimationProcedureAPI):
7-
"""Version 1 API implementation for estimation procedure resources."""
14+
"""V1 API implementation for estimation procedures.
15+
16+
Fetches estimation procedures from the v1 XML API endpoint.
17+
"""
18+
19+
def list(self) -> list[OpenMLEstimationProcedure]:
20+
"""Return a list of all estimation procedures which are on OpenML.
21+
22+
Returns
23+
-------
24+
procedures : list
25+
A list of all estimation procedures. Every procedure is represented by
26+
a dictionary containing the following information: id, task type id,
27+
name, type, repeats, folds, stratified.
28+
"""
29+
path = "estimationprocedure/list"
30+
response = self._http.get(path)
31+
xml_content = response.text
32+
33+
procs_dict = xmltodict.parse(xml_content)
34+
35+
# Minimalistic check if the XML is useful
36+
if "oml:estimationprocedures" not in procs_dict:
37+
raise ValueError("Error in return XML, does not contain tag oml:estimationprocedures.")
38+
39+
if "@xmlns:oml" not in procs_dict["oml:estimationprocedures"]:
40+
raise ValueError(
41+
"Error in return XML, does not contain tag "
42+
"@xmlns:oml as a child of oml:estimationprocedures.",
43+
)
44+
45+
if procs_dict["oml:estimationprocedures"]["@xmlns:oml"] != "http://openml.org/openml":
46+
raise ValueError(
47+
"Error in return XML, value of "
48+
"oml:estimationprocedures/@xmlns:oml is not "
49+
"http://openml.org/openml, but {}".format(
50+
str(procs_dict["oml:estimationprocedures"]["@xmlns:oml"])
51+
),
52+
)
53+
54+
procs: list[OpenMLEstimationProcedure] = []
55+
for proc_ in procs_dict["oml:estimationprocedures"]["oml:estimationprocedure"]:
56+
task_type_int = int(proc_["oml:ttid"])
57+
try:
58+
task_type_id = TaskType(task_type_int)
59+
procs.append(
60+
OpenMLEstimationProcedure(
61+
id=int(proc_["oml:id"]),
62+
task_type_id=task_type_id,
63+
name=proc_["oml:name"],
64+
type=proc_["oml:type"],
65+
)
66+
)
67+
except ValueError as e:
68+
warnings.warn(
69+
f"Could not create task type id for {task_type_int} due to error {e}",
70+
RuntimeWarning,
71+
stacklevel=2,
72+
)
73+
74+
return procs
875

976

1077
class EstimationProcedureV2API(ResourceV2API, EstimationProcedureAPI):
11-
"""Version 2 API implementation for estimation procedure resources."""
78+
"""V2 API implementation for estimation procedures.
79+
80+
Fetches estimation procedures from the v2 JSON API endpoint.
81+
"""
82+
83+
def list(self) -> list[OpenMLEstimationProcedure]:
84+
self._not_supported(method="list")
Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,60 @@
11
from __future__ import annotations
22

3+
import xmltodict
4+
35
from .base import EvaluationMeasureAPI, ResourceV1API, ResourceV2API
46

57

68
class EvaluationMeasureV1API(ResourceV1API, EvaluationMeasureAPI):
7-
"""Version 1 API implementation for evaluation measure resources."""
9+
"""V1 API implementation for evaluation measures.
10+
11+
Fetches evaluation measures from the v1 XML API endpoint.
12+
"""
13+
14+
def list(self) -> list[str]:
15+
"""List all evaluation measures available on OpenML.
16+
17+
Returns
18+
-------
19+
list[str]
20+
A list of evaluation measure names.
21+
"""
22+
path = "evaluationmeasure/list"
23+
response = self._http.get(path)
24+
xml_content = response.text
25+
26+
qualities = xmltodict.parse(xml_content, force_list=("oml:measures"))
27+
# Minimalistic check if the XML is useful
28+
if "oml:evaluation_measures" not in qualities:
29+
raise ValueError('Error in return XML, does not contain "oml:evaluation_measures"')
30+
31+
if not isinstance(
32+
qualities["oml:evaluation_measures"]["oml:measures"][0]["oml:measure"], list
33+
):
34+
raise TypeError('Error in return XML, does not contain "oml:measure" as a list')
35+
36+
return qualities["oml:evaluation_measures"]["oml:measures"][0]["oml:measure"]
837

938

1039
class EvaluationMeasureV2API(ResourceV2API, EvaluationMeasureAPI):
11-
"""Version 2 API implementation for evaluation measure resources."""
40+
"""V2 API implementation for evaluation measures.
41+
42+
Fetches evaluation measures from the v2 JSON API endpoint.
43+
"""
44+
45+
def list(self) -> list[str]:
46+
"""List all evaluation measures available on OpenML.
47+
48+
Returns
49+
-------
50+
list[str]
51+
A list of evaluation measure names.
52+
"""
53+
path = "evaluationmeasure/list"
54+
response = self._http.get(path)
55+
data = response.json()
56+
57+
if not isinstance(data, list):
58+
raise ValueError(f"Expected list, got {type(data)}")
59+
60+
return data

0 commit comments

Comments
 (0)