Skip to content

Client WebApp API #44

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
23 changes: 23 additions & 0 deletions dataikuapi/dss/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from .discussion import DSSObjectDiscussions
from .ml import DSSMLTask
from .analysis import DSSAnalysis
from .webapp import DSSWebApp
from dataikuapi.utils import DataikuException


Expand Down Expand Up @@ -823,6 +824,28 @@ def get_macro(self, runnable_type):
"""
return DSSMacro(self.client, self.project_key, runnable_type)

########################################################
# Webapps
########################################################

def list_webapps(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adopt the newer standard of returning either listitem or object. See list_scenarios for an up-to-date example. This allows basic listing with a single API call instead of N+1

Provide properties on the core things in the DSSWebAppListItem

"""
List the webapps heads of this project

:returns: the list of the webapps as :class:`dataikuapi.dss.webapp.DSSWebApp`
"""
webapps = self.client._perform_json("GET", "/projects/%s/webapps/" % self.project_key)
return [DSSWebApp(self.client, self.project_key, w["id"]) for w in webapps]

def get_webapp(self, webapp_id):
"""
Get a handle to interact with a specific webapp

:param webapp_id: the identifier of a webapp
:returns: A :class:`dataikuapi.dss.webapp.DSSWebApp` webapp handle
"""
return DSSWebApp(self.client, self.project_key, webapp_id)

########################################################
# Wiki
########################################################
Expand Down
121 changes: 121 additions & 0 deletions dataikuapi/dss/webapp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import sys
from .future import DSSFuture

if sys.version_info >= (3,0):
import urllib.parse
dku_quote_fn = urllib.parse.quote
else:
import urllib
dku_quote_fn = urllib.quote


class DSSWebApp(object):
"""
A handle to manage a webapp
"""
def __init__(self, client, project_key, webapp_id):
"""Do not call directly, use :meth:`dataikuapi.dss.project.DSSProject.get_webapps`"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo

self.client = client
self.project_key = project_key
self.webapp_id = webapp_id

def get_state(self):
"""
Return the state of the webapp

:return: the state of the webapp
:rtype: :class:`DSSWebAppBackendState`
"""
state = self.client._perform_json("GET", "/projects/%s/webapps/%s/backend/state" % (self.project_key, self.webapp_id))
return DSSWebAppBackendState(self.client, self.project_key, self.webapp_id, state)

def stop_backend(self):
"""
Stop a webapp
"""
self.client._perform_empty("PUT", "/projects/%s/webapps/%s/backend/actions/stop" % (self.project_key, self.webapp_id))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check with @FChataigner whether it's a good thing that this does not return a future


