Skip to content

Commit 9628f5e

Browse files
author
Valentin Thorey
committed
Merge branch 'master' into feature/mlflow-experiment-tracking
2 parents 8da9c54 + 35846a2 commit 9628f5e

File tree

11 files changed

+359
-41
lines changed

11 files changed

+359
-41
lines changed

dataikuapi/dss/admin.py

Lines changed: 268 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -695,13 +695,18 @@ def set_definition(self, env):
695695
696696
* env.permissions, env.usableByAll, env.desc.owner
697697
* env.specCondaEnvironment, env.specPackageList, env.externalCondaEnvName, env.desc.installCorePackages,
698-
env.desc.installJupyterSupport, env.desc.yarnPythonBin
698+
env.desc.corePackagesSet, env.desc.installJupyterSupport, env.desc.yarnPythonBin, env.desc.yarnRBin
699+
env.desc.envSettings, env.desc.allContainerConfs, env.desc.containerConfs,
700+
env.desc.allSparkKubernetesConfs, env.desc.sparkKubernetesConfs
699701
700702
Fields that can be updated in automation node (where {version} is the updated version):
701703
702-
* env.permissions, env.usableByAll, env.owner
704+
* env.permissions, env.usableByAll, env.owner, env.envSettings
703705
* env.{version}.specCondaEnvironment, env.{version}.specPackageList, env.{version}.externalCondaEnvName,
704-
env.{version}.desc.installCorePackages, env.{version}.desc.installJupyterSupport, env.{version}.desc.yarnPythonBin
706+
env.{version}.desc.installCorePackages, env.{version}.corePackagesSet, env.{version}.desc.installJupyterSupport
707+
env.{version}.desc.yarnPythonBin, env.{version}.desc.yarnRBin, env.{version}.desc.allContainerConfs,
708+
env.{version}.desc.containerConfs, env.{version}.desc.allSparkKubernetesConfs,
709+
env.{version}.{version}.desc.sparkKubernetesConfs
705710
706711
Note: this call requires an API key with admin rights
707712
@@ -722,6 +727,39 @@ def get_version_for_project(self, project_key):
722727
return self.client._perform_json(
723728
"GET", "/admin/code-envs/%s/%s/%s/version" % (self.env_lang, self.env_name, project_key))
724729

730+
731+
def get_settings(self):
732+
"""
733+
Returns the settings of this code env as a :class:`DSSCodeEnvSettings`, or one of its subclasses.
734+
735+
Known subclasses of :class:`DSSCodeEnvSettings` include :class:`DSSDesignCodeEnvSettings`
736+
and :class:`DSSAutomationCodeEnvSettings`
737+
738+
You must use :meth:`~DSSCodeEnvSettings.save()` on the returned object to make your changes effective
739+
on the code env.
740+
741+
.. code-block:: python
742+
743+
# Example: setting the required packagd
744+
codeenv = client.get_code_env("PYTHON", "code_env_name")
745+
settings = codeenv.get_settings()
746+
settings.set_required_packages("dash==2.0.0", "bokeh<2.0")
747+
settings.save()
748+
# then proceed to update_packages()
749+
750+
:rtype: :class:`DSSCodeEnvSettings`
751+
"""
752+
data = self.client._perform_json(
753+
"GET", "/admin/code-envs/%s/%s" % (self.env_lang, self.env_name))
754+
755+
# you can't just use deploymentMode to check if it's an automation code
756+
# env, because some modes are common to both types of nodes. So we rely
757+
# on a non-null field that only the automation code envs have
758+
if data.get("versions", None) is not None:
759+
return DSSAutomationCodeEnvSettings(self, data)
760+
else:
761+
return DSSDesignCodeEnvSettings(self, data)
762+
725763

726764
########################################################
727765
# Code env actions
@@ -806,6 +844,171 @@ def get_log(self, log_name):
806844
"GET", "/admin/code-envs/%s/%s/logs/%s" % (self.env_lang, self.env_name, log_name))
807845

808846

