Skip to content

Commit c7c24a3

Browse files
committed
Merge branch 'migration' of https://github.com/geetu040/openml-python into flow-migration-stacked
2 parents 920ff21 + 2b2db96 commit c7c24a3

2 files changed

Lines changed: 228 additions & 0 deletions

File tree

openml/testing.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,13 @@
1111
import unittest
1212
from pathlib import Path
1313
from typing import ClassVar
14+
from urllib.parse import urljoin
1415

1516
import requests
1617

1718
import openml
19+
from openml._api.clients import HTTPCache, HTTPClient
20+
from openml._api.config import RetryPolicy
1821
from openml.exceptions import OpenMLServerException
1922
from openml.tasks import TaskType
2023

@@ -276,6 +279,91 @@ def _check_fold_timing_evaluations( # noqa: PLR0913
276279
assert evaluation <= max_val
277280

278281

282+
class TestAPIBase(unittest.TestCase):
283+
server: str
284+
base_url: str
285+
api_key: str
286+
timeout: int
287+
retries: int
288+
retry_policy: RetryPolicy
289+
dir: str
290+
ttl: int
291+
cache: HTTPCache
292+
http_client: HTTPClient
293+
294+
def setUp(self) -> None:
295+
self.server = "https://test.openml.org/"
296+
self.base_url = "api/v1/xml"
297+
self.api_key = "normaluser"
298+
self.timeout = 10
299+
self.retries = 3
300+
self.retry_policy = RetryPolicy.HUMAN
301+
self.dir = "test_cache"
302+
self.ttl = 60 * 60 * 24 * 7
303+
304+
self.cache = self._get_http_cache(
305+
path=Path(self.dir),
306+
ttl=self.ttl,
307+
)
308+
self.http_client = self._get_http_client(
309+
server=self.server,
310+
base_url=self.base_url,
311+
api_key=self.api_key,
312+
timeout=self.timeout,
313+
retries=self.retries,
314+
retry_policy=self.retry_policy,
315+
cache=self.cache,
316+
)
317+
318+
if self.cache.path.exists():
319+
shutil.rmtree(self.cache.path)
320+
321+
def tearDown(self) -> None:
322+
if self.cache.path.exists():
323+
shutil.rmtree(self.cache.path)
324+
325+
def _get_http_cache(
326+
self,
327+
path: Path,
328+
ttl: int,
329+
) -> HTTPCache:
330+
return HTTPCache(
331+
path=path,
332+
ttl=ttl,
333+
)
334+
335+
def _get_http_client( # noqa: PLR0913
336+
self,
337+
server: str,
338+
base_url: str,
339+
api_key: str,
340+
timeout: int,
341+
retries: int,
342+
retry_policy: RetryPolicy,
343+
cache: HTTPCache | None = None,
344+
) -> HTTPClient:
345+
return HTTPClient(
346+
server=server,
347+
base_url=base_url,
348+
api_key=api_key,
349+
timeout=timeout,
350+
retries=retries,
351+
retry_policy=retry_policy,
352+
cache=cache,
353+
)
354+
355+
def _get_url(
356+
self,
357+
server: str | None = None,
358+
base_url: str | None = None,
359+
path: str | None = None,
360+
) -> str:
361+
server = server if server else self.server
362+
base_url = base_url if base_url else self.base_url
363+
path = path if path else ""
364+
return urljoin(self.server, urljoin(self.base_url, path))
365+
366+
279367
def check_task_existence(
280368
task_type: TaskType,
281369
dataset_id: int,

tests/test_api/test_http.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
from requests import Response, Request
2+
import time
3+
import xmltodict
4+
import pytest
5+
from openml.testing import TestAPIBase
6+
7+
8+
class TestHTTPClient(TestAPIBase):
9+
def test_cache(self):
10+
url = self._get_url(path="task/31")
11+
params = {"param1": "value1", "param2": "value2"}
12+
13+
key = self.cache.get_key(url, params)
14+
15+
# validate key
16+
self.assertEqual(
17+
key,
18+
"org/openml/test/api/v1/task/31/param1=value1&param2=value2",
19+
)
20+
21+
# create fake response
22+
req = Request("GET", url).prepare()
23+
response = Response()
24+
response.status_code = 200
25+
response.url = url
26+
response.reason = "OK"
27+
response._content = b"<xml>test</xml>"
28+
response.headers = {"Content-Type": "text/xml"}
29+
response.encoding = "utf-8"
30+
response.request = req
31+
response.elapsed = type("Elapsed", (), {"total_seconds": lambda self: 0.1})()
32+
33+
# save to cache
34+
self.cache.save(key, response)
35+
36+
# load from cache
37+
cached_response = self.cache.load(key)
38+
39+
# validate loaded response
40+
self.assertEqual(cached_response.status_code, 200)
41+
self.assertEqual(cached_response.url, url)
42+
self.assertEqual(cached_response.content, b"<xml>test</xml>")
43+
self.assertEqual(
44+
cached_response.headers["Content-Type"], "text/xml"
45+
)
46+
47+
@pytest.mark.uses_test_server()
48+
def test_get(self):
49+
response = self.http_client.get("task/1")
50+
51+
self.assertEqual(response.status_code, 200)
52+
self.assertIn(b"<oml:task", response.content)
53+
54+
@pytest.mark.uses_test_server()
55+
def test_get_with_cache_creates_cache(self):
56+
response = self.http_client.get("task/1", use_cache=True)
57+
58+
self.assertEqual(response.status_code, 200)
59+
self.assertTrue(self.cache.path.exists())
60+
61+
# verify cache directory structure exists
62+
cache_key = self.cache.get_key(
63+
self._get_url(path="task/1"),
64+
{},
65+
)
66+
cache_path = self.cache._key_to_path(cache_key)
67+
68+
self.assertTrue((cache_path / "meta.json").exists())
69+
self.assertTrue((cache_path / "headers.json").exists())
70+
self.assertTrue((cache_path / "body.bin").exists())
71+
72+
@pytest.mark.uses_test_server()
73+
def test_get_uses_cached_response(self):
74+
# first request populates cache
75+
response1 = self.http_client.get("task/1", use_cache=True)
76+
77+
# second request should load from cache
78+
response2 = self.http_client.get("task/1", use_cache=True)
79+
80+
self.assertEqual(response1.content, response2.content)
81+
self.assertEqual(response1.status_code, response2.status_code)
82+
83+
@pytest.mark.uses_test_server()
84+
def test_get_cache_expires(self):
85+
# force short TTL
86+
self.cache.ttl = 1
87+
path = "task/1"
88+
89+
url = self._get_url(path=path)
90+
key = self.cache.get_key(url, {})
91+
cache_path = self.cache._key_to_path(key) / "meta.json"
92+
93+
response1 = self.http_client.get(path, use_cache=True)
94+
response1_cache_time_stamp = cache_path.stat().st_ctime
95+
96+
time.sleep(2)
97+
98+
response2 = self.http_client.get(path, use_cache=True)
99+
response2_cache_time_stamp = cache_path.stat().st_ctime
100+
101+
# cache expired -> new request
102+
self.assertNotEqual(response1_cache_time_stamp, response2_cache_time_stamp)
103+
self.assertEqual(response2.status_code, 200)
104+
self.assertEqual(response1.content, response2.content)
105+
106+
@pytest.mark.uses_test_server()
107+
def test_post_and_delete(self):
108+
task_xml = """
109+
<oml:task_inputs xmlns:oml="http://openml.org/openml">
110+
<oml:task_type_id>5</oml:task_type_id>
111+
<oml:input name="source_data">193</oml:input>
112+
<oml:input name="estimation_procedure">17</oml:input>
113+
</oml:task_inputs>
114+
"""
115+
116+
task_id = None
117+
try:
118+
# POST the task
119+
post_response = self.http_client.post(
120+
"task",
121+
files={"description": task_xml},
122+
)
123+
self.assertEqual(post_response.status_code, 200)
124+
xml_resp = xmltodict.parse(post_response.content)
125+
task_id = int(xml_resp["oml:upload_task"]["oml:id"])
126+
127+
# GET the task to verify it exists
128+
get_response = self.http_client.get(f"task/{task_id}")
129+
self.assertEqual(get_response.status_code, 200)
130+
131+
finally:
132+
# DELETE the task if it was created
133+
if task_id is not None:
134+
try:
135+
del_response = self.http_client.delete(f"task/{task_id}")
136+
# optional: verify delete
137+
if del_response.status_code != 200:
138+
print(f"Warning: delete failed for task {task_id}")
139+
except Exception as e:
140+
print(f"Warning: failed to delete task {task_id}: {e}")

0 commit comments

Comments
 (0)