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"], w) 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
90 changes: 90 additions & 0 deletions dataikuapi/dss/webapp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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, state=None):
"""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
self.state = state

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

:return: the state of the webapp
"""
return self.state

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

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
"""
future = self.client._perform_json("PUT", "/projects/%s/webapps/%s/restart-backend" % (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 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
:returns: a wrapper to a future
"""
future = 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))
return future
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
74 changes: 74 additions & 0 deletions tests/webapps_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
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 list_webapps_test(self):
webapps = self.project.list_webapps();
ok_(len(webapps) > 0)

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

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

def update_python_webapp_test(self):
webapp = self.project.get_webapp(testWebAppPythonId)
definition = webapp.get_definition()
old_def = dict(definition.get_definition())
future = definition.save()
ok_(future is not None)
eq_(remove_key(definition.get_definition(), "versionTag"), remove_key(old_def, "versionTag"))
eq_(definition.get_definition()["versionTag"]["versionNumber"], old_def["versionTag"]["versionNumber"] + 1)

def restart_backend_test(self):
"""
WARNING: you should manually stop the backend before this test
"""
filtered_webapps = [w for w in self.project.list_webapps() if w.webapp_id == testWebAppPythonId]
ok_(not filtered_webapps[0].get_state()["backendRunning"], "The backend should be stopped before the test")
future = filtered_webapps[0].restart_backend()
future.wait_for_result()
filtered_webapps = [w for w in self.project.list_webapps() if w.webapp_id == testWebAppPythonId]
ok_(filtered_webapps[0].get_state()["backendRunning"])

def stop_backend_test(self):
"""
WARNING: you should manually start the backend before this test
"""
filtered_webapps = [w for w in self.project.list_webapps() if w.webapp_id == testWebAppPythonId]
ok_(filtered_webapps[0].get_state()["backendRunning"],"The backend should be started before the test")
filtered_webapps[0].stop_backend()
sleep(2)
filtered_webapps = [w for w in self.project.list_webapps() if w.webapp_id == testWebAppPythonId]
ok_(not filtered_webapps[0].get_state()["backendRunning"])