def MgetRubrics(self): """Retrieve list of all rubrics from server. Raises: PlomAuthenticationException: Authentication error. PlomSeriousException: any other unexpected error. Returns: list: list of dicts, possibly an empty list if server has no rubrics. """ self.SRmutex.acquire() try: response = self.session.get( "https://{}/MK/rubric".format(self.server), json={ "user": self.user, "token": self.token, }, verify=False, ) response.raise_for_status() return response.json() except requests.HTTPError as e: if response.status_code == 401: raise PlomAuthenticationException() from None else: raise PlomSeriousException( "Error of type {} getting rubric list".format(e)) from None finally: self.SRmutex.release()
def MgetRubricsByQuestion(self, question_number): """Retrieve list of all rubrics from server for given question. Args: question_number (int) Raises: PlomAuthenticationException: Authentication error. PlomSeriousException: Other error types, possible needs fix or debugging. Returns: list: list of dicts, possibly an empty list if server has no rubrics for this question. """ self.SRmutex.acquire() try: response = self.session.get( "https://{}/MK/rubric/{}".format(self.server, question_number), json={ "user": self.user, "token": self.token, }, verify=False, ) response.raise_for_status() return response.json() except requests.HTTPError as e: if response.status_code == 401: raise PlomAuthenticationException() from None else: raise PlomSeriousException( "Error of type {} getting rubric list".format(e)) from None finally: self.SRmutex.release()
def requestAndSaveToken(self, user, pw): """Get a authorisation token from the server. The token is then used to authenticate future transactions with the server. raises: PlomAPIException: a mismatch between server/client versions. PlomExistingLoginException: user already has a token: currently, we do not support getting another one. PlomAuthenticationException: wrong password, account disabled, etc: check contents for details. PlomSeriousException: something else unexpected such as a network failure. """ self.SRmutex.acquire() try: response = self.session.put( "https://{}/users/{}".format(self.server, user), json={ "user": user, "pw": pw, "api": Plom_API_Version, "client_ver": __version__, }, verify=False, timeout=5, ) # throw errors when response code != 200. response.raise_for_status() self.token = response.json() self.user = user except requests.HTTPError as e: if response.status_code == 401: raise PlomAuthenticationException(response.json()) from None elif response.status_code == 400: raise PlomAPIException(response.json()) from None elif response.status_code == 409: raise PlomExistingLoginException(response.json()) from None else: raise PlomSeriousException( "Some other sort of error {}".format(e)) from None except requests.ConnectionError as err: raise PlomSeriousException( "Cannot connect to server\n {}\n Please check details before trying again." .format(self.server)) from None finally: self.SRmutex.release()
def McreateRubric(self, new_rubric): """Ask server to make a new rubric and get key back. Args: new_rubric (dict): the new rubric info as dict. Raises: PlomAuthenticationException: Authentication error. PlomSeriousException: Other error types, possible needs fix or debugging. Returns: list: A list of: [False] If operation was unsuccessful. [True, updated_commments_list] including the new comments. """ self.SRmutex.acquire() try: response = self.session.put( "https://{}/MK/rubric".format(self.server), json={ "user": self.user, "token": self.token, "rubric": new_rubric, }, verify=False, ) response.raise_for_status() new_key = response.json() messenger_response = [True, new_key] except requests.HTTPError as e: if response.status_code == 401: raise PlomAuthenticationException() from None elif response.status_code == 406: raise PlomSeriousException( "Rubric sent was incomplete.") from None else: raise PlomSeriousException( "Error of type {} when creating new rubric".format( e)) from None messenger_response = [False] finally: self.SRmutex.release() return messenger_response
def MsaveUserRubricTabs(self, question, tab_config): """Cache the user's rubric-tabs config for this question onto the server Args: question (int): the current question number tab_config (dict): the user's rubric pane configuration for this question. Raises: PlomAuthenticationException PlomSeriousException Returns: None """ self.SRmutex.acquire() try: response = self.session.put( "https://{}/MK/user/{}/{}".format(self.server, self.user, question), json={ "user": self.user, "token": self.token, "question": question, "rubric_config": tab_config, }, verify=False, ) response.raise_for_status() except requests.HTTPError as e: if response.status_code == 401: raise PlomAuthenticationException() from None elif response.status_code == 403: raise PlomSeriousException(response.text) from None else: raise PlomSeriousException( "Error of type {} when creating new rubric".format( e)) from None finally: self.SRmutex.release()
def getInfoShortName(self): self.SRmutex.acquire() try: response = self.session.get("https://{}/info/shortName".format( self.server), verify=False) response.raise_for_status() shortName = response.text except requests.HTTPError as e: if response.status_code == 404: raise PlomSeriousException( "Server could not find the spec - this should not happen!" ) from None else: raise PlomSeriousException( "Some other sort of error {}".format(e)) from None finally: self.SRmutex.release() return shortName
def IDreturnIDdTask(self, code, studentID, studentName): """Return a completed IDing task: identify a paper. Exceptions: PlomConflict: `studentID` already used on a different paper. PlomAuthenticationException: login problems. PlomSeriousException: other errors. """ self.SRmutex.acquire() try: response = self.session.put( "https://{}/ID/tasks/{}".format(self.server, code), json={ "user": self.user, "token": self.token, "sid": studentID, "sname": studentName, }, verify=False, ) response.raise_for_status() except requests.HTTPError as e: if response.status_code == 409: raise PlomConflict(e) from None elif response.status_code == 401: raise PlomAuthenticationException() from None elif response.status_code == 404: raise PlomSeriousException(e) from None else: raise PlomSeriousException( "Some other sort of error {}".format(e)) from None finally: self.SRmutex.release() # TODO - do we need this return value? return True
def replaceMissingHWQuestion(self, student_id=None, test=None, question=None): # can replace by SID or by test-number self.SRmutex.acquire() try: response = self.session.put( "https://{}/admin/missingHWQuestion".format(self.server), verify=False, json={ "user": self.user, "token": self.token, "question": question, "sid": student_id, "test": test, }, ) response.raise_for_status() rval = response.json() except requests.HTTPError as e: if response.status_code == 404: raise PlomSeriousException( "Server could not find the TPV - this should not happen!" ) from None elif response.status_code == 401: raise PlomAuthenticationException() from None elif response.status_code == 409: # that question already has pages raise PlomTakenException() from None else: raise PlomSeriousException( "Some other sort of error {}".format(e)) from None finally: self.SRmutex.release() return rval
def get_spec(self): """Get the specification of the exam from the server. Returns: dict: the server's spec file, as in :func:`plom.SpecVerifier`. """ self.SRmutex.acquire() try: response = self.session.get( "https://{}/info/spec".format(self.server), verify=False, ) response.raise_for_status() except requests.HTTPError as e: if response.status_code == 404: raise PlomSeriousException( "Server could not find the spec") from None else: raise PlomSeriousException( "Some other sort of error {}".format(e)) finally: self.SRmutex.release() return response.json()
def MrequestOneImage(self, task_code, image_id, md5sum): """Download one image from server by its database id. args: code (str): the task code such as "q1234g9". TODO: consider removing code/`task` from URL. image_id (int): TODO: int/str? The key into the server's database of images. md5sum (str): the expected md5sum, just for sanity checks or something I suppose. return: bytes: png/jpeg or whatever as bytes. Errors/Exceptions 401: not authenticated 404: no such image 409: wrong md5sum provided """ self.SRmutex.acquire() try: response = self.session.get( "https://{}/MK/images/{}/{}/{}".format(self.server, task_code, image_id, md5sum), json={ "user": self.user, "token": self.token }, verify=False, ) response.raise_for_status() image = BytesIO(response.content).getvalue() except requests.HTTPError as e: if response.status_code == 401: raise PlomAuthenticationException() from None elif response.status_code == 409: raise PlomConflict("Wrong md5sum provided") from None elif response.status_code == 404: raise PlomNoMoreException("Cannot find image") from None else: raise PlomSeriousException( "Some other unexpected error {}".format(e)) from None finally: self.SRmutex.release() return image
def uploadHWPage(self, sid, question, order, sname, fname, md5sum, bundle, bundle_order): self.SRmutex.acquire() try: param = { "user": self.user, "token": self.token, "fileName": sname, "sid": sid, "question": question, "order": order, "md5sum": md5sum, "bundle": bundle, "bundle_order": bundle_order, } mime_type = mimetypes.guess_type(sname)[0] dat = MultipartEncoder( fields={ "param": json.dumps(param), "originalImage": (sname, open(fname, "rb"), mime_type), # image }) response = self.session.put( "https://{}/admin/hwPages".format(self.server), json={ "user": self.user, "token": self.token }, data=dat, headers={"Content-Type": dat.content_type}, verify=False, ) response.raise_for_status() except requests.HTTPError as e: if response.status_code == 401: raise PlomAuthenticationException() from None else: raise PlomSeriousException( "Some other sort of error {}".format(e)) from None finally: self.SRmutex.release() return response.json()
def IDrequestClasslist(self): """Ask server for the classlist. Returns: list: ordered list of (student id, student name) pairs. Both are strings. Raises: PlomAuthenticationException: login troubles. PlomBenignException: server has no classlist. PlomSeriousException: all other failures. """ self.SRmutex.acquire() try: response = self.session.get( "https://{}/ID/classlist".format(self.server), json={ "user": self.user, "token": self.token }, verify=False, ) # throw errors when response code != 200. response.raise_for_status() # you can assign to the encoding to override the autodetection # TODO: define API such that classlist must be utf-8? # print(response.encoding) # response.encoding = 'utf-8' # classlist = StringIO(response.text) classlist = response.json() except requests.HTTPError as e: if response.status_code == 401: raise PlomAuthenticationException() from None elif response.status_code == 404: raise PlomBenignException( "Server cannot find the class list") from None else: raise PlomSeriousException( "Some other sort of error {}".format(e)) from None finally: self.SRmutex.release() return classlist
def MrequestWholePaper(self, code, questionNumber=0): self.SRmutex.acquire() # note - added default value for questionNumber so that this works correctly # when called from identifier. - Fixes #921 try: response = self.session.get( "https://{}/MK/whole/{}/{}".format(self.server, code, questionNumber), json={ "user": self.user, "token": self.token }, verify=False, ) response.raise_for_status() # response should be multipart = [ pageData, f1,f2,f3..] imagesAsBytes = MultipartDecoder.from_response(response).parts images = [] i = 0 for iab in imagesAsBytes: if i == 0: pageData = json.loads(iab.content) else: images.append(BytesIO( iab.content).getvalue()) # pass back image as bytes i += 1 except requests.HTTPError as e: if response.status_code == 401: raise PlomAuthenticationException() from None # TODO? elif response.status_code == 409: raise PlomTakenException( "Task taken by another user.") from None else: raise PlomSeriousException( "Some other sort of error {}".format(e)) from None finally: self.SRmutex.release() return [pageData, images]
def clearAuthorisation(self, user, pw): self.SRmutex.acquire() try: response = self.session.delete( "https://{}/authorisation".format(self.server), json={ "user": user, "password": pw }, verify=False, ) response.raise_for_status() except requests.HTTPError as e: if response.status_code == 401: raise PlomAuthenticationException() from None else: raise PlomSeriousException( "Some other sort of error {}".format(e)) from None finally: self.SRmutex.release()
def triggerUpdateAfterLUpload(self): self.SRmutex.acquire() try: response = self.session.put( "https://{}/admin/loosePagesUploaded".format(self.server), verify=False, json={ "user": self.user, "token": self.token }, ) response.raise_for_status() except requests.HTTPError as e: if response.status_code == 401: raise PlomAuthenticationException() from None else: raise PlomSeriousException( "Some other sort of error {}".format(e)) from None finally: self.SRmutex.release() return response.json()
def MdidNotFinishTask(self, code): self.SRmutex.acquire() try: response = self.session.delete( "https://{}/MK/tasks/{}".format(self.server, code), json={ "user": self.user, "token": self.token }, verify=False, ) response.raise_for_status() except requests.HTTPError as e: if response.status_code == 401: raise PlomAuthenticationException() from None else: raise PlomSeriousException( "Some other sort of error {}".format(e)) from None finally: self.SRmutex.release() return True
def getMissingHW(self): self.SRmutex.acquire() try: response = self.session.get( "https://{}/REP/missingHW".format(self.server), verify=False, json={ "user": self.user, "token": self.token }, ) response.raise_for_status() except requests.HTTPError as e: if response.status_code == 401: raise PlomAuthenticationException() from None else: raise PlomSeriousException( "Some other sort of error {}".format(e)) from None finally: self.SRmutex.release() return response.json()
def IDrequestDoneTasks(self): self.SRmutex.acquire() try: response = self.session.get( "https://{}/ID/tasks/complete".format(self.server), json={ "user": self.user, "token": self.token }, verify=False, ) response.raise_for_status() idList = response.json() except requests.HTTPError as e: if response.status_code == 401: raise PlomAuthenticationException() from None else: raise PlomSeriousException( "Some other sort of error {}".format(e)) from None finally: self.SRmutex.release() return idList
def MgetMaxMark(self, question, ver): """Get the maximum mark for this question and version. Raises: PlomRangeExeception: `question` or `ver` is out of range. PlomAuthenticationException: PlomSeriousException: something unexpected happened. """ self.SRmutex.acquire() try: response = self.session.get( "https://{}/MK/maxMark".format(self.server), json={ "user": self.user, "token": self.token, "q": question, "v": ver }, verify=False, ) # throw errors when response code != 200. response.raise_for_status() # convert the content of the response to a textfile for identifier maxMark = response.json() except requests.HTTPError as e: if response.status_code == 401: raise PlomAuthenticationException() from None elif response.status_code == 416: raise PlomRangeException(response.text) from None else: raise PlomSeriousException( "Some other sort of error {}".format(e)) from None finally: self.SRmutex.release() return maxMark
def MreturnMarkedTask( self, code, pg, ver, score, mtime, tags, aname, pname, rubrics, integrity_check, image_md5_list, ): """Upload annotated image and associated data to the server. Returns: list: a 2-list of the form `[#done, #total]`. Raises: PlomAuthenticationException PlomConflict: integrity check failed, perhaps manager altered task. PlomTimeoutError: network trouble such as timeouts. PlomTaskChangedError PlomTaskDeletedError PlomSeriousException """ # doesn't like ints, so convert ints to strings param = { "user": self.user, "token": self.token, "pg": str(pg), "ver": str(ver), "score": str(score), "mtime": str(mtime), "tags": tags, "rubrics": rubrics, "md5sum": hashlib.md5(open(aname, "rb").read()).hexdigest(), "integrity_check": integrity_check, "image_md5s": image_md5_list, } dat = MultipartEncoder( fields={ "param": json.dumps(param), "annotated": (aname, open(aname, "rb"), "image/png"), # image "plom": (pname, open(pname, "rb"), "text/plain"), # plom-file }) self.SRmutex.acquire() try: response = self.session.put( "https://{}/MK/tasks/{}".format(self.server, code), data=dat, headers={"Content-Type": dat.content_type}, verify=False, timeout=(10, 120), ) response.raise_for_status() ret = response.json() except (requests.ConnectionError, requests.Timeout) as e: raise PlomTimeoutError( "Upload timeout/connect error: {}\n\n".format(e) + "Retries are NOT YET implemented: as a workaround," + "you can re-open the Annotator on '{}'.\n\n".format(code) + "We will now process any remaining upload queue.") from None except requests.HTTPError as e: if response.status_code == 401: raise PlomAuthenticationException() from None elif response.status_code == 406: raise PlomConflict( "Integrity check failed. This can happen if manager has altered the task while you are annotating it." ) from None elif response.status_code == 409: raise PlomTaskChangedError( "Task ownership has changed.") from None elif response.status_code == 410: raise PlomTaskDeletedError( "No such task - it has been deleted from server." ) from None elif response.status_code == 400: raise PlomSeriousException( "Image file is corrupted. This should not happen" ) from None else: raise PlomSeriousException( "Some other sort of error {}".format(e)) from None finally: self.SRmutex.release() return ret
def MrequestImages(self, code, integrity_check): """Download images relevant to a question, both original and annotated. Args: code (str): the task code such as "q1234g9". Returns: 3-tuple: `(image_metadata, annotated_image, plom_file)` `image_metadata` has various stuff: DB ids, md5sums, etc `annotated_image` and `plom_file` are the png file and and data associated with a previous annotations, or None. Raises: PlomAuthenticationException PlomTaskChangedError: you no longer own this task. PlomTaskDeletedError PlomSeriousException """ self.SRmutex.acquire() try: response = self.session.get( "https://{}/MK/images/{}".format(self.server, code), json={ "user": self.user, "token": self.token, "integrity_check": integrity_check, }, verify=False, ) response.raise_for_status() # response is either [metadata] or [metadata, annotated_image, plom_file] imagesAnnotAndPlom = MultipartDecoder.from_response(response).parts image_metadata = json.loads(imagesAnnotAndPlom[0].text) if len(imagesAnnotAndPlom) == 1: # all is fine - no annotated image or plom data anImage = None plDat = None elif len(imagesAnnotAndPlom) == 3: # all fine - last two parts are annotated image + plom-data anImage = BytesIO(imagesAnnotAndPlom[1].content).getvalue() plDat = BytesIO(imagesAnnotAndPlom[2].content).getvalue() else: raise PlomSeriousException( "Number of returns doesn't make sense: should be 1 or 3 but is {}" .format(len(imagesAnnotAndPlom))) except requests.HTTPError as e: if response.status_code == 401: raise PlomAuthenticationException() from None elif response.status_code == 404: raise PlomSeriousException( "Cannot find image file for {}.".format(code)) from None elif response.status_code == 409: raise PlomTaskChangedError( "Ownership of task {} has changed.".format(code)) from None elif response.status_code == 406: raise PlomTaskChangedError( "Task {} has been changed by manager.".format( code)) from None elif response.status_code == 410: raise PlomTaskDeletedError( "Task {} has been deleted by manager.".format( code)) from None else: raise PlomSeriousException( "Some other sort of error {}".format(e)) from None finally: self.SRmutex.release() return image_metadata, anImage, plDat