|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
| 3 | +from collections.abc import Mapping |
| 4 | +from typing import Any |
| 5 | + |
3 | 6 | import xmltodict |
4 | 7 |
|
5 | 8 | from openml._api.resources.base import APIVersion, ResourceAPI, ResourceType |
|
13 | 16 | class ResourceV1(ResourceAPI): |
14 | 17 | api_version: APIVersion = APIVersion.V1 |
15 | 18 |
|
| 19 | + def publish(self, path: str, files: Mapping[str, Any] | None) -> int: |
| 20 | + response = self._http.post(path, files=files) |
| 21 | + parsed_response = xmltodict.parse(response.content) |
| 22 | + return self._extract_id_from_upload(parsed_response) |
| 23 | + |
16 | 24 | def delete(self, resource_id: int) -> bool: |
17 | | - if self.resource_type == ResourceType.DATASET: |
18 | | - resource_type = "data" |
19 | | - else: |
20 | | - resource_type = self.resource_type.name |
21 | | - |
22 | | - legal_resources = { |
23 | | - "data", |
24 | | - "flow", |
25 | | - "task", |
26 | | - "run", |
27 | | - "study", |
28 | | - "user", |
29 | | - } |
| 25 | + resource_type = self._get_endpoint_name() |
| 26 | + |
| 27 | + legal_resources = {"data", "flow", "task", "run", "study", "user"} |
30 | 28 | if resource_type not in legal_resources: |
31 | 29 | raise ValueError(f"Can't delete a {resource_type}") |
32 | 30 |
|
33 | | - url_suffix = f"{resource_type}/{resource_id}" |
| 31 | + path = f"{resource_type}/{resource_id}" |
34 | 32 | try: |
35 | | - response = self._http.delete(url_suffix) |
| 33 | + response = self._http.delete(path) |
36 | 34 | result = xmltodict.parse(response.content) |
37 | 35 | return f"oml:{resource_type}_delete" in result |
38 | 36 | except OpenMLServerException as e: |
39 | | - # https://github.com/openml/OpenML/blob/21f6188d08ac24fcd2df06ab94cf421c946971b0/openml_OS/views/pages/api_new/v1/xml/pre.php |
40 | | - # Most exceptions are descriptive enough to be raised as their standard |
41 | | - # OpenMLServerException, however there are two cases where we add information: |
42 | | - # - a generic "failed" message, we direct them to the right issue board |
43 | | - # - when the user successfully authenticates with the server, |
44 | | - # but user is not allowed to take the requested action, |
45 | | - # in which case we specify a OpenMLNotAuthorizedError. |
46 | | - by_other_user = [323, 353, 393, 453, 594] |
47 | | - has_dependent_entities = [324, 326, 327, 328, 354, 454, 464, 595] |
48 | | - unknown_reason = [325, 355, 394, 455, 593] |
49 | | - if e.code in by_other_user: |
50 | | - raise OpenMLNotAuthorizedError( |
51 | | - message=( |
52 | | - f"The {resource_type} can not be deleted " |
53 | | - "because it was not uploaded by you." |
54 | | - ), |
55 | | - ) from e |
56 | | - if e.code in has_dependent_entities: |
57 | | - raise OpenMLNotAuthorizedError( |
58 | | - message=( |
59 | | - f"The {resource_type} can not be deleted because " |
60 | | - f"it still has associated entities: {e.message}" |
61 | | - ), |
62 | | - ) from e |
63 | | - if e.code in unknown_reason: |
64 | | - raise OpenMLServerError( |
65 | | - message=( |
66 | | - f"The {resource_type} can not be deleted for unknown reason," |
67 | | - " please open an issue at: https://github.com/openml/openml/issues/new" |
68 | | - ), |
69 | | - ) from e |
70 | | - raise e |
71 | | - |
72 | | - def publish(self) -> None: |
73 | | - pass |
| 37 | + self._handle_delete_exception(resource_type, e) |
| 38 | + raise |
| 39 | + |
| 40 | + def tag(self, resource_id: int, tag: str) -> list[str]: |
| 41 | + resource_type = self._get_endpoint_name() |
| 42 | + |
| 43 | + legal_resources = {"data", "task", "flow", "setup", "run"} |
| 44 | + if resource_type not in legal_resources: |
| 45 | + raise ValueError(f"Can't tag a {resource_type}") |
| 46 | + |
| 47 | + path = f"{resource_type}/tag" |
| 48 | + data = {f"{resource_type}_id": resource_id, "tag": tag} |
| 49 | + response = self._http.post(path, data=data) |
| 50 | + |
| 51 | + main_tag = f"oml:{resource_type}_tag" |
| 52 | + parsed_response = xmltodict.parse(response.content, force_list={"oml:tag"}) |
| 53 | + result = parsed_response[main_tag] |
| 54 | + tags: list[str] = result.get("oml:tag", []) |
| 55 | + |
| 56 | + return tags |
| 57 | + |
| 58 | + def untag(self, resource_id: int, tag: str) -> list[str]: |
| 59 | + resource_type = self._get_endpoint_name() |
| 60 | + |
| 61 | + legal_resources = {"data", "task", "flow", "setup", "run"} |
| 62 | + if resource_type not in legal_resources: |
| 63 | + raise ValueError(f"Can't tag a {resource_type}") |
| 64 | + |
| 65 | + path = f"{resource_type}/untag" |
| 66 | + data = {f"{resource_type}_id": resource_id, "tag": tag} |
| 67 | + response = self._http.post(path, data=data) |
| 68 | + |
| 69 | + main_tag = f"oml:{resource_type}_untag" |
| 70 | + parsed_response = xmltodict.parse(response.content, force_list={"oml:tag"}) |
| 71 | + result = parsed_response[main_tag] |
| 72 | + tags: list[str] = result.get("oml:tag", []) |
| 73 | + |
| 74 | + return tags |
| 75 | + |
| 76 | + def _get_endpoint_name(self) -> str: |
| 77 | + if self.resource_type == ResourceType.DATASET: |
| 78 | + return "data" |
| 79 | + return self.resource_type.name |
| 80 | + |
| 81 | + def _handle_delete_exception( |
| 82 | + self, resource_type: str, exception: OpenMLServerException |
| 83 | + ) -> None: |
| 84 | + # https://github.com/openml/OpenML/blob/21f6188d08ac24fcd2df06ab94cf421c946971b0/openml_OS/views/pages/api_new/v1/xml/pre.php |
| 85 | + # Most exceptions are descriptive enough to be raised as their standard |
| 86 | + # OpenMLServerException, however there are two cases where we add information: |
| 87 | + # - a generic "failed" message, we direct them to the right issue board |
| 88 | + # - when the user successfully authenticates with the server, |
| 89 | + # but user is not allowed to take the requested action, |
| 90 | + # in which case we specify a OpenMLNotAuthorizedError. |
| 91 | + by_other_user = [323, 353, 393, 453, 594] |
| 92 | + has_dependent_entities = [324, 326, 327, 328, 354, 454, 464, 595] |
| 93 | + unknown_reason = [325, 355, 394, 455, 593] |
| 94 | + if exception.code in by_other_user: |
| 95 | + raise OpenMLNotAuthorizedError( |
| 96 | + message=( |
| 97 | + f"The {resource_type} can not be deleted because it was not uploaded by you." |
| 98 | + ), |
| 99 | + ) from exception |
| 100 | + if exception.code in has_dependent_entities: |
| 101 | + raise OpenMLNotAuthorizedError( |
| 102 | + message=( |
| 103 | + f"The {resource_type} can not be deleted because " |
| 104 | + f"it still has associated entities: {exception.message}" |
| 105 | + ), |
| 106 | + ) from exception |
| 107 | + if exception.code in unknown_reason: |
| 108 | + raise OpenMLServerError( |
| 109 | + message=( |
| 110 | + f"The {resource_type} can not be deleted for unknown reason," |
| 111 | + " please open an issue at: https://github.com/openml/openml/issues/new" |
| 112 | + ), |
| 113 | + ) from exception |
| 114 | + raise exception |
| 115 | + |
| 116 | + def _extract_id_from_upload(self, parsed: Mapping[str, Any]) -> int: |
| 117 | + # reads id from |
| 118 | + # sample parsed dict: {"oml:openml": {"oml:upload_flow": {"oml:id": "42"}}} |
| 119 | + |
| 120 | + # xmltodict always gives exactly one root key |
| 121 | + ((_, root_value),) = parsed.items() |
| 122 | + |
| 123 | + if not isinstance(root_value, Mapping): |
| 124 | + raise ValueError("Unexpected XML structure") |
| 125 | + |
| 126 | + # upload node (e.g. oml:upload_task, oml:study_upload, ...) |
| 127 | + ((_, upload_value),) = root_value.items() |
| 128 | + |
| 129 | + if not isinstance(upload_value, Mapping): |
| 130 | + raise ValueError("Unexpected upload node structure") |
| 131 | + |
| 132 | + # ID is the only leaf value |
| 133 | + for v in upload_value.values(): |
| 134 | + if isinstance(v, (str, int)): |
| 135 | + return int(v) |
| 136 | + |
| 137 | + raise ValueError("No ID found in upload response") |
74 | 138 |
|
75 | 139 |
|
76 | 140 | class ResourceV2(ResourceAPI): |
77 | 141 | api_version: APIVersion = APIVersion.V2 |
78 | 142 |
|
79 | | - def delete(self, resource_id: int) -> bool: |
| 143 | + def publish(self, path: str, files: Mapping[str, Any] | None) -> int: |
80 | 144 | raise NotImplementedError(self._get_not_implemented_message("publish")) |
81 | 145 |
|
82 | | - def publish(self) -> None: |
83 | | - raise NotImplementedError(self._get_not_implemented_message("publish")) |
| 146 | + def delete(self, resource_id: int) -> bool: |
| 147 | + raise NotImplementedError(self._get_not_implemented_message("delete")) |
| 148 | + |
| 149 | + def tag(self, resource_id: int, tag: str) -> list[str]: |
| 150 | + raise NotImplementedError(self._get_not_implemented_message("untag")) |
| 151 | + |
| 152 | + def untag(self, resource_id: int, tag: str) -> list[str]: |
| 153 | + raise NotImplementedError(self._get_not_implemented_message("untag")) |
0 commit comments