diff --git a/osm_fieldwork/OdkCentral.py b/osm_fieldwork/OdkCentral.py index 82f283ca..be6d713a 100755 --- a/osm_fieldwork/OdkCentral.py +++ b/osm_fieldwork/OdkCentral.py @@ -54,7 +54,17 @@ def downloadThread( xforms: list, odk_credentials: dict ): - """Download a list of submissions from ODK Central""" + """ + Download a list of submissions from ODK Central + + Args: + project_id (int): The ID of the project on ODK Central + xforms (list): A list of the XForms to down the submissions from + odk_credentials (dict): The authentication credentials for ODK Collect + + Returns: + (list): The submissions in JSON format + """ timer = Timer(text="downloadThread() took {seconds:.0f}s") timer.start() data = list() @@ -79,11 +89,21 @@ def downloadThread( class OdkCentral(object): def __init__(self, - url: str = None, - user: str = None, - passwd: str = None - ): - """A Class for accessing an ODK Central server via it's REST API""" + url: str = None, + user: str = None, + passwd: str = None + ): + """ + A Class for accessing an ODK Central server via it's REST API + + Args: + url (str): The URL of the ODK Central + user (str): The user's account name on ODK Central + passwd (str): The user's account password on ODK Central + + Returns: + (OdkCentral): An instance of this class + """ if not url: url = os.getenv("ODK_CENTRAL_URL", default=None) self.url = url @@ -148,11 +168,21 @@ def __init__(self, self.cores = info['count'] def authenticate(self, - url:str = None, - user:str = None, - passwd:str = None + url: str = None, + user: str = None, + passwd: str = None ): - """Setup authenticate to an ODK Central server""" + """ + Setup authenticate to an ODK Central server. + + Args: + url (str): The URL of the ODK Central + user (str): The user's account name on ODK Central + passwd (str): The user's account password on ODK Central + + Returns: + (HTTPBasicAuth): A session to the ODK Central server + """ if not self.url: self.url = url if not self.user: @@ -166,8 +196,13 @@ def authenticate(self, return self.session.get(self.url, auth=self.auth, verify=self.verify) def listProjects(self): - """Fetch a list of projects from an ODK Central server, and - store it as an indexed list.""" + """ + Fetch a list of projects from an ODK Central server, and + store it as an indexed list. + + Returns: + (list): A list of projects on a ODK Central server + """ log.info("Getting a list of projects from %s" % self.url) url = f'{self.base}projects' result = self.session.get(url, auth=self.auth, verify=self.verify) @@ -183,17 +218,21 @@ def listProjects(self): def createProject(self, name: str ): - """Create a new project on an ODK Central server if it doesn't - already exist""" + """ + Create a new project on an ODK Central server if it doesn't + already exist + + Args: + name (str): The name for the new project + + Returns: + (json): The response from ODK Central + """ log.debug(f"Checking if project named {name} exists already") exists = self.findProject(name=name) if exists: log.debug(f"Project named {name} already exists.") - return exists - else: - url = f"{self.base}projects" - log.debug(f"POSTing project {name} to {url} with verify={self.verify}") - try: + returq try: result = self.session.post( url, auth=self.auth, json={"name": name}, verify=self.verify, timeout=4 ) @@ -210,7 +249,15 @@ def createProject(self, def deleteProject(self, project_id: int ): - """Delete a project on an ODK Central server""" + """ + Delete a project on an ODK Central server + + Args: + project_id (int): The ID of the project on ODK Central + + Returns: + (str): The project name + """ url = f"{self.base}projects/{project_id}" self.session.delete(url, auth=self.auth, verify=self.verify) # update the internal list of projects @@ -218,10 +265,17 @@ def deleteProject(self, return self.findProject(project_id=project_id) def findProject(self, - project_id: int = None, name: str = None ): - """Get the project data from Central""" + """ + Get the project data from Central + Args: + project_id (int): The project ID on ODK Central + name (str): The name of the project + + Returns: + (dict): the project data from ODK Central + """ # First, populate self.projects self.listProjects() @@ -244,7 +298,16 @@ def findAppUser(self, user_id: int, name: str = None ): - """Get the data for an app user""" + """ + Get the data for an app user + + Args: + user_id (int): The user ID of the app-user on ODK Central + name (str): The name of the app-user on ODK Central + + Returns: + (dict): The data for an app-user on ODK Central + """ if self.appusers: if name is not None: result = [d for d in self.appusers if d['displayName']==name] @@ -263,7 +326,12 @@ def findAppUser(self, return None def listUsers(self): - """Fetch a list of users on the ODK Central server""" + """ + Fetch a list of users on the ODK Central server + + Returns: + (list): A list of users on ODK Central, not app-users + """ log.info("Getting a list of users from %s" % self.url) url = self.base + "users" result = self.session.get(url, auth=self.auth, verify=self.verify) @@ -290,8 +358,20 @@ def dump(self): class OdkProject(OdkCentral): """Class to manipulate a project on an ODK Central server""" - - def __init__(self, url=None, user=None, passwd=None): + def __init__(self, + url: str = None, + user: str = None, + passwd: str = None + ): + """ + Args: + url (str): The URL of the ODK Central + user (str): The user's account name on ODK Central + passwd (str): The user's account password on ODK Central + + Returns: + (OdkProject): An instance of this object + """ super().__init__(url, user, passwd) self.forms = list() self.submissions = list() @@ -302,23 +382,45 @@ def __init__(self, url=None, user=None, passwd=None): def getData(self, keyword: str ): - return self.data[keyword] + """ + Args: + keyword (str): The keyword to search for + Returns: + (json): The data for the keyword + """ + return self.data[keyword] def listForms(self, - xform: str + project_id: int ): - """Fetch a list of forms in a project on an ODK Central server.""" + """ + Fetch a list of forms in a project on an ODK Central server. + + Args: + project_id (int): The ID of the project on ODK Central + + Returns: + (list): The list of XForms in this project + """ url = f"{self.base}projects/{xform}/forms" result = self.session.get(url, auth=self.auth, verify=self.verify) self.forms = result.json() return self.forms - def getAllSubmissions(self, - project_id: int, - xforms: list = None - ): - """Fetch a list of submissions in a project on an ODK Central server.""" + project_id: int, + xforms: list = None + ): + """ + Fetch a list of submissions in a project on an ODK Central server. + + Args: + project_id (int): The ID of the project on ODK Central + xforms (list): The list of XForms to get the submissions of + + Returns: + (json): All of the submissions for all of the XForm in a project + """ timer = Timer(text="getAllSubmissions() took {seconds:.0f}s") timer.start() if not xforms: @@ -367,7 +469,15 @@ def getAllSubmissions(self, def listAppUsers(self, projectId: int ): - """Fetch a list of app users for a project from an ODK Central server.""" + """ + Fetch a list of app users for a project from an ODK Central server. + + Args: + projectId (int): The ID of the project on ODK Central + + Returns: + (list): A list of app-users on ODK Central for this project + """ url = f"{self.base}projects/{projectId}/app-users" result = self.session.get(url, auth=self.auth, verify=self.verify) self.appusers = result.json() @@ -376,7 +486,15 @@ def listAppUsers(self, def listAssignments(self, projectId: int ): - """List the Role & Actor assignments for users on a project""" + """ + List the Role & Actor assignments for users on a project + + Args: + projectId (int): The ID of the project on ODK Central + + Returns: + (json): The list of assignments + """ url = f"{self.base}projects/{projectId}/assignments" result = self.session.get(url, auth=self.auth, verify=self.verify) return result.json() @@ -384,7 +502,15 @@ def listAssignments(self, def getDetails(self, projectId: int ): - """Get all the details for a project on an ODK Central server""" + """ + Get all the details for a project on an ODK Central server + + Args: + projectId (int): The ID of the project on ODK Central + + Returns: + (json): Get the data about a project on ODK Central + """ url = f"{self.base}projects/{projectId}" result = self.session.get(url, auth=self.auth, verify=self.verify) self.data = result.json() @@ -393,7 +519,15 @@ def getDetails(self, def getFullDetails(self, projectId: int ): - """Get extended details for a project on an ODK Central server""" + """ + Get extended details for a project on an ODK Central server + + Args: + projectId (int): The ID of the project on ODK Central + + Returns: + (json): Get the data about a project on ODK Central + """ url = f"{self.base}projects/{projectId}" self.session.headers.update({"X-Extended-Metadata": "true"}) result = self.session.get(url, auth=self.auth, verify=self.verify) @@ -427,6 +561,15 @@ def __init__(self, user: str = None, passwd: str = None ): + """ + Args: + url (str): The URL of the ODK Central + user (str): The user's account name on ODK Central + passwd (str): The user's account password on ODK Central + + Returns: + (OdkForm): An instance of this object + """ super().__init__(url, user, passwd) self.name = None # Draft is for a form that isn't published yet @@ -442,25 +585,38 @@ def __init__(self, # self.xmlFormId = None # self.projectId = None - def getName(self): - """Extract the name from a form on an ODK Central server""" - if "name" in self.data: - return self.data["name"] - else: - log.warning("Execute OdkForm.getDetails() to get this data.") - - def getFormId(self): - """Extract the xmlFormId from a form on an ODK Central server""" - if "xmlFormId" in self.data: - return self.data["xmlFormId"] - else: - log.warning("Execute OdkForm.getDetails() to get this data.") + # def getName(self): + # """ + # Extract the name from a form on an ODK Central server + # + # Returns: + # """ + # if "name" in self.data: + # return self.data["name"] + # else: + # log.warning("Execute OdkForm.getDetails() to get this data.") + + # def getFormId(self): + # """Extract the xmlFormId from a form on an ODK Central server""" + # if "xmlFormId" in self.data: + # return self.data["xmlFormId"] + # else: + # log.warning("Execute OdkForm.getDetails() to get this data.") def getDetails(self, projectId: int, xform: str ): - """Get all the details for a form on an ODK Central server""" + """ + Get all the details for a form on an ODK Central server + + Args: + projectId (int): The ID of the project on ODK Central + xform (str): The XForm to get the details of from ODK Central + + Returns: + (json): The data for this XForm + """ url = f"{self.base}projects/{projectId}/forms/{xform}" result = self.session.get(url, auth=self.auth, verify=self.verify) self.data = result.json() @@ -468,7 +624,18 @@ def getDetails(self, def getFullDetails(self, projectId: int, - xform: str): + xform: str + ): + """ + Get the full details for a form on an ODK Central server + + Args: + projectId (int): The ID of the project on ODK Central + xform (str): The XForm to get the details of from ODK Central + + Returns: + (json): The data for this XForm + """ url = f"{self.base}projects/{projectId}/forms/{xform}" self.session.headers.update({"X-Extended-Metadata": "true"}) result = self.session.get(url, auth=self.auth, verify=self.verify) @@ -479,7 +646,16 @@ def listSubmissionBasicInfo(self, projectId: int, xform: str ): - """Fetch a list of submission instances basic information for a given form.""" + """ + Fetch a list of submission instances basic information for a given form. + + Args: + projectId (int): The ID of the project on ODK Central + xform (str): The XForm to get the details of from ODK Central + + Returns: + (json): The data for this XForm + """ url = f"{self.base}projects/{projectId}/forms/{xform}/submissions" result = self.session.get(url, auth=self.auth, verify=self.verify) return result.json() @@ -489,7 +665,16 @@ def listSubmissions(self, projectId: int, xform: str ): - """Fetch a list of submission instances for a given form.""" + """ + Fetch a list of submission instances for a given form. + + Args: + projectId (int): The ID of the project on ODK Central + xform (str): The XForm to get the details of from ODK Central + + Returns: + (list): The list of Submissions + """ url = f"{self.base}projects/{projectId}/forms/{xform}.svc/Submissions" result = self.session.get(url, auth=self.auth, verify=self.verify) if result.ok: @@ -502,7 +687,18 @@ def listAssignments(self, projectId: int, xform: str ): - """List the Role & Actor assignments for users on a project""" + """ + List the Role & Actor assignments for users on a project + + Fetch a list of submission instances basic information for a given form. + + Args: + projectId (int): The ID of the project on ODK Central + xform (str): The XForm to get the details of from ODK Central + + Returns: + (json): The data for this XForm + """ url = f"{self.base}projects/{projectId}/forms/{xform}/assignments" result = self.session.get(url, auth=self.auth, verify=self.verify) return result.json() @@ -514,7 +710,19 @@ def getSubmissions(self, disk: bool = False, json: bool = True ): - """Fetch a CSV file of the submissions without media to a survey form.""" + """ + Fetch a CSV or JSON file of the submissions without media to a survey form. + + Args: + projectId (int): The ID of the project on ODK Central + xform (str): The XForm to get the details of from ODK Central + submission_id (int): The ID of the submissions to download + disk (bool): Whether to write the downloaded file to disk + json (bool): Download JSON or CSV format + + Returns: + (list): The lit of submissions + """ headers = {"Content-Type": "application/json"} now = datetime.now() timestamp = f"{now.year}_{now.hour}_{now.minute}" @@ -551,7 +759,16 @@ def getSubmissionMedia(self, projectId: int, xform: str ): - """Fetch a ZIP file of the submissions with media to a survey form.""" + """ + Fetch a ZIP file of the submissions with media to a survey form. + + Args: + projectId (int): The ID of the project on ODK Central + xform (str): The XForm to get the details of from ODK Central + + Returns: + (list): The media file + """ url = self.base + f"projects/{projectId}/forms/{xform}/submissions.csv.zip" result = self.session.get(url, auth=self.auth, verify=self.verify) return result @@ -560,7 +777,13 @@ def addMedia(self, media: str, filespec: str ): - """Add a data file to this form""" + """ + Add a data file to this form + + Args: + media (str): The media file + filespec (str): the name of the media + """ # FIXME: this also needs the data self.media[filespec] = media @@ -569,14 +792,29 @@ def addXMLForm(self, xmlFormId: int, xform: str ): - """Add an XML file to this form""" + """ + Add an XML file to this form + + Args: + projectId (int): The ID of the project on ODK Central + xform (str): The XForm to get the details of from ODK Central + """ self.xml = xform def listMedia(self, projectId: int, xform: str ): - """List all the attchements for this form""" + """ + List all the attchements for this form + + Args: + projectId (int): The ID of the project on ODK Central + xform (str): The XForm to get the details of from ODK Central + + Returns: + (list): A list of al the media files for this project + """ if self.draft: url = f"{self.base}projects/{projectId}/forms/{xform}/draft/attachments" else: @@ -592,7 +830,15 @@ def uploadMedia(self, filespec: str, convert_to_draft: bool = True ): - """Upload an attachement to the ODK Central server""" + """ + Upload an attachement to the ODK Central server + + Args: + projectId (int): The ID of the project on ODK Central + xform (str): The XForm to get the details of from ODK Central + filespec (str): The filespec of the media file + convert_to_draft (bool): Whether to convert a published XForm to draft + """ title = os.path.basename(os.path.splitext(filespec)[0]) datafile = f"{title}.geojson" xid = xform.split('_')[2] @@ -627,7 +873,17 @@ def getMedia(self, xform: str, filename: str ): - """Fetch a specific attachment by filename from a submission to a form.""" + """ + Fetch a specific attachment by filename from a submission to a form. + + Args: + projectId (int): The ID of the project on ODK Central + xform (str): The XForm to get the details of from ODK Central + filename (str): The name of the attachment for the XForm on ODK Central + + Returns: + (bytes): The media data + """ if self.draft: url = f"{self.base}projects/{projectId}/forms/{xform}/draft/attachments/{filename}" else: @@ -647,7 +903,18 @@ def createForm(self, filespec: str, draft: bool = False ): - """Create a new form on an ODK Central server""" + """ + Create a new form on an ODK Central server + + Args: + projectId (int): The ID of the project on ODK Central + xform (str): The XForm to get the details of from ODK Central + filespec (str): The name of the attachment for the XForm on ODK Central + draft (bool): Whether to create the XForm in draft or published + + Returns: + + """ if draft is not None: self.draft = draft headers = {"Content-Type": "application/xml"} @@ -678,7 +945,15 @@ def deleteForm(self, projectId: int, xform: str ): - """Delete a form from an ODK Central server""" + """ + Delete a form from an ODK Central server + + Args: + projectId (int): The ID of the project on ODK Central + xform (str): The XForm to get the details of from ODK Central + Returns: + (bool): did it get deleted + """ # FIXME: If your goal is to prevent it from showing up on survey clients like ODK Collect, consider # setting its state to closing or closed if self.draft: @@ -692,7 +967,16 @@ def publishForm(self, projectId: int, xform: str ): - """Publish a draft form. When creating a form that isn't a draft, it can get publised then""" + """ + Publish a draft form. When creating a form that isn't a draft, it can get publised then + + Args: + projectId (int): The ID of the project on ODK Central + xform (str): The XForm to get the details of from ODK Central + + Returns: + (int): The staus code from ODK Central + """ version = now = datetime.now().strftime("%Y-%m-%dT%TZ") if xform.find("_") > 0: xid = xform.split('_')[2] @@ -720,7 +1004,17 @@ def dump(self): class OdkAppUser(OdkCentral): def __init__(self, url=None, user=None, passwd=None): - """A Class for app user data""" + """ + A Class for app user data + + Args: + url (str): The URL of the ODK Central + user (str): The user's account name on ODK Central + passwd (str): The user's account password on ODK Central + + Returns: + (OdkAppUser): An instance of this object + """ super().__init__(url, user, passwd) self.user = None self.qrcode = None @@ -730,7 +1024,16 @@ def create(self, projectId: int, name: str ): - """Create a new app-user for a form""" + """ + Create a new app-user for a form + + Args: + projectId (int): The ID of the project on ODK Central + name (str): The name of the XForm + + Returns: + (bool): Whether it was created or not + """ url = f"{self.base}projects/{projectId}/app-users" result = self.session.post( url, auth=self.auth, json={"displayName": name}, verify=self.verify @@ -742,7 +1045,16 @@ def delete(self, projectId: int, userId: int ): - """Create a new app-user for a form""" + """ + Create a new app-user for a form + + Args: + projectId (int): The ID of the project on ODK Central + userID (int): The ID of the user on ODK Central to delete + + Returns: + bool): Whether the user got deleted or not + """ url = f"{self.base}projects/{projectId}/app-users/{userId}" result = self.session.delete(url, auth=self.auth, verify=self.verify) return result @@ -753,7 +1065,18 @@ def updateRole(self, roleId: int = 2, actorId: int = None ): - """Update the role of an app user for a form""" + """ + Update the role of an app user for a form + + Args: + projectId (int): The ID of the project on ODK Central + xform (str): The XForm to get the details of from ODK Central + roleId (int): The role for the user + actorId (int): The ID of the user + + Returns + (bool): Whether it was update or not + """ log.info("Update access to XForm %s for %s" % (xform, actorId)) url = f"{self.base}projects/{projectId}/forms/{xform}/assignments/{roleId}/{actorId}" result = self.session.post(url, auth=self.auth, verify=self.verify) @@ -766,7 +1089,19 @@ def grantAccess(self, xform: str = None, actorId: int = None ): - """Grant access to an app user for a form""" + """ + Grant access to an app user for a form + + Args: + projectId (int): The ID of the project on ODK Central + roleId (int): The role ID + userId (int): The user ID of the user on ODK Central + xform (str): The XForm to get the details of from ODK Central + actorId (int): The actor ID of the user on ODK Central + + Returns + (bool): Whether access was granted or not + """ url = f"{self.base}projects/{projectId}/forms/{xform}/assignments/{roleId}/{actorId}" result = self.session.post(url, auth=self.auth, verify=self.verify) return result @@ -776,7 +1111,17 @@ def createQRCode(self, token: str, name: str ): - """Get the QR Code for an app-user""" + """ + Get the QR Code for an app-user + + Args: + project_id (int): The ID of the project on ODK Central + token (str): The user's token + name (str): The name of the project + + Returns: + (bytes): The new QR code + """ log.info( 'Generating QR Code for app-user "%s" for project %s' % (name, project_id) ) @@ -799,6 +1144,10 @@ def createQRCode(self, # This following code is only for debugging purposes, since this is easier # to use a debugger with instead of pytest. if __name__ == '__main__': + """ + This main function lets this class be run standalone by a bash script + for development purposes. To use it, try the odk_client program instead. + """ logging.basicConfig( level=log_level, format=(