847+
class DSSCodeEnvSettings(object):
848+
"""
849+
Base settings class for a DSS code env.
850+
Do not instantiate this class directly, use :meth:`DSSCodeEnv.get_settings`
851+
852+
Use :meth:`save` to save your changes
853+
"""
854+
855+
def __init__(self, codeenv, settings):
856+
self.codeenv = codeenv
857+
self.settings = settings
858+
859+
def get_raw(self):
860+
"""Get the raw code env settings as a dict"""
861+
return self.settings
862+
863+
@property
864+
def env_lang(self):
865+
return self.codeenv.env_lang
866+
867+
@property
868+
def env_name(self):
869+
return self.codeenv.env_name
870+
871+
def save(self):
872+
self.codeenv.client._perform_json(
873+
"PUT", "/admin/code-envs/%s/%s" % (self.env_lang, self.env_name), body=self.settings)
874+
875+
class DSSCodeEnvPackageListBearer(object):
876+
def get_required_packages(self, as_list=False):
877+
"""
878+
Return the list of required packages, as a single string
879+
880+
:param boolean as_list: if True, return the spec as a list of lines; if False, return as a single multiline string
881+
"""
882+
x = self.settings.get("specPackageList", "")
883+
return x.split('\n') if as_list else x
884+
def set_required_packages(self, *packages):
885+
"""
886+
Set the list of required packages
887+
"""
888+
self.settings["specPackageList"] = '\n'.join(packages)
889+
890+
def get_required_conda_spec(self, as_list=False):
891+
"""
892+
Return the list of required conda packages, as a single string
893+
894+
:param boolean as_list: if True, return the spec as a list of lines; if False, return as a single multiline string
895+
"""
896+
x = self.settings.get("specCondaEnvironment", "")
897+
return x.split('\n') if as_list else x
898+
def set_required_conda_spec(self, *spec):
899+
"""
900+
Set the list of required conda packages
901+
"""
902+
self.settings["specCondaEnvironment"] = '\n'.join(packages)
903+
904+
class DSSCodeEnvContainerConfsBearer(object):
905+
def get_built_for_all_container_confs(self):
906+
"""
907+
Return whether the code env creates an image for each container config
908+
"""
909+
return self.settings.get("allContainerConfs", False)
910+
def get_built_container_confs(self):
911+
"""
912+
Return the list of container configs for which the code env builds an image (if not all)
913+
"""
914+
return self.settings.get("containerConfs", [])
915+
def set_built_container_confs(self, *configs, **kwargs):
916+
"""
917+
Set the list of container configs for which the code env builds an image
918+
919+
:param boolean all: if True, an image is built for each config
920+
:param list configs: list of configuration names to build images for
921+
"""
922+
all = kwargs.get("all", False)
923+
self.settings['allContainerConfs'] = all
924+
if not all:
925+
self.settings['containerConfs'] = configs
926+
def built_for_all_spark_kubernetes_confs(self):
927+
"""
928+
Return whether the code env creates an image for each managed Spark over Kubernetes config
929+
"""
930+
return self.settings.get("allSparkKubernetesConfs", False)
931+
def get_built_spark_kubernetes_confs(self):
932+
"""
933+
Return the list of managed Spark over Kubernetes configs for which the code env builds an image (if not all)
934+
"""
935+
return self.settings.get("sparkKubernetesConfs", [])
936+
def set_built_spark_kubernetes_confs(self, *configs, **kwargs):
937+
"""
938+
Set the list of managed Spark over Kubernetes configs for which the code env builds an image
939+
940+
:param boolean all: if True, an image is built for each config
941+
:param list configs: list of configuration names to build images for
942+
"""
943+
all = kwargs.get("all", False)
944+
self.settings['allSparkKubernetesConfs'] = all
945+
if not all:
946+
self.settings['sparkKubernetesConfs'] = configs
947+
948+
949+
class DSSDesignCodeEnvSettings(DSSCodeEnvSettings, DSSCodeEnvPackageListBearer, DSSCodeEnvContainerConfsBearer):
950+
"""
951+
Base settings class for a DSS code env on a design node.
952+
Do not instantiate this class directly, use :meth:`DSSCodeEnv.get_settings`
953+
954+
Use :meth:`save` to save your changes
955+
"""
956+
957+
def __init__(self, codeenv, settings):
958+
super(DSSDesignCodeEnvSettings, self).__init__(codeenv, settings)
959+
960+
961+
class DSSAutomationCodeEnvSettings(DSSCodeEnvSettings, DSSCodeEnvContainerConfsBearer):
962+
"""
963+
Base settings class for a DSS code env on an automation node.
964+
Do not instantiate this class directly, use :meth:`DSSCodeEnv.get_settings`
965+
966+
Use :meth:`save` to save your changes
967+
"""
968+
969+
def __init__(self, codeenv, settings):
970+
super(DSSAutomationCodeEnvSettings, self).__init__(codeenv, settings)
971+
972+
973+
def get_version(self, version_id=None):
974+
"""
975+
Get a specific code env version (for versioned envs) or the single
976+
version
977+
978+
:param string version_id: for versioned code env, identifier of the desired version
979+
980+
:rtype: :class:`DSSAutomationCodeEnvVersionSettings`
981+
"""
982+
deployment_mode = self.settings.get("deploymentMode", None)
983+
if deployment_mode in ['AUTOMATION_SINGLE']:
984+
return DSSAutomationCodeEnvVersionSettings(self.codeenv, self.settings.get('currentVersion', {}))
985+
elif deployment_mode in ['AUTOMATION_VERSIONED']:
986+
versions = self.settings.get("versions", [])
987+
version_ids = [v.get('versionId') for v in versions]
988+
if version_id is None:
989+
raise Exception("A version id is required in a versioned code env. Existing ids: %s" % ', '.join(version_ids))
990+
for version in versions:
991+
if version_id == version.get("versionId"):
992+
return DSSAutomationCodeEnvVersionSettings(self.codeenv, version)
993+
raise Exception("Version %s not found in : %s" % (version_id, ', '.join(version_ids)))
994+
elif deployment_mode in ['PLUGIN_NON_MANAGED', 'PLUGIN_MANAGED', 'AUTOMATION_NON_MANAGED_PATH', 'EXTERNAL_CONDA_NAMED']:
995+
return DSSAutomationCodeEnvVersionSettings(self.codeenv, self.settings.get('noVersion', {}))
996+
else:
997+
raise Exception("Unexpected deployment mode %s for an automation node code env. Alter the settings directly with get_raw()", deployment_mode)
998+
999+
class DSSAutomationCodeEnvVersionSettings(DSSCodeEnvPackageListBearer):
1000+
"""
1001+
Base settings class for a DSS code env version on an automation node.
1002+
Do not instantiate this class directly, use :meth:`DSSAutomationCodeEnvSettings.get_version`
1003+
1004+
Use :meth:`save` on the :class:`DSSAutomationCodeEnvSettings` to save your changes
1005+
"""
1006+
1007+
def __init__(self, codeenv_settings, version_settings):
1008+
self.codeenv_settings = codeenv_settings
1009+
self.settings = version_settings
1010+
1011+
8091012
class DSSGlobalApiKey(object):
8101013
"""
8111014
A global API key on the DSS instance
@@ -1004,3 +1207,65 @@ def get_raw(self):
10041207
Gets the whole status as a raw dictionary.
10051208
"""
10061209
return self.status
1210+
1211+
1212+
class DSSInstanceVariables(dict):
1213+
"""
1214+
Dict containing the instance variables. The variables can be modified directly in the dict and persisted using its :meth:`save` method.
1215+
1216+
Do not create this directly, use :meth:`dataikuapi.DSSClient.get_global_variables`
1217+
"""
1218+
1219+
def __init__(self, client, variables):
1220+
super(dict, self).__init__()
1221+
self.update(variables)
1222+
self.client = client
1223+
1224+
def save(self):
1225+
"""
1226+
Save the changes made to the instance variables.
1227+
1228+
Note: this call requires an API key with admin rights.
1229+
"""
1230+
return self.client._perform_empty("PUT", "/admin/variables/", body=self)
1231+
1232+
1233+
class DSSGlobalUsageSummary(object):
1234+
"""
1235+
The summary of the usage of the DSS instance.
1236+
Do not create this directly, use :meth:`dataikuapi.dss.DSSClient.get_global_usage_summary`
1237+
"""
1238+
def __init__(self, data):
1239+
self.data = data
1240+
1241+
@property
1242+
def raw(self):
1243+
return self.data
1244+
1245+
@property
1246+
def projects_count(self):
1247+
return self.data["projects"]
1248+
1249+
@property
1250+
def total_datasets_count(self):
1251+
return self.data["datasets"]["all"]
1252+
1253+
@property
1254+
def total_recipes_count(self):
1255+
return self.data["recipes"]["all"]
1256+
1257+
@property
1258+
def total_jupyter_notebooks_count(self):
1259+
return self.data["notebooks"]["nbJupyterNotebooks"]
1260+
1261+
@property
1262+
def total_sql_notebooks_count(self):
1263+
return self.data["notebooks"]["nbSqlNotebooks"]
1264+
1265+
@property
1266+
def total_scenarios_count(self):
1267+
return self.data["scenarios"]["all"]
1268+
1269+
@property
1270+
def total_active_with_trigger_scenarios_count(self):
1271+
return self.data["scenarios"]["activeWithTriggers"]