def restart_backend(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this corresponds to what the API really does, the fact that there is a "restart" method but no "start" method could be confusing.

Not sure what the best to do is here. Maybe call the method start_or_restart_backend

"""
Restart a webapp
:returns: a handle to a DSS future to track the progress of the restart
:rtype: :class:`dataikuapi.dss.future.DSSFuture`
"""
future = self.client._perform_json("PUT", "/projects/%s/webapps/%s/backend/actions/restart" % (self.project_key, self.webapp_id))
return DSSFuture(self.client, future["jobId"])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use DSSFuture.from_resp which properly handles the case where the future instantly succeeded or failed (here, it will do KeyError on jobId


def get_definition(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"get_definition" is the legacy naming for legacy methods that return a dict. Modern methods that return settings must be called get_settings and return an object called XXXSettings

"""
Get a webapp definition

:returns: a handle to manage the webapp definition
:rtype: :class:`dataikuapi.dss.webapp.DSSWebAppDefinition`
"""
definition = self.client._perform_json("GET", "/projects/%s/webapps/%s/" % (self.project_key, self.webapp_id))
return DSSWebAppDefinition(self.client, self.project_key, self.webapp_id, definition)


class DSSWebAppBackendState(object):
"""
A handle to manage WebApp backend state
"""
def __init__(self, client, project_key, webapp_id, state):
"""Do not call directly, use :meth:`dataikuapi.dss.webapp.DSSWebApp.get_state`"""
self.client = client
self.project_key = project_key
self.webapp_id = webapp_id
self.state = state

def get_state(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make this a property

"""
Returns the dict containing the current state of the webapp backend.
Warning : this dict is replaced when webapp backend state changes

:returns: a dict
"""
return self.state

def is_running(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make this a property called "running"

"""
Tells if the webapp app backend is running or not

:returns: a bool
"""
return "futureInfo" in self.state and \
"alive" in self.state["futureInfo"] and \
self.state["futureInfo"]["alive"]


class DSSWebAppDefinition(object):
"""
A handle to manage a WebApp definition
"""
def __init__(self, client, project_key, webapp_id, definition):
"""Do not call directly, use :meth:`dataikuapi.dss.webapp.DSSWebApp.get_definition`"""
self.client = client
self.project_key = project_key
self.webapp_id = webapp_id
self.definition = definition

def get_definition(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this would be webapp.get_definition().get_definition() :) Standard practice is to call this method get_raw

so it will be webapp.get_settings().get_raw()

"""
Get the definition

:returns: the definition of the webapp
"""
return self.definition

def set_definition(self, definition):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, no set function on a "modern" settings object. It's modification in place.

"""
Set the definition

:param definition : the definition of the webapp
"""
self.definition = definition

def save(self):
"""
Save the current webapp definition and update it.
"""
self.client._perform_json("PUT", "/projects/%s/webapps/%s" % (self.project_key, self.webapp_id), body=self.definition)
self.definition = self.client._perform_json("GET", "/projects/%s/webapps/%s" % (self.project_key, self.webapp_id))
12 changes: 6 additions & 6 deletions tests/user_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,23 @@ def list_users_test():

def create_delete_user_test():
client = DSSClient(host, apiKey)
count = len(client.list_users())
count = len(client.list_users())

user = client.create_user("toto", "password", "display name of toto", groups=['a','b'])
eq_(count + 1, len(client.list_users()))

user.delete()
eq_(count, len(client.list_users()))

def get_set_user_test():
client = DSSClient(host, apiKey)
user = client.create_user("toto", "password", "display name of toto", groups=['a','b'])

desc = user.get_definition()
desc['displayName'] = 'tata'
user.set_definition(desc)
desc2 = user.get_definition()

eq_('tata', desc2['displayName'])

user.delete()
Expand Down
82 changes: 82 additions & 0 deletions tests/webapps_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from time import sleep
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would remove this test class as it cannot be run automatically

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Talk with @Basharsh96 first :)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made a copy of the test and I'll add it later to the tests in the CI pipeline I'm working on. Thank you, you can safely remove it 😊

from dataikuapi.dssclient import DSSClient
from dataikuapi.dss.project import DSSProject
from dataikuapi.dss.webapp import DSSWebApp
from nose.tools import ok_
from nose.tools import eq_

host="http://localhost:8083"
apiKey="CMZBjFkUgcDh08S3awoPyVIweBelxPjy"
testProjectKey="WEBAPPS"
testWebAppPythonId="VCMN2ra"


def remove_key(d, key):
r = dict(d)
del r[key]
return r


class WebappApi_tests(object):

def __init__(self):
self.client = None
self.project = None;

def setUp(self):
self.client = DSSClient(host, apiKey)
self.project = DSSProject(self.client, testProjectKey)

def t01_list_webapps_test(self):
webapps = self.project.list_webapps();
ok_(len(webapps) > 0)

def t02_get_python_webapp_test(self):
webapp = self.project.get_webapp(testWebAppPythonId)
ok_(webapp is not None)

def t03_get_definition_test(self):
webapp = self.project.get_webapp(testWebAppPythonId)
definition = webapp.get_definition()
print "Definition " + str(definition)
ok_(definition is not None)
eq_(definition.webapp_id, testWebAppPythonId)
eq_(definition.get_definition()["id"], testWebAppPythonId)

def t04_update_python_webapp_test(self):
webapp = self.project.get_webapp(testWebAppPythonId)
definition = webapp.get_definition()
old_def = dict(definition.get_definition())
definition.save()
eq_(remove_key(definition.get_definition(), "versionTag"), remove_key(old_def, "versionTag"))
eq_(definition.get_definition()["versionTag"]["versionNumber"], old_def["versionTag"]["versionNumber"] + 1)

def t05_restart_backend_test(self):
"""
WARNING: you should manually stop the backend before this test
"""
webapp = self.project.get_webapp(testWebAppPythonId)
ok_(not webapp.get_state().is_running(), "The backend should be stopped before the test")
future = webapp.restart_backend()
future.wait_for_result()
ok_(webapp.get_state().is_running())

def t06_stop_backend_test(self):
"""
WARNING: you should manually start the backend before this test
"""
webapp = self.project.get_webapp(testWebAppPythonId)
ok_(webapp.get_state().is_running(),"The backend should be started before the test")
webapp.stop_backend()
sleep(2)
eq_(webapp.get_state().is_running(), False)

def t07_state_consistency_test(self):
webapp = self.project.get_webapp(testWebAppPythonId)
webapp.stop_backend()
eq_(webapp.get_state().is_running(), False)
future = webapp.restart_backend()
future.wait_for_result()
eq_(webapp.get_state().is_running(), True)
webapp.stop_backend()
eq_(webapp.get_state().is_running(), False)