Skip to content

Commit 5606a54

Browse files
committed
Closing OPEN-4186 API for validating goal statuses
1 parent ac08e49 commit 5606a54

File tree

5 files changed

+194
-9
lines changed

5 files changed

+194
-9
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1919
* Added `protobuf<3.20` to requirements to fix compatibility issue with Tensorflow.
2020
* Warnings if the dependencies from the `requirement_txt_file` and current environment are inconsistent.
2121
* Paths to custom SSL certificates can now be modified by altering `openlayer.api.VERIFY_REQUESTS`. The value can either be True (default), False, or a path to a certificate.
22+
* Ability to check for goal statuses through the API.
2223

2324
### Changed
2425

openlayer/__init__.py

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import yaml
3434

3535
from . import api, exceptions, utils
36+
from .project_versions import ProjectVersion
3637
from .projects import Project
3738
from .schemas import BaselineModelSchema, DatasetSchema, ModelSchema
3839
from .tasks import TaskType
@@ -473,7 +474,7 @@ def add_model(
473474

474475
def add_baseline_model(
475476
self,
476-
project_id: int,
477+
project_id: str,
477478
task_type: TaskType,
478479
model_config_file_path: Optional[str] = None,
479480
force: bool = False,
@@ -994,7 +995,7 @@ class probabilities. For example, for a binary classification
994995
task_type=task_type,
995996
)
996997

997-
def commit(self, message: str, project_id: int, force: bool = False):
998+
def commit(self, message: str, project_id: str, force: bool = False):
998999
"""Adds a commit message to staged resources.
9991000
10001001
Parameters
@@ -1081,7 +1082,7 @@ def commit(self, message: str, project_id: int, force: bool = False):
10811082

10821083
print("Committed!")
10831084

1084-
def push(self, project_id: int, task_type: TaskType):
1085+
def push(self, project_id: str, task_type: TaskType) -> Optional[ProjectVersion]:
10851086
"""Pushes the commited resources to the platform.
10861087
10871088
Notes
@@ -1124,15 +1125,17 @@ def push(self, project_id: int, task_type: TaskType):
11241125
f"\t - Date: {commit['date']}"
11251126
)
11261127
payload = {"commit": {"message": commit["message"]}}
1127-
self.api.upload(
1128+
response_body = self.api.upload(
11281129
endpoint=f"projects/{project_id}/versions",
11291130
file_path=tar_file_path,
11301131
object_name="tarfile",
11311132
body=payload,
11321133
)
1134+
project_version = ProjectVersion(json=response_body, client=self)
11331135

11341136
self._post_push_cleanup(project_dir=project_dir)
11351137
print("Pushed!")
1138+
return project_version
11361139

11371140
def _ready_for_push(self, project_dir: str, task_type: TaskType) -> bool:
11381141
"""Checks if the project's staging area is ready to be pushed to the platform.
@@ -1183,7 +1186,7 @@ def _post_push_cleanup(self, project_dir: str) -> None:
11831186
shutil.rmtree(project_dir)
11841187
os.makedirs(project_dir, exist_ok=True)
11851188

1186-
def export(self, destination_dir: str, project_id: int, task_type: TaskType):
1189+
def export(self, destination_dir: str, project_id: str, task_type: TaskType):
11871190
"""Exports the commited resources as a tarfile to the location specified
11881191
by ``destination_dir``.
11891192
@@ -1229,7 +1232,7 @@ def export(self, destination_dir: str, project_id: int, task_type: TaskType):
12291232
self._post_push_cleanup(project_dir=project_dir)
12301233
print("Exported tarfile!")
12311234

1232-
def status(self, project_id: int):
1235+
def status(self, project_id: str):
12331236
"""Shows the state of the staging area.
12341237
12351238
Examples
@@ -1277,7 +1280,7 @@ def status(self, project_id: int):
12771280
print(f"\t {commit['message']}")
12781281
print("Use the `push` method to push your changes to the platform.")
12791282

1280-
def restore(self, *resource_names: str, project_id: int):
1283+
def restore(self, *resource_names: str, project_id: str):
12811284
"""Removes the resource specified by ``resource_name`` from the staging area.
12821285
12831286
Parameters
@@ -1328,7 +1331,7 @@ def restore(self, *resource_names: str, project_id: int):
13281331
os.remove(f"{project_dir}/commit.yaml")
13291332

13301333
def _stage_resource(
1331-
self, resource_name: str, resource_dir: str, project_id: int, force: bool
1334+
self, resource_name: str, resource_dir: str, project_id: str, force: bool
13321335
):
13331336
"""Adds the resource specified by `resource_name` to the project's staging directory.
13341337
@@ -1370,3 +1373,43 @@ def _stage_resource(
13701373
shutil.copytree(resource_dir, project_dir + "/" + resource_name)
13711374

13721375
print(f"Staged the `{resource_name}` resource!")
1376+
1377+
def load_project_version(self, version_id: str) -> Project:
1378+
"""Loads an existing project version from the Openlayer platform. Can be used
1379+
to check the status of the project version and the number of passing, failing
1380+
and skipped goals.
1381+
1382+
Parameters
1383+
----------
1384+
id : str
1385+
UUID of the project to be loaded. You can find the UUID of a project by
1386+
navigating to the project's page on the Openlayer platform.
1387+
1388+
.. note::
1389+
When you run :obj:`push`, it will return the project version object,
1390+
which you can use to check your goal statuses.
1391+
1392+
Returns
1393+
-------
1394+
ProjectVersion
1395+
An object that is used to check for upload progress and goal statuses.
1396+
Also contains other useful information about a project version.
1397+
1398+
Examples
1399+
--------
1400+
Instantiate the client and load the project version:
1401+
1402+
>>> import openlayer
1403+
>>> client = openlayer.OpenlayerClient('YOUR_API_KEY_HERE')
1404+
>>>
1405+
>>> version = client.load_project_version(id='YOUR_PROJECT_ID_HERE')
1406+
>>> version.wait_for_completion()
1407+
>>> version.print_goal_report()
1408+
1409+
With the ProjectVersion object loaded, you are able to check progress and
1410+
goal statuses.
1411+
"""
1412+
endpoint = f"versions/{version_id}"
1413+
version_data = self.api.get_request(endpoint)
1414+
version = ProjectVersion(version_data, self)
1415+
return version

openlayer/project_versions.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
"""Module for the ProjectVersion class."""
2+
3+
import enum
4+
import time
5+
from typing import Optional
6+
7+
import tabulate
8+
9+
10+
class TaskStatus(enum.Enum):
11+
"""An enum containing the possible states of a project version."""
12+
13+
RUNNING = "running"
14+
COMPLETED = "completed"
15+
FAILED = "failed"
16+
QUEUED = "queued"
17+
PAUSED = "paused"
18+
UNKNOWN = "unknown"
19+
20+
21+
class ProjectVersion:
22+
"""An object containing information about a project version on the
23+
Openlayer platform.
24+
"""
25+
26+
def __init__(self, json, client):
27+
self._json = json
28+
self.id = json["id"]
29+
self.client = client
30+
31+
def __getattr__(self, name):
32+
if name in self._json:
33+
return self._json[name]
34+
raise AttributeError(f"'{type(self).__name__}' object has no attribute {name}")
35+
36+
def __hash__(self):
37+
return hash(self.id)
38+
39+
def __str__(self):
40+
return f"ProjectVersion(id={self.id})"
41+
42+
def __repr__(self):
43+
return f"ProjectVersion({self._json})"
44+
45+
def to_dict(self):
46+
"""Returns object properties as a dict.
47+
48+
Returns
49+
-------
50+
Dict with object properties.
51+
"""
52+
return self._json
53+
54+
@property
55+
def status(self) -> TaskStatus:
56+
"""Returns the current state of the project version."""
57+
return TaskStatus(self._json["status"])
58+
59+
@property
60+
def status_message(self) -> str:
61+
"""Returns the status message of the project version."""
62+
return self._json["statusMessage"]
63+
64+
@property
65+
def passing_goal_count(self) -> int:
66+
"""Returns the number of passing goals for the project version."""
67+
return self._json["passingGoalCount"]
68+
69+
@property
70+
def failing_goal_count(self) -> int:
71+
"""Returns the number of failing goals for the project version."""
72+
return self._json["failingGoalCount"]
73+
74+
@property
75+
def skipped_goal_count(self) -> int:
76+
"""Returns the number of failing goals for the project version."""
77+
return (
78+
self._json["totalGoalCount"]
79+
- self._json["passingGoalCount"]
80+
- self._json["failingGoalCount"]
81+
)
82+
83+
@property
84+
def total_goal_count(self) -> int:
85+
"""Returns the number of failing goals for the project version."""
86+
return self._json["totalGoalCount"]
87+
88+
def wait_for_completion(self, timeout: Optional[int] = None):
89+
"""Waits for the project version to complete.
90+
91+
Parameters
92+
----------
93+
timeout : int, optional
94+
Number of seconds to wait before timing out. If None, waits
95+
indefinitely.
96+
97+
Returns
98+
-------
99+
ProjectVersion
100+
The project version object.
101+
"""
102+
while self.status not in [TaskStatus.COMPLETED, TaskStatus.FAILED]:
103+
self.refresh()
104+
time.sleep(1)
105+
if timeout:
106+
timeout -= 1
107+
if timeout <= 0:
108+
print(
109+
"Timeout exceeded. Visit the Openlayer dashboard to"
110+
" check the status of the project version."
111+
)
112+
break
113+
if self.status == TaskStatus.FAILED:
114+
print("Project version failed with message:", self.status_message)
115+
elif self.status == TaskStatus.COMPLETED:
116+
print("Project version processed successfully.")
117+
118+
def refresh(self):
119+
"""Refreshes the project version object with the latest
120+
information from the server."""
121+
self._json = self.client.load_project_version(self.id).to_dict()
122+
123+
def print_goal_report(self):
124+
"""Prints the goal results of the project version."""
125+
if self.status != TaskStatus.COMPLETED:
126+
print("Project version is not complete. Nothing to print.")
127+
return
128+
print(
129+
tabulate.tabulate(
130+
[
131+
["Passed", self.passing_goal_count],
132+
["Failed", self.failing_goal_count],
133+
["Skipped", self.skipped_goal_count],
134+
["Total", self.total_goal_count],
135+
],
136+
headers=["Goals", "Count"],
137+
tablefmt="fancy_grid",
138+
),
139+
f"\nVisit {self.links['app']} to view detailed results.",
140+
)

openlayer/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,4 @@
2222
data=data,
2323
)
2424
"""
25-
__version__ = "0.1.0a1"
25+
__version__ = "0.1.0a2"

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ install_requires =
4242
pandas
4343
requests
4444
tqdm
45+
tabulate
4546
marshmallow
4647
marshmallow_oneofschema
4748
requests_toolbelt

0 commit comments

Comments
 (0)