dataikuapi/dss/app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ def __init__(self, client, project_key):
135135
DSSAppInstance.__init__(self, client,project_key)
136136

137137
def close(self):
138-
self.get_as_project().delete(drop_data=True)
138+
self.get_as_project().delete(clear_managed_datasets=True)
139139

140140
def __enter__(self,):
141141
return self

dataikuapi/dss/flow.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,22 @@ def add_item(self, obj):
338338
self._raw = self.client._perform_json("POST", "/projects/%s/flow/zones/%s/items" % (self.flow.project.project_key, self.id),
339339
body=self.flow._to_smart_ref(obj))
340340

341+
def add_items(self, items):
342+
"""
343+
Adds items to this zone.
344+
345+
The items will automatically be moved from their existing zones. Additional items may be moved to this zone
346+
as a result of the operations (notably the recipe generating the `items`).
347+
348+
:param list items: A list of objects, either :class:`dataikuapi.dss.dataset.DSSDataset`, :class:`dataikuapi.dss.managedfolder.DSSManagedFolder`,
349+
or :class:`dataikuapi.dss.savedmodel.DSSSavedModel` to add to the zone
350+
"""
351+
smart_refs = []
352+
for item in items:
353+
smart_refs.append(self.flow._to_smart_ref(item))
354+
self._raw = self.client._perform_json("POST", "/projects/%s/flow/zones/%s/add-items" % (self.flow.project.project_key, self.id),
355+
body=smart_refs)
356+
341357
@property
342358
def items(self):
343359
"""

dataikuapi/dss/managedfolder.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -111,18 +111,19 @@ def put_file(self, path, f):
111111

112112
def upload_folder(self, path, folder):
113113
"""
114-
Upload folder and its content as path in the managed folder.
114+
Upload the content of a folder at path in the managed folder.
115+
116+
Note: upload_folder("target", "source") will result in "target" containing the content
117+
of "source", not in "target" containing "source".
115118
116119
:param str path: the destination path of the folder in the managed folder
117120
:param str folder: path (absolute or relative) of the source folder to upload
118121
"""
119-
real_root = os.path.realpath(folder)
120-
for root, _, files in os.walk(real_root):
122+
for root, _, files in os.walk(folder):
121123
for file in files:
122124
filename = os.path.join(root, file)
123-
relpath = os.path.relpath(filename, real_root)
124125
with open(filename, "rb") as f:
125-
self.put_file(os.path.join(path, relpath), f)
126+
self.put_file(os.path.join(path, os.path.relpath(filename, folder)), f)
126127

127128
########################################################
128129
# Managed folder actions

dataikuapi/dss/project.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,17 +83,25 @@ def move_to_folder(self, folder):
8383
# Project deletion
8484
########################################################
8585

86-
def delete(self, drop_data=False):
86+
def delete(self, clear_managed_datasets=False, clear_output_managed_folders=False, clear_job_and_scenario_logs=True, **kwargs):
8787
"""
8888
Delete the project
8989
9090
This call requires an API key with admin rights
9191
92-
:param bool drop_data: Should the data of managed datasets be dropped
92+
:param bool clear_managed_datasets: Should the data of managed datasets be cleared
93+
:param bool clear_output_managed_folders: Should the data of managed folders used as outputs of recipes be cleared
94+
:param bool clear_job_and_scenario_logs: Should the job and scenario logs be cleared
9395
"""
96+
# For backwards compatibility
97+
if 'drop_data' in kwargs and kwargs['drop_data']:
98+
clear_managed_datasets = True
99+
94100
return self.client._perform_empty(
95-
"DELETE", "/projects/%s" % self.project_key, params = {
96-
"dropData": drop_data
101+
"DELETE", "/projects/%s" % self.project_key, params={
102+
"clearManagedDatasets": clear_managed_datasets,
103+
"clearOutputManagedFolders": clear_output_managed_folders,
104+
"clearJobAndScenarioLogs": clear_job_and_scenario_logs
97105
})
98106

99107
########################################################
@@ -721,8 +729,6 @@ def create_mlflow_pyfunc_model(self, name, prediction_type = None):
721729
:param string name: Human readable name for the new saved model in the flow
722730
:param string prediction_type: Optional (but needed for most operations). One of BINARY_CLASSIFICATION, MULTICLASS or REGRESSION
723731
"""
724-
if not name:
725-
raise ValueError("name can not be empty")
726732
model = {
727733
"savedModelType" : "MLFLOW_PYFUNC",
728734
"predictionType" : prediction_type,

0 commit comments

Comments
 (0)