Skip to content

Commit 1a200ee

Browse files
committed
Implement endpoints for model output creation and query
Add API Spec v2.1.0
1 parent 62d8935 commit 1a200ee

File tree

11 files changed

+712
-24
lines changed

11 files changed

+712
-24
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ jobs:
3434
MEORG_EMAIL: ${{ secrets.MEORG_EMAIL }}
3535
MEORG_PASSWORD: ${{ secrets.MEORG_PASSWORD }}
3636
MEORG_MODEL_OUTPUT_ID: ${{ secrets.MEORG_MODEL_OUTPUT_ID }}
37+
MEORG_MODEL_OUTPUT_NAME: ${{ secrets.MEORG_MODEL_OUTPUT_NAME}}
38+
MEORG_MODEL_PROFILE_ID: ${{ secrets.MEORG_MODEL_PROFILE_ID }}
39+
MEORG_EXPERIMENT_ID: ${{ secrets.MEORG_EXPERIMENT_ID }}
3740
run: |
3841
conda install pytest
3942
pytest -v

docs/cli.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,39 @@ modelevaluation.org/modelOutput/display/**kafS53HgWu2CDXxgC**
9393

9494
This command will return an `$ANALYSIS_ID` upon success which is used in `analysis status`.
9595

96+
### model output create
97+
98+
To create a model output, execute the following command:
99+
100+
```shell
101+
meorg output create $MODEL_PROFILE_ID $EXPERIMENT_ID $MODEL_OUTPUT_NAME
102+
```
103+
104+
Where `$MODEL_PROFILE_ID` and `$EXPERIMENT_ID` are found on the model profile and corresponding experiment details pages on modelevaluation.org. `$MODEL_OUTPUT_NAME` is a unique name for the newly created model output.
105+
106+
This command will return the newly created `$MODEL_OUTPUT_ID` upon success which is used for further analysis. It will also print whether an existing model output record was overwritten.
107+
108+
### model output query
109+
110+
Retrieve Model output details via `$MODEL_OUTPUT_ID`
111+
112+
```shell
113+
meorg output query $MODEL_OUTPUT_ID
114+
```
115+
116+
This command will print the `id` and `name` of the modeloutput. If developer mode is enabled, print the JSON representation for the model output with metadata. An example model output data response would be:
117+
118+
```json
119+
{
120+
"id": "MnCj3tMzGx3NsuzwS",
121+
"name": "temp-output",
122+
"created": "2025-04-04T00:09:44.258Z",
123+
"modified": "2025-04-17T05:12:08.135Z",
124+
"stateSelection": "default model initialisation",
125+
"benchmarks": []
126+
}
127+
```
128+
96129
### analysis status
97130

98131
To query the status of an analysis, execute the following command:

meorg_client/cli.py

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
import click
44
from meorg_client.client import Client
55
import meorg_client.utilities as mcu
6+
import meorg_client.constants as mcc
67
from meorg_client import __version__
8+
import json
79
import os
810
import sys
911
import getpass
@@ -20,17 +22,16 @@ def _get_client() -> Client:
2022
Client object.
2123
"""
2224
# Get the dev-mode flag from the environment, better than passing the dev flag everywhere.
23-
dev_mode = os.getenv("MEORG_DEV_MODE", "0") == "1"
2425

2526
credentials = mcu.get_user_data_filepath("credentials.json")
2627
credentials_dev = mcu.get_user_data_filepath("credentials-dev.json")
2728

2829
# In dev mode and the configuration file exists
29-
if dev_mode and credentials_dev.is_file():
30+
if mcu.is_dev_mode() and credentials_dev.is_file():
3031
credentials = mcu.load_user_data("credentials-dev.json")
3132

3233
# In dev mode and it doesn't (i.e. Actions)
33-
elif dev_mode and not credentials_dev.is_file():
34+
elif mcu.is_dev_mode() and not credentials_dev.is_file():
3435
credentials = dict(
3536
email=os.getenv("MEORG_EMAIL"), password=os.getenv("MEORG_PASSWORD")
3637
)
@@ -41,7 +42,9 @@ def _get_client() -> Client:
4142

4243
# Get the client
4344
return Client(
44-
email=credentials["email"], password=credentials["password"], dev_mode=dev_mode
45+
email=credentials["email"],
46+
password=credentials["password"],
47+
dev_mode=mcu.is_dev_mode(),
4548
)
4649

4750

@@ -68,7 +71,7 @@ def _call(func: callable, **kwargs) -> dict:
6871
click.echo(ex.msg, err=True)
6972

7073
# Bubble up the exception
71-
if os.getenv("MEORG_DEV_MODE") == "1":
74+
if mcu.is_dev_mode():
7275
raise
7376

7477
sys.exit(1)
@@ -214,6 +217,72 @@ def analysis_start(id: str):
214217
click.echo(analysis_id)
215218

216219

220+
@click.command("create")
221+
@click.argument("mod_prof_id")
222+
@click.argument("exp_id")
223+
@click.argument("name")
224+
def create_new_model_output(mod_prof_id: str, exp_id: str, name: str):
225+
"""
226+
Create a new model output profile.
227+
228+
229+
Parameters
230+
----------
231+
mod_prof_id : str
232+
Model profile ID.
233+
234+
exp_id : str
235+
Experiment ID.
236+
237+
name : str
238+
New model output name
239+
240+
Prints modeloutput ID of created object, and whether it already existed or not.
241+
"""
242+
client = _get_client()
243+
244+
response = _call(
245+
client.model_output_create, mod_prof_id=mod_prof_id, exp_id=exp_id, name=name
246+
)
247+
248+
if client.success():
249+
model_output_id = response.get("data").get("modeloutput")
250+
existing = response.get("data").get("existing")
251+
click.echo(f"Model Output ID: {model_output_id}")
252+
if existing is not None:
253+
click.echo("Warning: Overwriting existing model output ID")
254+
return model_output_id
255+
256+
257+
@click.command("query")
258+
@click.argument("model_id")
259+
def model_output_query(model_id: str):
260+
"""
261+
Get details for a specific new model output entity
262+
263+
Parameters
264+
----------
265+
model_id : str
266+
Model Output ID.
267+
268+
Prints the `id` and `name` of the modeloutput, and JSON representation for the remaining metadata.
269+
"""
270+
client = _get_client()
271+
272+
response = _call(client.model_output_query, model_id=model_id)
273+
274+
if client.success():
275+
276+
model_output_data = response.get("data").get("modeloutput")
277+
model_output_id = model_output_data.get("id")
278+
name = model_output_data.get("name")
279+
if mcu.is_dev_mode():
280+
click.echo(f"Model Output: {json.dumps(model_output_data, indent=4)}")
281+
else:
282+
click.echo(f"Model Output ID: {model_output_id}")
283+
click.echo(f"Model Output Name: {name}")
284+
285+
217286
@click.command("status")
218287
@click.argument("id")
219288
def analysis_status(id: str):
@@ -291,6 +360,11 @@ def cli_analysis():
291360
pass
292361

293362

363+
@click.group("output", help="Model output commands.")
364+
def cli_model_output():
365+
pass
366+
367+
294368
# Add file commands
295369
cli_file.add_command(file_list)
296370
cli_file.add_command(file_upload)
@@ -304,11 +378,16 @@ def cli_analysis():
304378
cli_analysis.add_command(analysis_start)
305379
cli_analysis.add_command(analysis_status)
306380

381+
# Add output command
382+
cli_model_output.add_command(create_new_model_output)
383+
cli_model_output.add_command(model_output_query)
384+
307385
# Add subparsers to the master
308386
cli.add_command(cli_endpoints)
309387
cli.add_command(cli_file)
310388
cli.add_command(cli_analysis)
311389
cli.add_command(initialise)
390+
cli.add_command(cli_model_output)
312391

313392

314393
if __name__ == "__main__":

meorg_client/client.py

Lines changed: 69 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import hashlib as hl
55
import os
66
from typing import Union
7-
from urllib.parse import urljoin
7+
from urllib.parse import urljoin, urlencode
88
from meorg_client.exceptions import RequestException
99
import meorg_client.constants as mcc
1010
import meorg_client.endpoints as endpoints
@@ -38,9 +38,7 @@ def __init__(self, email: str = None, password: str = None, dev_mode: bool = Fal
3838
mt.init()
3939

4040
# Dev mode can be set by the user or from the environment
41-
dev_mode = dev_mode or os.getenv("MEORG_DEV_MODE", "0") == "1"
42-
43-
if dev_mode:
41+
if dev_mode or mu.is_dev_mode():
4442
self.base_url = os.getenv("MEORG_BASE_URL_DEV", None)
4543
else:
4644
self.base_url = mcc.MEORG_BASE_URL_PROD
@@ -56,6 +54,7 @@ def _make_request(
5654
self,
5755
method: str,
5856
endpoint: str,
57+
url_path_fields: dict = {},
5958
url_params: dict = {},
6059
data: dict = {},
6160
json: dict = {},
@@ -72,8 +71,10 @@ def _make_request(
7271
HTTP method.
7372
endpoint : str
7473
URL template for the API endpoint.
74+
url_path_fields : dict, optional
75+
Fields to interpolate into the URL template, by default {}
7576
url_params : dict, optional
76-
Parameters to interpolate into the URL template, by default {}
77+
Parameters to add at end of URL, by default {}
7778
data : dict, optional
7879
Data to send along with the request, by default {}
7980
json : dict, optional
@@ -106,7 +107,7 @@ def _make_request(
106107

107108
# Get the function and URL
108109
func = getattr(requests, method.lower())
109-
url = self._get_url(endpoint, **url_params)
110+
url = self._get_url(endpoint, url_params, **url_path_fields)
110111

111112
# Assemble the headers
112113
_headers = self._merge_headers(headers)
@@ -129,22 +130,29 @@ def _make_request(
129130
# For flexibility
130131
return self.last_response
131132

132-
def _get_url(self, endpoint: str, **kwargs):
133+
def _get_url(self, endpoint: str, url_params: dict = {}, **url_path_fields: dict):
133134
"""Get the well-formed URL for the call.
134135
135136
Parameters
136137
----------
137138
endpoint : str
138139
Endpoint to be appended to the base URL.
139-
**kwargs :
140-
Key/value pairs to interpolate into the URL template.
140+
url_path_fields : dict, optional
141+
Fields to interpolate into the URL template
142+
url_params : dict, optional
143+
Parameters to add at end of URL, by default {}
141144
142145
Returns
143146
-------
144147
str
145148
URL.
146149
"""
147-
return urljoin(self.base_url + "/", endpoint).format(**kwargs)
150+
# Add endpoint to base URL, interpolating url_path_fields
151+
url_path = urljoin(self.base_url + "/", endpoint).format(**url_path_fields)
152+
# Add URL parameters (if any)
153+
if url_params:
154+
url_path = f"{url_path}?{urlencode(url_params)}"
155+
return url_path
148156

149157
def _merge_headers(self, headers: dict = dict()):
150158
"""Merge additional headers into the client headers (i.e. Auth)
@@ -348,7 +356,7 @@ def _upload_file(
348356
method=mcc.HTTP_POST,
349357
endpoint=endpoints.FILE_UPLOAD,
350358
files=payload,
351-
url_params=dict(id=id),
359+
url_path_fields=dict(id=id),
352360
return_json=True,
353361
)
354362

@@ -372,7 +380,9 @@ def list_files(self, id: str) -> Union[dict, requests.Response]:
372380
Response from ME.org.
373381
"""
374382
return self._make_request(
375-
method=mcc.HTTP_GET, endpoint=endpoints.FILE_LIST, url_params=dict(id=id)
383+
method=mcc.HTTP_GET,
384+
endpoint=endpoints.FILE_LIST,
385+
url_path_fields=dict(id=id),
376386
)
377387

378388
def delete_file_from_model_output(self, id: str, file_id: str):
@@ -393,7 +403,7 @@ def delete_file_from_model_output(self, id: str, file_id: str):
393403
return self._make_request(
394404
method=mcc.HTTP_DELETE,
395405
endpoint=endpoints.FILE_DELETE,
396-
url_params=dict(id=id, fileId=file_id),
406+
url_path_fields=dict(id=id, fileId=file_id),
397407
)
398408

399409
def delete_all_files_from_model_output(self, id: str):
@@ -439,7 +449,51 @@ def start_analysis(self, id: str) -> Union[dict, requests.Response]:
439449
return self._make_request(
440450
method=mcc.HTTP_PUT,
441451
endpoint=endpoints.ANALYSIS_START,
442-
url_params=dict(id=id),
452+
url_path_fields=dict(id=id),
453+
)
454+
455+
def model_output_create(
456+
self, mod_prof_id: str, exp_id: str, name: str
457+
) -> Union[dict, requests.Response]:
458+
"""
459+
Create a new model output entity
460+
Parameters
461+
----------
462+
mod_prof_id : str
463+
Model Profile ID
464+
exp_id : str
465+
Experiment ID
466+
name : str
467+
Name of Model Output
468+
469+
Returns
470+
-------
471+
Union[dict, requests.Response]
472+
Response from ME.org.
473+
"""
474+
return self._make_request(
475+
method=mcc.HTTP_POST,
476+
endpoint=endpoints.MODEL_OUTPUT_CREATE,
477+
data=dict(experiment=exp_id, model=mod_prof_id, name=name),
478+
)
479+
480+
def model_output_query(self, model_id: str) -> Union[dict, requests.Response]:
481+
"""
482+
Get details for a specific new model output entity
483+
Parameters
484+
----------
485+
model_id : str
486+
Model Output ID
487+
488+
Returns
489+
-------
490+
Union[dict, requests.Response]
491+
Response from ME.org.
492+
"""
493+
return self._make_request(
494+
method=mcc.HTTP_GET,
495+
endpoint=endpoints.MODEL_OUTPUT_QUERY,
496+
url_params=dict(id=model_id),
443497
)
444498

445499
def get_analysis_status(self, id: str) -> Union[dict, requests.Response]:
@@ -458,7 +512,7 @@ def get_analysis_status(self, id: str) -> Union[dict, requests.Response]:
458512
return self._make_request(
459513
method=mcc.HTTP_GET,
460514
endpoint=endpoints.ANALYSIS_STATUS,
461-
url_params=dict(id=id),
515+
url_path_fields=dict(id=id),
462516
)
463517

464518
def list_endpoints(self) -> Union[dict, requests.Response]:

meorg_client/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import os
2+
13
"""Constants."""
24

35
# Valid HTTP methods

0 commit comments

Comments
 (0)