Skip to content

Commit

Permalink
Merge pull request #55 from lindsay-stevens/url-encoding
Browse files Browse the repository at this point in the history
url-encode parameters URL paths and the fallback_id header
  • Loading branch information
lognaturel authored Jul 13, 2023
2 parents babf03b + ffc4ae7 commit b097da1
Show file tree
Hide file tree
Showing 14 changed files with 232 additions and 44 deletions.
8 changes: 6 additions & 2 deletions pyodk/_endpoints/comments.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ def list(

response = self.session.response_or_error(
method="GET",
url=self.urls.list.format(project_id=pid, form_id=fid, instance_id=iid),
url=self.session.urlformat(
self.urls.list, project_id=pid, form_id=fid, instance_id=iid
),
logger=log,
)
data = response.json()
Expand Down Expand Up @@ -105,7 +107,9 @@ def post(

response = self.session.response_or_error(
method="POST",
url=self.urls.post.format(project_id=pid, form_id=fid, instance_id=iid),
url=self.session.urlformat(
self.urls.post, project_id=pid, form_id=fid, instance_id=iid
),
logger=log,
json=json,
)
Expand Down
4 changes: 2 additions & 2 deletions pyodk/_endpoints/form_assignments.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ def assign(

response = self.session.response_or_error(
method="POST",
url=self.urls.post.format(
project_id=pid, form_id=fid, role_id=rid, user_id=uid
url=self.session.urlformat(
self.urls.post, project_id=pid, form_id=fid, role_id=rid, user_id=uid
),
logger=log,
)
Expand Down
4 changes: 3 additions & 1 deletion pyodk/_endpoints/form_draft_attachments.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ def upload(
with open(file_path, "rb") as fd:
response = self.session.response_or_error(
method="POST",
url=self.urls.post.format(project_id=pid, form_id=fid, fname=file_name),
url=self.session.urlformat(
self.urls.post, project_id=pid, form_id=fid, fname=file_name
),
logger=log,
data=fd,
)
Expand Down
8 changes: 5 additions & 3 deletions pyodk/_endpoints/form_drafts.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def create(
)
headers = {
"Content-Type": content_type,
"X-XlsForm-FormId-Fallback": file_path.stem,
"X-XlsForm-FormId-Fallback": self.session.urlquote(file_path.stem),
}
except PyODKError as err:
log.error(err, exc_info=True)
Expand All @@ -85,7 +85,7 @@ def create(
with open(file_path, "rb") if file_path is not None else nullcontext() as fd:
response = self.session.response_or_error(
method="POST",
url=self.urls.post.format(project_id=pid, form_id=fid),
url=self.session.urlformat(self.urls.post, project_id=pid, form_id=fid),
logger=log,
headers=headers,
params=params,
Expand Down Expand Up @@ -121,7 +121,9 @@ def publish(

response = self.session.response_or_error(
method="POST",
url=self.urls.post_publish.format(project_id=pid, form_id=fid),
url=self.session.urlformat(
self.urls.post_publish, project_id=pid, form_id=fid
),
logger=log,
params=params,
)
Expand Down
16 changes: 8 additions & 8 deletions pyodk/_endpoints/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def list(self, project_id: Optional[int] = None) -> List[Form]:
else:
response = self.session.response_or_error(
method="GET",
url=self.urls.list.format(project_id=pid),
url=self.session.urlformat(self.urls.list, project_id=pid),
logger=log,
)
data = response.json()
Expand Down Expand Up @@ -113,7 +113,7 @@ def get(
else:
response = self.session.response_or_error(
method="GET",
url=self.urls.get.format(project_id=pid, form_id=fid),
url=self.session.urlformat(self.urls.get, project_id=pid, form_id=fid),
logger=log,
)
data = response.json()
Expand All @@ -137,8 +137,8 @@ def update(
* form attachments only
* form attachments with `version_updater`
If a definition is provided, the new version name must be specified in the definition.
If no definition is provided, a default version will be set using
If a definition is provided, the new version name must be specified in the
definition. If no definition is provided, a default version will be set using
the current datetime is ISO format.
The default datetime version can be overridden by providing a `version_updater`
Expand All @@ -150,12 +150,12 @@ def update(
:param form_id: The xmlFormId of the Form being referenced.
:param project_id: The id of the project this form belongs to.
:param definition: The path to a form definition file to upload. The form definition
must include an updated version string.
:param definition: The path to a form definition file to upload. The form
definition must include an updated version string.
:param attachments: The paths of the form attachment file(s) to upload.
:param version_updater: A function that accepts a version name string and returns
a version name string, which is used for the new form version. Not allowed if a form
definition is specified.
a version name string, which is used for the new form version. Not allowed if a
form definition is specified.
"""
if definition is None and attachments is None:
raise PyODKError("Must specify a form definition and/or attachments.")
Expand Down
4 changes: 2 additions & 2 deletions pyodk/_endpoints/project_app_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def list(

response = self.session.response_or_error(
method="GET",
url=self.urls.list.format(project_id=pid),
url=self.session.urlformat(self.urls.list, project_id=pid),
logger=log,
)
data = response.json()
Expand Down Expand Up @@ -92,7 +92,7 @@ def create(

response = self.session.response_or_error(
method="POST",
url=self.urls.post.format(project_id=pid),
url=self.session.urlformat(self.urls.post, project_id=pid),
logger=log,
json=json,
)
Expand Down
2 changes: 1 addition & 1 deletion pyodk/_endpoints/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def get(self, project_id: Optional[int] = None) -> Project:
else:
response = self.session.response_or_error(
method="GET",
url=self.urls.get.format(project_id=pid),
url=self.session.urlformat(self.urls.get, project_id=pid),
logger=log,
)
data = response.json()
Expand Down
64 changes: 41 additions & 23 deletions pyodk/_endpoints/submissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def list(

response = self.session.response_or_error(
method="GET",
url=self.urls.list.format(project_id=pid, form_id=fid),
url=self.session.urlformat(self.urls.list, project_id=pid, form_id=fid),
logger=log,
)
data = response.json()
Expand Down Expand Up @@ -119,7 +119,9 @@ def get(

response = self.session.response_or_error(
method="GET",
url=self.urls.get.format(project_id=pid, form_id=fid, instance_id=iid),
url=self.session.urlformat(
self.urls.get, project_id=pid, form_id=fid, instance_id=iid
),
logger=log,
)
data = response.json()
Expand Down Expand Up @@ -180,7 +182,9 @@ def get_table(

response = self.session.response_or_error(
method="GET",
url=self.urls.get_table.format(project_id=pid, form_id=fid, table_name=table),
url=self.session.urlformat(
self.urls.get_table, project_id=pid, form_id=fid, table_name=table
),
logger=log,
params=params,
)
Expand All @@ -192,6 +196,7 @@ def create(
form_id: Optional[str] = None,
project_id: Optional[int] = None,
device_id: Optional[str] = None,
encoding: str = "utf-8",
) -> Submission:
"""
Create a Submission.
Expand All @@ -212,6 +217,7 @@ def create(
:param form_id: The xmlFormId of the Form being referenced.
:param project_id: The id of the project this form belongs to.
:param device_id: An optional deviceID associated with the submission.
:param encoding: The encoding of the submission XML, default "utf-8".
"""
try:
pid = pv.validate_project_id(project_id, self.default_project_id)
Expand All @@ -225,11 +231,11 @@ def create(

response = self.session.response_or_error(
method="POST",
url=self.urls.post.format(project_id=pid, form_id=fid),
url=self.session.urlformat(self.urls.post, project_id=pid, form_id=fid),
logger=log,
headers={"Content-Type": "application/xml"},
params=params,
data=xml,
data=xml.encode(encoding=encoding),
)
data = response.json()
return Submission(**data)
Expand All @@ -240,27 +246,16 @@ def _put(
xml: str,
form_id: Optional[str] = None,
project_id: Optional[int] = None,
encoding: str = "utf-8",
) -> Submission:
"""
Update Submission data.
Example submission XML structure:
```
<data id="my_form" version="v1">
<meta>
<deprecatedID>uuid:85cb9aff-005e-4edd-9739-dc9c1a829c44</deprecatedID>
<instanceID>uuid:315c2f74-c8fc-4606-ae3f-22f8983e441e</instanceID>
</meta>
<name>Alice</name>
<age>36</age>
</data>
```
:param instance_id: The instanceId of the Submission being referenced.
:param xml: The submission XML.
:param form_id: The xmlFormId of the Form being referenced.
:param project_id: The id of the project this form belongs to.
:param encoding: The encoding of the submission XML, default "utf-8".
"""
try:
pid = pv.validate_project_id(project_id, self.default_project_id)
Expand All @@ -272,10 +267,12 @@ def _put(

response = self.session.response_or_error(
method="PUT",
url=self.urls.put.format(project_id=pid, form_id=fid, instance_id=iid),
url=self.session.urlformat(
self.urls.put, project_id=pid, form_id=fid, instance_id=iid
),
logger=log,
headers={"Content-Type": "application/xml"},
data=xml,
data=xml.encode(encoding=encoding),
)
data = response.json()
return Submission(**data)
Expand Down Expand Up @@ -308,7 +305,9 @@ def _patch(

response = self.session.response_or_error(
method="PATCH",
url=self.urls.patch.format(project_id=pid, form_id=fid, instance_id=iid),
url=self.session.urlformat(
self.urls.patch, project_id=pid, form_id=fid, instance_id=iid
),
logger=log,
json=json,
)
Expand All @@ -322,18 +321,37 @@ def edit(
form_id: Optional[str] = None,
project_id: Optional[int] = None,
comment: Optional[str] = None,
encoding: str = "utf-8",
) -> None:
"""
Edit a submission and optionally comment on it.
:param instance_id: The instanceId of the Submission being referenced.
Example edited submission XML structure:
```
<data id="my_form" version="v1">
<meta>
<deprecatedID>uuid:85cb9aff-005e-4edd-9739-dc9c1a829c44</deprecatedID>
<instanceID>uuid:315c2f74-c8fc-4606-ae3f-22f8983e441e</instanceID>
</meta>
<name>Alice</name>
<age>36</age>
</data>
```
:param instance_id: The instanceId of the Submission being referenced. The
`instance_id` each Submission is first submitted with will always represent
that Submission as a whole. Each version of the Submission, though, has its own
`instance_id`. So `instance_id` will not necessarily match the values in
the XML elements named `instanceID` and `deprecatedID`.
:param xml: The submission XML.
:param form_id: The xmlFormId of the Form being referenced.
:param project_id: The id of the project this form belongs to.
:param comment: The text of the comment.
:param encoding: The encoding of the submission XML, default "utf-8".
"""
fp_ids = {"form_id": form_id, "project_id": project_id}
self._put(instance_id=instance_id, xml=xml, **fp_ids)
self._put(instance_id=instance_id, xml=xml, encoding=encoding, **fp_ids)
if comment is not None:
self.add_comment(instance_id=instance_id, comment=comment, **fp_ids)

Expand Down
24 changes: 23 additions & 1 deletion pyodk/_utils/session.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from logging import Logger
from urllib.parse import urljoin
from string import Formatter
from typing import Any
from urllib.parse import quote_plus, urljoin

from requests import PreparedRequest, Response
from requests import Session as RequestsSession
Expand All @@ -12,6 +14,18 @@
from pyodk.errors import PyODKError


class URLFormatter(Formatter):
"""
Makes a valid URL by sending each format input field through urllib.parse.quote_plus.
"""

def format_field(self, value: Any, format_spec: str) -> Any:
return format(quote_plus(str(value)), format_spec)


_URL_FORMATTER = URLFormatter()


class Adapter(HTTPAdapter):
def __init__(self, *args, **kwargs):
if "timeout" in kwargs:
Expand Down Expand Up @@ -100,6 +114,14 @@ def base_url_validate(base_url: str, api_version: str):
def urljoin(self, url: str) -> str:
return urljoin(self.base_url, url.lstrip("/"))

@staticmethod
def urlformat(url: str, *args, **kwargs) -> str:
return _URL_FORMATTER.format(url, *args, **kwargs)

@staticmethod
def urlquote(url: str) -> str:
return _URL_FORMATTER.format_field(url, format_spec="")

def request(self, method, url, *args, **kwargs):
return super().request(method, self.urljoin(url), *args, **kwargs)

Expand Down
Loading

0 comments on commit b097da1

Please sign in to comment.