Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ jobs:
MEORG_EMAIL: ${{ secrets.MEORG_EMAIL }}
MEORG_PASSWORD: ${{ secrets.MEORG_PASSWORD }}
MEORG_MODEL_OUTPUT_ID: ${{ secrets.MEORG_MODEL_OUTPUT_ID }}
MEORG_MODEL_OUTPUT_NAME: ${{ secrets.MEORG_MODEL_OUTPUT_NAME}}
MEORG_MODEL_PROFILE_ID: ${{ secrets.MEORG_MODEL_PROFILE_ID }}
MEORG_EXPERIMENT_ID: ${{ secrets.MEORG_EXPERIMENT_ID }}
run: |
conda install pytest
pytest -v
33 changes: 33 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,39 @@ modelevaluation.org/modelOutput/display/**kafS53HgWu2CDXxgC**

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

### model output create

To create a model output, execute the following command:

```shell
meorg output create $MODEL_PROFILE_ID $EXPERIMENT_ID $MODEL_OUTPUT_NAME
```

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.

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.

### model output query

Retrieve Model output details via `$MODEL_OUTPUT_ID`

```shell
meorg output query $MODEL_OUTPUT_ID
```

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:

```json
{
"id": "MnCj3tMzGx3NsuzwS",
"name": "temp-output",
"created": "2025-04-04T00:09:44.258Z",
"modified": "2025-04-17T05:12:08.135Z",
"stateSelection": "default model initialisation",
"benchmarks": []
}
```

### analysis status

To query the status of an analysis, execute the following command:
Expand Down
89 changes: 84 additions & 5 deletions meorg_client/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import click
from meorg_client.client import Client
import meorg_client.utilities as mcu
import meorg_client.constants as mcc
from meorg_client import __version__
import json
import os
import sys
import getpass
Expand All @@ -20,17 +22,16 @@ def _get_client() -> Client:
Client object.
"""
# Get the dev-mode flag from the environment, better than passing the dev flag everywhere.
dev_mode = os.getenv("MEORG_DEV_MODE", "0") == "1"

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

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

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

# Get the client
return Client(
email=credentials["email"], password=credentials["password"], dev_mode=dev_mode
email=credentials["email"],
password=credentials["password"],
dev_mode=mcu.is_dev_mode(),
)


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

# Bubble up the exception
if os.getenv("MEORG_DEV_MODE") == "1":
if mcu.is_dev_mode():
raise

sys.exit(1)
Expand Down Expand Up @@ -214,6 +217,72 @@ def analysis_start(id: str):
click.echo(analysis_id)


@click.command("create")
@click.argument("mod_prof_id")
@click.argument("exp_id")
@click.argument("name")
def create_new_model_output(mod_prof_id: str, exp_id: str, name: str):
"""
Create a new model output profile.


Parameters
----------
mod_prof_id : str
Model profile ID.

exp_id : str
Experiment ID.

name : str
New model output name

Prints modeloutput ID of created object, and whether it already existed or not.
"""
client = _get_client()

response = _call(
client.model_output_create, mod_prof_id=mod_prof_id, exp_id=exp_id, name=name
)

if client.success():
model_output_id = response.get("data").get("modeloutput")
existing = response.get("data").get("existing")
click.echo(f"Model Output ID: {model_output_id}")
if existing is not None:
click.echo("Warning: Overwriting existing model output ID")
return model_output_id


@click.command("query")
@click.argument("model_id")
def model_output_query(model_id: str):
"""
Get details for a specific new model output entity

Parameters
----------
model_id : str
Model Output ID.

Prints the `id` and `name` of the modeloutput, and JSON representation for the remaining metadata.
"""
client = _get_client()

response = _call(client.model_output_query, model_id=model_id)

if client.success():

model_output_data = response.get("data").get("modeloutput")
model_output_id = model_output_data.get("id")
name = model_output_data.get("name")
if mcu.is_dev_mode():
click.echo(f"Model Output: {json.dumps(model_output_data, indent=4)}")
else:
click.echo(f"Model Output ID: {model_output_id}")
click.echo(f"Model Output Name: {name}")


@click.command("status")
@click.argument("id")
def analysis_status(id: str):
Expand Down Expand Up @@ -291,6 +360,11 @@ def cli_analysis():
pass


@click.group("output", help="Model output commands.")
def cli_model_output():
pass


# Add file commands
cli_file.add_command(file_list)
cli_file.add_command(file_upload)
Expand All @@ -304,11 +378,16 @@ def cli_analysis():
cli_analysis.add_command(analysis_start)
cli_analysis.add_command(analysis_status)

# Add output command
cli_model_output.add_command(create_new_model_output)
cli_model_output.add_command(model_output_query)

# Add subparsers to the master
cli.add_command(cli_endpoints)
cli.add_command(cli_file)
cli.add_command(cli_analysis)
cli.add_command(initialise)
cli.add_command(cli_model_output)


if __name__ == "__main__":
Expand Down
84 changes: 69 additions & 15 deletions meorg_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import hashlib as hl
import os
from typing import Union
from urllib.parse import urljoin
from urllib.parse import urljoin, urlencode
from meorg_client.exceptions import RequestException
import meorg_client.constants as mcc
import meorg_client.endpoints as endpoints
Expand Down Expand Up @@ -38,9 +38,7 @@ def __init__(self, email: str = None, password: str = None, dev_mode: bool = Fal
mt.init()

# Dev mode can be set by the user or from the environment
dev_mode = dev_mode or os.getenv("MEORG_DEV_MODE", "0") == "1"

if dev_mode:
if dev_mode or mu.is_dev_mode():
self.base_url = os.getenv("MEORG_BASE_URL_DEV", None)
else:
self.base_url = mcc.MEORG_BASE_URL_PROD
Expand All @@ -56,6 +54,7 @@ def _make_request(
self,
method: str,
endpoint: str,
url_path_fields: dict = {},
url_params: dict = {},
data: dict = {},
json: dict = {},
Expand All @@ -72,8 +71,10 @@ def _make_request(
HTTP method.
endpoint : str
URL template for the API endpoint.
url_path_fields : dict, optional
Fields to interpolate into the URL template, by default {}
url_params : dict, optional
Parameters to interpolate into the URL template, by default {}
Parameters to add at end of URL, by default {}
data : dict, optional
Data to send along with the request, by default {}
json : dict, optional
Expand Down Expand Up @@ -106,7 +107,7 @@ def _make_request(

# Get the function and URL
func = getattr(requests, method.lower())
url = self._get_url(endpoint, **url_params)
url = self._get_url(endpoint, url_params, **url_path_fields)

# Assemble the headers
_headers = self._merge_headers(headers)
Expand All @@ -129,22 +130,29 @@ def _make_request(
# For flexibility
return self.last_response

def _get_url(self, endpoint: str, **kwargs):
def _get_url(self, endpoint: str, url_params: dict = {}, **url_path_fields: dict):
"""Get the well-formed URL for the call.

Parameters
----------
endpoint : str
Endpoint to be appended to the base URL.
**kwargs :
Key/value pairs to interpolate into the URL template.
url_path_fields : dict, optional
Fields to interpolate into the URL template
url_params : dict, optional
Parameters to add at end of URL, by default {}

Returns
-------
str
URL.
"""
return urljoin(self.base_url + "/", endpoint).format(**kwargs)
# Add endpoint to base URL, interpolating url_path_fields
url_path = urljoin(self.base_url + "/", endpoint).format(**url_path_fields)
# Add URL parameters (if any)
if url_params:
url_path = f"{url_path}?{urlencode(url_params)}"
return url_path

def _merge_headers(self, headers: dict = dict()):
"""Merge additional headers into the client headers (i.e. Auth)
Expand Down Expand Up @@ -348,7 +356,7 @@ def _upload_file(
method=mcc.HTTP_POST,
endpoint=endpoints.FILE_UPLOAD,
files=payload,
url_params=dict(id=id),
url_path_fields=dict(id=id),
return_json=True,
)

Expand All @@ -372,7 +380,9 @@ def list_files(self, id: str) -> Union[dict, requests.Response]:
Response from ME.org.
"""
return self._make_request(
method=mcc.HTTP_GET, endpoint=endpoints.FILE_LIST, url_params=dict(id=id)
method=mcc.HTTP_GET,
endpoint=endpoints.FILE_LIST,
url_path_fields=dict(id=id),
)

def delete_file_from_model_output(self, id: str, file_id: str):
Expand All @@ -393,7 +403,7 @@ def delete_file_from_model_output(self, id: str, file_id: str):
return self._make_request(
method=mcc.HTTP_DELETE,
endpoint=endpoints.FILE_DELETE,
url_params=dict(id=id, fileId=file_id),
url_path_fields=dict(id=id, fileId=file_id),
)

def delete_all_files_from_model_output(self, id: str):
Expand Down Expand Up @@ -439,7 +449,51 @@ def start_analysis(self, id: str) -> Union[dict, requests.Response]:
return self._make_request(
method=mcc.HTTP_PUT,
endpoint=endpoints.ANALYSIS_START,
url_params=dict(id=id),
url_path_fields=dict(id=id),
)

def model_output_create(
self, mod_prof_id: str, exp_id: str, name: str
) -> Union[dict, requests.Response]:
"""
Create a new model output entity
Parameters
----------
mod_prof_id : str
Model Profile ID
exp_id : str
Experiment ID
name : str
Name of Model Output

Returns
-------
Union[dict, requests.Response]
Response from ME.org.
"""
return self._make_request(
method=mcc.HTTP_POST,
endpoint=endpoints.MODEL_OUTPUT_CREATE,
data=dict(experiment=exp_id, model=mod_prof_id, name=name),
)

def model_output_query(self, model_id: str) -> Union[dict, requests.Response]:
"""
Get details for a specific new model output entity
Parameters
----------
model_id : str
Model Output ID

Returns
-------
Union[dict, requests.Response]
Response from ME.org.
"""
return self._make_request(
method=mcc.HTTP_GET,
endpoint=endpoints.MODEL_OUTPUT_QUERY,
url_params=dict(id=model_id),
)

def get_analysis_status(self, id: str) -> Union[dict, requests.Response]:
Expand All @@ -458,7 +512,7 @@ def get_analysis_status(self, id: str) -> Union[dict, requests.Response]:
return self._make_request(
method=mcc.HTTP_GET,
endpoint=endpoints.ANALYSIS_STATUS,
url_params=dict(id=id),
url_path_fields=dict(id=id),
)

def list_endpoints(self) -> Union[dict, requests.Response]:
Expand Down
2 changes: 2 additions & 0 deletions meorg_client/constants.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import os

"""Constants."""

# Valid HTTP methods
Expand Down
Loading