Skip to content

Commit 166fc65

Browse files
authored
Merge branch 'main' into docs-for-get-publish
2 parents dd2ba38 + 1f6fed4 commit 166fc65

9 files changed

Lines changed: 490 additions & 93 deletions

File tree

CONTRIBUTING.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,32 @@ $env:OPENML_TEST_SERVER_ADMIN_KEY = "admin-key"
107107
export OPENML_TEST_SERVER_ADMIN_KEY="admin-key"
108108
```
109109
110+
#### Diagnosing Slow Tests
111+
112+
If you suspect a test (or the suite as a whole) is running too slowly, `pytest` already exposes everything you need to investigate it. A few invocations that are useful when looking into test runtimes:
113+
114+
```bash
115+
# Show the 20 slowest tests (use 0 to list every test's duration)
116+
pytest tests --durations=20
117+
118+
# Fail any test that exceeds the given timeout (requires pytest-timeout)
119+
pytest tests --timeout=600
120+
121+
# Investigate only fixture/setup costs without actually running the tests
122+
pytest tests --setup-only
123+
124+
# Profile a specific module, class, or test
125+
pytest tests/test_datasets/test_dataset.py --durations=0
126+
127+
# Skip the slow live-server tests while profiling locally
128+
pytest tests --durations=0 -m "not production_server and not test_server"
129+
130+
# Run the suite in parallel to reproduce CI behaviour (requires pytest-xdist)
131+
pytest tests -n 4 --dist=load --durations=0
132+
```
133+
134+
Combining these with the marker filters (`production_server`, `test_server`, `sklearn`) makes it straightforward to narrow the investigation down to the slow tests without changing project configuration.
135+
110136
### Pull Request Checklist
111137
112138
You can go to the `openml-python` GitHub repository to create the pull request by [comparing the branch](https://github.com/openml/openml-python/compare) from your fork with the `main` branch of the `openml-python` repository. When creating a pull request, make sure to follow the comments and structured provided by the template on GitHub.
@@ -214,4 +240,4 @@ When dependencies are installed, run
214240
```bash
215241
mkdocs serve
216242
```
217-
This will open a preview of the website.
243+
This will open a preview of the website.

openml/_api/resources/base/resources.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
from .base import ResourceAPI
1111

1212
if TYPE_CHECKING:
13+
import pandas as pd
14+
1315
from openml.estimation_procedures import OpenMLEstimationProcedure
1416
from openml.evaluations import OpenMLEvaluation
1517
from openml.flows.flow import OpenMLFlow
@@ -80,6 +82,17 @@ class StudyAPI(ResourceAPI):
8082

8183
resource_type: ResourceType = ResourceType.STUDY
8284

85+
@abstractmethod
86+
def list( # noqa: PLR0913
87+
self,
88+
limit: int | None = None,
89+
offset: int | None = None,
90+
status: str | None = None,
91+
main_entity_type: str | None = None,
92+
uploader: list[int] | None = None,
93+
benchmark_suite: int | None = None,
94+
) -> pd.DataFrame: ...
95+
8396

8497
class RunAPI(ResourceAPI):
8598
"""Abstract API interface for run resources."""

openml/_api/resources/base/versions.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
ResourceType.DATASET,
2626
ResourceType.TASK,
2727
ResourceType.FLOW,
28+
ResourceType.STUDY,
2829
ResourceType.SETUP,
2930
ResourceType.RUN,
3031
]

openml/_api/resources/study.py

Lines changed: 154 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,162 @@
11
from __future__ import annotations
22

3-
from .base import ResourceV1API, ResourceV2API, StudyAPI
3+
import builtins
4+
5+
import pandas as pd
6+
import xmltodict
7+
8+
from openml._api.resources.base import ResourceV1API, ResourceV2API, StudyAPI
49

510

611
class StudyV1API(ResourceV1API, StudyAPI):
7-
"""Version 1 API implementation for study resources."""
12+
def list( # noqa: PLR0913
13+
self,
14+
limit: int | None = None,
15+
offset: int | None = None,
16+
status: str | None = None,
17+
main_entity_type: str | None = None,
18+
uploader: builtins.list[int] | None = None,
19+
benchmark_suite: int | None = None,
20+
) -> pd.DataFrame:
21+
"""List studies using V1 API.
22+
23+
Parameters
24+
----------
25+
limit : int, optional
26+
Maximum number of studies to return.
27+
offset : int, optional
28+
Number of studies to skip.
29+
status : str, optional
30+
Filter by status (active, in_preparation, deactivated, all).
31+
main_entity_type : str, optional
32+
Filter by main entity type (run, task).
33+
uploader : list[int], optional
34+
Filter by uploader IDs.
35+
benchmark_suite : int, optional
36+
Filter by benchmark suite ID.
37+
38+
Returns
39+
-------
40+
pd.DataFrame
41+
DataFrame containing study information.
42+
"""
43+
api_call = self._build_url(
44+
limit=limit,
45+
offset=offset,
46+
status=status,
47+
main_entity_type=main_entity_type,
48+
uploader=uploader,
49+
benchmark_suite=benchmark_suite,
50+
)
51+
response = self._http.get(api_call)
52+
xml_string = response.content.decode("utf-8")
53+
return self._parse_list_xml(xml_string)
54+
55+
@staticmethod
56+
def _build_url( # noqa: PLR0913
57+
limit: int | None = None,
58+
offset: int | None = None,
59+
status: str | None = None,
60+
main_entity_type: str | None = None,
61+
uploader: builtins.list[int] | None = None,
62+
benchmark_suite: int | None = None,
63+
) -> str:
64+
"""Build the V1 API URL for listing studies.
65+
66+
Parameters
67+
----------
68+
limit : int, optional
69+
Maximum number of studies to return.
70+
offset : int, optional
71+
Number of studies to skip.
72+
status : str, optional
73+
Filter by status (active, in_preparation, deactivated, all).
74+
main_entity_type : str, optional
75+
Filter by main entity type (run, task).
76+
uploader : list[int], optional
77+
Filter by uploader IDs.
78+
benchmark_suite : int, optional
79+
Filter by benchmark suite ID.
80+
81+
Returns
82+
-------
83+
str
84+
The API call string with all filters applied.
85+
"""
86+
api_call = "study/list"
87+
88+
if limit is not None:
89+
api_call += f"/limit/{limit}"
90+
if offset is not None:
91+
api_call += f"/offset/{offset}"
92+
if status is not None:
93+
api_call += f"/status/{status}"
94+
if main_entity_type is not None:
95+
api_call += f"/main_entity_type/{main_entity_type}"
96+
if uploader is not None:
97+
api_call += f"/uploader/{','.join(str(u) for u in uploader)}"
98+
if benchmark_suite is not None:
99+
api_call += f"/benchmark_suite/{benchmark_suite}"
100+
101+
return api_call
102+
103+
@staticmethod
104+
def _parse_list_xml(xml_string: str) -> pd.DataFrame:
105+
"""Parse the XML response from study list API.
106+
107+
Parameters
108+
----------
109+
xml_string : str
110+
The XML response from the API.
111+
112+
Returns
113+
-------
114+
pd.DataFrame
115+
DataFrame containing study information.
116+
"""
117+
study_dict = xmltodict.parse(xml_string, force_list=("oml:study",))
118+
119+
# Minimalistic check if the XML is useful
120+
assert isinstance(study_dict["oml:study_list"]["oml:study"], list), type(
121+
study_dict["oml:study_list"],
122+
)
123+
assert study_dict["oml:study_list"]["@xmlns:oml"] == "http://openml.org/openml", study_dict[
124+
"oml:study_list"
125+
]["@xmlns:oml"]
126+
127+
studies = {}
128+
for study_ in study_dict["oml:study_list"]["oml:study"]:
129+
# maps from xml name to a tuple of (dict name, casting fn)
130+
expected_fields = {
131+
"oml:id": ("id", int),
132+
"oml:alias": ("alias", str),
133+
"oml:main_entity_type": ("main_entity_type", str),
134+
"oml:benchmark_suite": ("benchmark_suite", int),
135+
"oml:name": ("name", str),
136+
"oml:status": ("status", str),
137+
"oml:creation_date": ("creation_date", str),
138+
"oml:creator": ("creator", int),
139+
}
140+
study_id = int(study_["oml:id"])
141+
current_study = {}
142+
for oml_field_name, (real_field_name, cast_fn) in expected_fields.items():
143+
if oml_field_name in study_:
144+
current_study[real_field_name] = cast_fn(study_[oml_field_name])
145+
current_study["id"] = int(current_study["id"])
146+
studies[study_id] = current_study
147+
148+
return pd.DataFrame.from_dict(studies, orient="index")
8149

9150

10151
class StudyV2API(ResourceV2API, StudyAPI):
11-
"""Version 2 API implementation for study resources."""
152+
def list( # noqa: PLR0913
153+
self,
154+
limit: int | None = None, # noqa: ARG002
155+
offset: int | None = None, # noqa: ARG002
156+
status: str | None = None, # noqa: ARG002
157+
main_entity_type: str | None = None, # noqa: ARG002
158+
uploader: builtins.list[int] | None = None, # noqa: ARG002
159+
benchmark_suite: int | None = None, # noqa: ARG002
160+
) -> pd.DataFrame:
161+
"""V2 API for listing studies is not yet available."""
162+
self._not_supported(method="list")

openml/datasets/functions.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,11 @@ def get_datasets(
364364
-------
365365
datasets : list of datasets
366366
A list of dataset objects.
367+
368+
Examples
369+
--------
370+
>>> import openml
371+
>>> datasets = openml.datasets.get_datasets([1, 2, 3]) # doctest: +SKIP
367372
"""
368373
datasets = []
369374
for dataset_id in dataset_ids:
@@ -446,6 +451,13 @@ def get_dataset( # noqa: C901, PLR0912
446451
-------
447452
dataset : :class:`openml.OpenMLDataset`
448453
The downloaded dataset.
454+
455+
Examples
456+
--------
457+
>>> import openml
458+
>>> dataset = openml.datasets.get_dataset(1) # doctest: +SKIP
459+
>>> dataset = openml.datasets.get_dataset("iris", version=1) # doctest: +SKIP
460+
>>> dataset = openml.datasets.get_dataset(1, download_data=True) # doctest: +SKIP
449461
"""
450462
if download_all_files:
451463
warnings.warn(

openml/runs/functions.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,15 @@ def run_model_on_task( # noqa: PLR0913
103103
Result of the run.
104104
flow : OpenMLFlow (optional, only if `return_flow` is True).
105105
Flow generated from the model.
106+
107+
Examples
108+
--------
109+
>>> import openml
110+
>>> import openml_sklearn # doctest: +SKIP
111+
>>> from sklearn.tree import DecisionTreeClassifier # doctest: +SKIP
112+
>>> clf = DecisionTreeClassifier() # doctest: +SKIP
113+
>>> task = openml.tasks.get_task(6) # doctest: +SKIP
114+
>>> run = openml.runs.run_model_on_task(clf, task) # doctest: +SKIP
106115
"""
107116
if avoid_duplicate_runs is None:
108117
avoid_duplicate_runs = openml.config.avoid_duplicate_runs
@@ -558,9 +567,14 @@ def _run_task_get_arffcontent( # noqa: PLR0915, PLR0912, C901
558567
) # job_rvals contain the output of all the runs with one-to-one correspondence with `jobs`
559568

560569
for n_fit, rep_no, fold_no, sample_no in jobs:
561-
pred_y, proba_y, test_indices, test_y, inner_trace, user_defined_measures_fold = job_rvals[
562-
n_fit - 1
563-
]
570+
(
571+
pred_y,
572+
proba_y,
573+
test_indices,
574+
test_y,
575+
inner_trace,
576+
user_defined_measures_fold,
577+
) = job_rvals[n_fit - 1]
564578

565579
if inner_trace is not None:
566580
traces.append(inner_trace)
@@ -845,7 +859,10 @@ def get_run(run_id: int, ignore_cache: bool = False) -> OpenMLRun: # noqa: FBT0
845859
return _create_run_from_xml(run_xml)
846860

847861

848-
def _create_run_from_xml(xml: str, from_server: bool = True) -> OpenMLRun: # noqa: PLR0915, PLR0912, C901, FBT002
862+
def _create_run_from_xml( # noqa: PLR0915, PLR0912, C901
863+
xml: str,
864+
from_server: bool = True, # noqa: FBT002
865+
) -> OpenMLRun:
849866
"""Create a run object from xml returned from server.
850867
851868
Parameters

0 commit comments

Comments
 (0)