Skip to content

Commit 7874938

Browse files
committed
New helpers for app handling and start adding flow manipulation helpers
1 parent 0500a9f commit 7874938

File tree

5 files changed

+185
-8
lines changed

5 files changed

+185
-8
lines changed

dataikuapi/dss/app.py

Lines changed: 97 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
import os.path as osp
33
from .future import DSSFuture
44
from dataikuapi.utils import DataikuException
5+
import random, string
56

7+
def random_string(length):
8+
return ''.join(random.choice(string.ascii_letters) for _ in range(length))
69

710
class DSSApp(object):
811
"""
@@ -13,27 +16,115 @@ def __init__(self, client, app_id):
1316
self.client = client
1417
self.app_id = app_id
1518

16-
1719
########################################################
1820
# Instances
1921
########################################################
2022

23+
def create_instance(self, instance_key, instance_name, wait=True):
24+
"""
25+
Creates a new instance of this app. Each instance. must have a globally unique
26+
instance key, separate from any project key across the whole DSS instance
27+
28+
:return:
29+
"""
30+
future_resp = self.client._perform_json(
31+
"POST", "/apps/%s/instances" % self.app_id, body={
32+
"targetProjectKey" : instance_key,
33+
"targetProjectName" : instance_name
34+
})
35+
future = DSSFuture(self.client, future_resp.get("jobId", None), future_resp)
36+
if wait:
37+
result = future.wait_for_result()
38+
return DSSAppInstance(self.client, instance_key)
39+
else:
40+
return future
41+
42+
def create_temporary_instance(self):
43+
"""
44+
Creates a new temporary instance of this app.
45+
The return value should be used as a Python context manager. Upon exit, the temporary app
46+
instance is deleted
47+
:return a :class:`TemporaryDSSAppInstance`
48+
"""
49+
key = "%s_tmp_%s" % (self.app_id, random_string(10))
50+
instance = self.create_instance(key, key, True)
51+
return TemporaryDSSAppInstance(self.client, key)
52+
53+
def list_instance_keys(self):
54+
"""
55+
List the existing instances of this app
56+
57+
:return a list of instance keys, each as a string
58+
"""
59+
return [x["projectKey"] for x in self.list_instances()]
60+
2161
def list_instances(self):
2262
"""
23-
List the instances in this project
63+
List the existing instances of this app
2464
25-
Returns:
26-
the list of the instances, each one as a JSON object
65+
:rtype: list of dicts
66+
:return a list of instances, each as a dict containing at least a "projectKey" field
2767
"""
2868
return self.client._perform_json(
2969
"GET", "/apps/%s/instances/" % self.app_id)
3070

71+
def get_instance(self, instance_key):
72+
return DSSAppInstance(self.client, instance_key)
73+
74+
75+
def get_manifest(self):
76+
raw_data = self.client._perform_json("GET", "/apps/%s/" % self.app_id)
77+
return DSSAppManifest(self.client, raw_data)
78+
79+
80+
class DSSAppManifest(object):
3181

32-
class DSSDataikuAppInstance(object):
82+
def __init__(self, client, raw_data):
83+
"""The manifest for an app. Do not create this class directly"""
84+
self.client = client
85+
self.raw_data = raw_data
86+
87+
def get_raw(self):
88+
return self.raw_data
89+
90+
def get_all_actions(self):
91+
return [x for section in self.raw_data["homepageSections"] for x in section["tiles"]]
92+
93+
def get_runnable_scenarios(self):
94+
"""Return the scenario identifiers that are declared as actions for this app"""
95+
return [x["scenarioId"] for x in self.get_all_actions() if x["type"] == "SCENARIO_RUN"]
96+
97+
class DSSAppInstance(object):
3398

3499
def __init__(self, client, project_key):
35100
self.client = client
36101
self.project_key = project_key
37102

38103
def get_as_project(self):
39-
self
104+
"""
105+
Get the :class:`dataikuapi.dss.project DSSProject` corresponding to this app instance
106+
"""
107+
return self.client.get_project(self.project_key)
108+
109+
def get_manifest(self):
110+
"""
111+
Get the app manifest for this instance, as a :class:`DSSAppManifest`
112+
"""
113+
raw_data = self.client._perform_json("GET", "/projects/%s/app-manifest" % self.project_key)
114+
return DSSAppManifest(self.client, raw_data)
115+
116+
class TemporaryDSSAppInstance(DSSAppInstance):
117+
"""internal class"""
118+
119+
def __init__(self, client, project_key):
120+
DSSAppInstance.__init__(self, client,project_key)
121+
122+
123+
def close(self):
124+
self.get_as_project().delete()
125+
126+
def __enter__(self,):
127+
return self
128+
129+
def __exit__(self, type, value, traceback):
130+
self.close()

dataikuapi/dss/flow.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from .utils import AnyLoc
2+
from .recipe import DSSRecipeDefinitionAndPayload
3+
import logging
4+
class DSSProjectFlow(object):
5+
def __init__(self, client, project):
6+
self.client = client
7+
self.project = project
8+
9+
10+
def replace_input_computable(self, current_ref, new_ref, type="DATASET"):
11+
"""
12+
This method replaces all references to a "computable" (Dataset, Managed Folder or Saved Model)
13+
as input of recipes in the whole Flow by a reference to another computable.
14+
15+
No specific checks are performed. It is your responsibility to ensure that the schema
16+
of the new dataset will be compatible with the previous one (in the case of datasets).
17+
18+
If `new_ref` references an object in a foreign project, this method will automatically
19+
ensure that `new_ref` is exposed to the current project
20+
21+
:param current_ref str: Either a "simple" object name (dataset name, model id, managed folder id)
22+
or a foreign object reference in the form "FOREIGN_PROJECT_KEY.local_id")
23+
:param new_ref str: Either a "simple" object name (dataset name, model id, managed folder id)
24+
or a foreign object reference in the form "FOREIGN_PROJECT_KEY.local_id")
25+
:param type str: The type of object being replaced (DATASET, SAVED_MODEL or MANAGED_FOLDER)
26+
"""
27+
28+
new_loc = AnyLoc.from_ref(self.project.project_key, new_ref)
29+
30+
if new_loc.project_key != self.project.project_key:
31+
logging.debug("New ref is in project %s, exposing it to project %s" % (new_loc.project_key, self.project.project_key))
32+
new_ref_src_project = self.client.get_project(new_loc.project_key)
33+
settings = new_ref_src_project.get_settings()
34+
settings.add_exposed_object(type, new_loc.object_id, self.project.project_key)
35+
settings.save()
36+
37+
for recipe in self.project.list_recipes():
38+
fake_rap = DSSRecipeDefinitionAndPayload({"recipe" : recipe})
39+
if fake_rap.has_input(current_ref):
40+
logging.debug("Recipe %s has %s as input, performing the replacement by %s"% \
41+
(recipe["name"], current_ref, new_ref))
42+
recipe_obj = self.project.get_recipe(recipe["name"])
43+
dap = recipe_obj.get_definition_and_payload()
44+
dap.replace_input(current_ref, new_ref)
45+
recipe_obj.set_definition_and_payload(dap)

dataikuapi/dss/project.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from .discussion import DSSObjectDiscussions
1616
from .ml import DSSMLTask
1717
from .analysis import DSSAnalysis
18+
from .flow import DSSProjectFlow
1819
from dataikuapi.utils import DataikuException
1920

2021

@@ -755,6 +756,13 @@ def create_recipe(self, recipe_proto, creation_settings):
755756
body = definition)['name']
756757
return DSSRecipe(self.client, self.project_key, recipe_name)
757758

759+
########################################################
760+
# Flow
761+
########################################################
762+
763+
def get_flow(self):
764+
return DSSProjectFlow(self.client, self)
765+
758766
########################################################
759767
# Security
760768
########################################################
@@ -1021,7 +1029,6 @@ def add_exposed_object(self, object_type, object_id, target_project):
10211029
Exposes an object from this project to another project.
10221030
Does nothing if the object was already exposed to the target project
10231031
"""
1024-
10251032
found_eo = None
10261033
for eo in self.settings["exposedObjects"]["objects"]:
10271034
if eo["type"] == object_type and eo["localName"] == object_id:

