diff --git a/.gitignore b/.gitignore index 8ee9e7b..358152c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ **/obj/** **/*.suo **/*.userprefs +**/__pycache__ \ No newline at end of file diff --git a/Python/api/__init__.py b/Python/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Python/api/boa_client.py b/Python/api/boa_client.py new file mode 100644 index 0000000..369013e --- /dev/null +++ b/Python/api/boa_client.py @@ -0,0 +1,378 @@ +import xmlrpc.client +import traceback +import util + +BOA_PROXY = "http://boa.cs.iastate.edu/boa/?q=boa/api" + +class NotLoggedInException(Exception): + pass + +class BoaException(Exception): + pass + +class BoaClient(object): + """ A client class for accessing boa's api + + Attributes: + server (xmlrpc.client.ServerProxy): + trans (xmlrpc.client.Transport) + """ + + def __init__(self): + """Create a new Boa API client, using the standard domain/path.""" + self.trans = util.CookiesTransport() + self.__logged_in = False + self.server = xmlrpc.client.ServerProxy(BOA_PROXY, transport=self.trans) + + def login(self, username, password): + """log into the boa framework using the remote api + + Args: + username (str): username for boa account + password (str): password for boa account + """ + try: + self.__logged_in = True + response = self.server.user.login(username, password) + self.trans.add_csrf(response["token"]) + return response + except xmlrpc.client.Fault as e: + raise BoaException(e).with_traceback(e.__traceback__) + + def close(self): + """Log out of the boa framework using the remote api + + Raises: + BoaException: if theres an issue reading from the server + """ + self.ensure_logged_in() + try: + self.server.user.logout() + self.__logged_in = False + except xmlrpc.client.Fault as e: + raise BoaException(e).with_traceback(e.__traceback__) + + def ensure_logged_in(self): + """Checks if a user is currently logged in through the remote api + + Returns: + bool: True if user is logged in, false if otherwise + + Raises: + NotLoggedInException: if user is not currently logged in + """ + if not self.__logged_in: + raise NotLoggedInException("User not currently logged in") + + def datasets(self): + """ Retrieves datasetsets currently provided by boa + + Returns: + list: a list of boa datasets + + Raises: + BoaException: if theres an issue reading from the server + """ + self.ensure_logged_in() + try: + return self.server.boa.datasets() + except xmlrpc.client.Fault as e: + raise BoaException(e).with_traceback(e.__traceback__) + + def dataset_names(self): + """Retrieves a list of names of all datasets provided by boa + + Returns: + list: the dataset names + + Raises: + BoaException: if theres an issue reading from the server + """ + self.ensure_logged_in() + try: + dataset_names = [] + datasets = self.datasets() + for x in datasets: + dataset_names.append(x['name']) + return dataset_names + except xmlrpc.client.Fault as e: + raise BoaException(e).with_traceback(e.__traceback__) + + def get_dataset(self, name): + """Retrieves a dataset given a name. + + Args: + name (str): The name of the input dataset to return. + + Returns: + dict: a dictionary with the keys id and name + + Raises: + BoaException: if theres an issue reading from the server + """ + self.ensure_logged_in() + try: + for x in self.datasets(): + if x['name'] == name: + return x + return None + except xmlrpc.client.Fault as e: + raise BoaException(e).with_traceback(e.__traceback__) + + def last_job(self): + """Retrieves the most recently submitted job + + Returns: + JobHandle: the last submitted job + + Raises: + BoaException: if theres an issue reading from the server + """ + self.ensure_logged_in() + try: + jobs = self.job_list(False, 0, 1) + return jobs[0] + except xmlrpc.client.Fault as e: + raise BoaException(e).with_traceback(e.__traceback__) + + def job_count(self, pub_only=False): + """Retrieves the number of jobs submitted by a user + + Args: + pub_only (bool, optional): if true, return only public jobs + otherwise return all jobs + + Returns: + int: the number of jobs submitted by a user + + Raises: + BoaException: if theres an issue reading from the server + """ + self.ensure_logged_in() + try: + return self.server.boa.count(pub_only) + except xmlrpc.client.Fault as e: + raise BoaException(e).with_traceback(e.__traceback__) + + def query(self, query, dataset=None): + """Submits a new query to Boa to query the specified and returns a handle to the new job. + + Args: + query (str): a boa query represented as a string. + dataset (str, optional): the name of the input dataset. + + Returns: + (JobHandle) a job + + Raises: + BoaException: if theres an issue reading from the server + """ + self.ensure_logged_in() + try: + id = 0 if dataset is None else dataset.get_id() + job = self.server.boa.submit(query, self.datasets()[id]['id']) + return util.parse_job(self, job) + except xmlrpc.client.Fault as e: + raise BoaException(e).with_traceback(e.__traceback__) + + def get_job(self, id): + """Retrieves a job given an id. + + Args: + id (int): the id of the job you want to retrieve + + Returns: + JobHandle: the desired job. + + Raises: + BoaException: if theres an issue reading from the server + """ + self.ensure_logged_in() + try: + return util.parse_job(self, self.server.boa.job(id)) + except xmlrpc.client.Fault as e: + raise BoaException(e).with_traceback(e.__traceback__) + + def job_list(self, pub_only=False, offset=0, length=1000): + """Returns a list of the most recent jobs, based on an offset and length. + + This includes public and private jobs. Returned jobs are ordered from newest to oldest + + Args: + pub_only (bool, optional): if true, only return public jobs otherwise return all jobs + offset (int, optional): the starting offset + length (int, optional): the number of jobs (at most) to return + + Returns: + list: a list of jobs where each element is a jobHandle + + Raises: + BoaException: if theres an issue reading from the server + """ + self.ensure_logged_in() + try: + list = self.server.boa.jobs(pub_only, offset, length) + newDict = [] + if(len(list) > 0): + for i in list: + newDict.append(util.parse_job(self, i)) + return newDict + except xmlrpc.client.Fault as e: + raise BoaException(e).with_traceback(e.__traceback__) + + def stop(self, job): + """Stops the execution of a job + + Args: + job (JobHandle): the job whose execution you want to stop + + Raises: + BoaException: if theres an issue reading from the server + """ + self.ensure_logged_in() + try: + self.server.job.stop(job.id) + except xmlrpc.client.Fault as e: + raise BoaException(e).with_traceback(e.__traceback__) + + def resubmit(self, job): + """Resubmits a job to the framework + + Args: + job (JobHandle): The job you want to resubmit + + Raises: + BoaException: if theres an issue reading from the server + """ + self.ensure_logged_in() + try: + self.server.job.resubmit(job.id) + except xmlrpc.client.Fault as e: + raise BoaException(e).with_traceback(e.__traceback__) + + def delete(self, job): + """Deletes this job from the framework. + + Args: + job (JobHandle): the job you want to delete + + Raises: + BoaException: if theres an issue reading from the server + """ + self.ensure_logged_in() + try: + self.server.job.delete(job.id) + except xmlrpc.client.Fault as e: + raise BoaException(e).with_traceback(e.__traceback__) + + def set_public(self, job, is_public): + """Modifies the public/private status of this job. + + Args: + is_public (bool): 'True' to make it public, False to make it private + job (JobHandle) + + Raises: + BoaException: if theres an issue reading from the server + """ + self.ensure_logged_in() + try: + if is_public is True: + self.server.job.setpublic(job.id, 1) + else: + self.server.job.setpublic(job.id, 0) + except xmlrpc.client.Fault as e: + raise BoaException(e).with_traceback(e.__traceback__) + + def public_status(self, job): + """Get the jobs public/private status. + + Args: + job (JobHandle) + + Raises: + BoaException: if theres an issue reading from the server + """ + self.ensure_logged_in() + try: + result = self.server.job.public(job.id) + if result is 1: + return True + else: + return False + except xmlrpc.client.Fault as e: + raise BoaException(e).with_traceback(e.__traceback__) + + def get_url(self, job): + """Retrieves the jobs URL. + + Args: + job (JobHandle) + + Raises: + BoaException: if theres an issue reading from the server + """ + self.ensure_logged_in() + try: + return self.server.job.url(job.id) + except xmlrpc.client.Fault as e: + raise BoaException(e).with_traceback(e.__traceback__) + + def public_url(self, job): + """Get the jobs public page URL. + + Args: + job (JobHandle) + + Raises: + BoaException: if theres an issue reading from the server + """ + self.ensure_logged_in() + try: + return self.server.job.publicurl(job.id) + except xmlrpc.client.Fault as e: + raise BoaException(e).with_traceback(e.__traceback__) + + def get_compiler_errors(self, job): + """Return any errors from trying to compile the job. + + Args: + job (JobHandle) + + Raises: + BoaException: if theres an issue reading from the server + """ + self.ensure_logged_in() + try: + return self.server.job.compilerErrors(job.id) + except xmlrpc.client.Fault as e: + raise BoaException(e).with_traceback(e.__traceback__) + + def source(self, job): + """Return the source query for this job. + + Args: + job (JobHandle) + + Raises: + BoaException: if theres an issue reading from the server + """ + self.ensure_logged_in() + try: + return self.server.job.source(job.id) + except xmlrpc.client.Fault as e: + raise BoaException(e).with_traceback(e.__traceback__) + + def output(self, job): + """Return the output for this job, if it finished successfully and has an output. + + Raises: + BoaException: if theres an issue reading from the server + """ + self.ensure_logged_in() + try: + if job.exec_status != "Finished": + return "Job is currently running" + return self.server.job.output(job.id) + except xmlrpc.client.Fault as e: + raise BoaException(e).with_traceback(e.__traceback__) \ No newline at end of file diff --git a/Python/api/job_handle.py b/Python/api/job_handle.py new file mode 100644 index 0000000..f5c9d47 --- /dev/null +++ b/Python/api/job_handle.py @@ -0,0 +1,76 @@ +class JobHandle: + """A class for handling jobs sent to the framework + + Attributes: + client (BoaClient): the xmlrpc client + id (int): the jobs id + date (str): the date and time the job was submitted + dataset (dict): the dataset used to executed the job + exec_status (str): the execution status for the job + compiler_status (str): the compiler status for the job + """ + + def __init__(self, client, id, date, dataset, compiler_status, exec_status): + self.client = client + self.id = id + self.date = date + self.dataset = dataset + self.compiler_status = compiler_status + self.exec_status = exec_status + + def __str__(self): + """string output for a job""" + return str('id: ' + str(self.id) + ', date:' + str(self.date) + + ', dataset:' + str(self.dataset) + ', compiler_status: (' + str(self.compiler_status) + ')' + +', execution_status: (' + str(self.exec_status) + ')') + + def stop(self): + """Stops the job if it is running.""" + return self.client.stop(self) + + def resubmit(self): + """Resubmits this job.""" + return self.client.resubmit(self) + + def delete(self): + """Deletes this job from the framework.""" + return self.client.delete(self) + + def get_url(self): + """Retrieves the jobs URL.""" + return self.client.get_url(self) + + def set_public(self, status): + """Modifies the public/private status of this job. + + Args: + status (bool): 'True' to make it public, False to make it private + """ + return self.client.set_public(self, status) + + def public_status(self): + """Get the jobs public/private status.""" + return self.client.public_status(self) + + def public_url(self): + """Get the jobs public page URL.""" + return self.client.public_url(self) + + def source(self): + """Return the source query for this job.""" + return self.client.source(self) + + def get_compiler_errors(self): + """Return any errors from trying to compile the job.""" + return self.client.get_compiler_errors(self) + + def output(self): + """Return the output for this job, if it finished successfully and has output.""" + return self.client.output(self) + + def refresh(self): + """Refreshes the cached data for this job.""" + job = self.client.get_job(self.id) + self.compiler_status = job.compiler_status + self.exec_status = job.exec_status + self.date = job.date \ No newline at end of file diff --git a/Python/api/util.py b/Python/api/util.py new file mode 100644 index 0000000..0e5b3c9 --- /dev/null +++ b/Python/api/util.py @@ -0,0 +1,30 @@ +import xmlrpc +from job_handle import JobHandle + +class CookiesTransport(xmlrpc.client.Transport): + """A Transport subclass that retains cookies over its lifetime.""" + + def __init__(self): + super().__init__() + self._cookies = [] + self._csrf = [] + + def add_csrf(self, token): + self._csrf.append(token) + + def send_headers(self, connection, headers): + if self._cookies: + connection.putheader("Cookie", "; ".join(self._cookies)) + connection.putheader("X-CSRF-Token", "; ".join(self._csrf)) + super().send_headers(connection, headers) + + def parse_response(self, response): + session_message = response.msg.get_all("Set-Cookie") + if session_message is not None: + for header in response.msg.get_all("Set-Cookie"): + cookie = header.split(";", 1)[0] + self._cookies.append(cookie) + return super().parse_response(response) + +def parse_job(client, job): + return JobHandle(client, job['id'], job['submitted'], job['input'], job['compiler_status'], job['hadoop_status'])