dataikuapi/dss/recipe.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,40 @@ def set_json_payload(self, payload):
211211
"""
212212
self.data['payload'] = json.dumps(payload)
213213

214+
def has_input(self, input_ref):
215+
"""Returns whether this recipe has a given ref as input"""
216+
inputs = self.get_recipe_inputs()
217+
for (input_role_name, input_role) in inputs.items():
218+
for item in input_role.get("items", []):
219+
if item.get("ref", None) == input_ref:
220+
return True
221+
return False
222+
223+
def has_output(self, output_ref):
224+
"""Returns whether this recipe has a given ref as output"""
225+
outputs = self.get_recipe_outputs()
226+
for (output_role_name, output_role) in outputs.items():
227+
for item in output_role.get("items", []):
228+
if item.get("ref", None) == output_ref:
229+
return True
230+
return False
231+
232+
def replace_input(self, current_input_ref, new_input_ref):
233+
"""Replaces an object reference as input of this recipe by another"""
234+
inputs = self.get_recipe_inputs()
235+
for (input_role_name, input_role) in inputs.items():
236+
for item in input_role.get("items", []):
237+
if item.get("ref", None) == current_input_ref:
238+
item["ref"] = new_input_ref
239+
240+
def replace_output(self, current_output_ref, new_output_ref):
241+
"""Replaces an object reference as output of this recipe by another"""
242+
outputs = self.get_recipe_outputs()
243+
for (output_role_name, output_role) in outputs.items():
244+
for item in output_role.get("items", []):
245+
if item.get("ref", None) == current_output_ref:
246+
item["ref"] = new_output_ref
247+
214248
class DSSRecipeCreator(object):
215249
"""
216250
Helper to create new recipes

dataikuapi/dssclient.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ def list_apps(self):
191191
"""
192192
List the apps
193193
194-
:returns: a list of apps, each as a dict. Each dictcontains at least a 'appId' field
194+
:returns: a list of apps, each as a dict. Each dict contains at least a 'appId' field
195195
:rtype: list of dicts
196196
"""
197197
return self._perform_json("GET", "/apps/")

0 commit comments

Comments
 (0)