Skip to content

Instantly share code, notes, and snippets.

@matthew-white
Created May 8, 2023 22:25
Show Gist options
  • Save matthew-white/3ece8de162c2ef5d974d5d2f6ed4ff23 to your computer and use it in GitHub Desktop.
Save matthew-white/3ece8de162c2ef5d974d5d2f6ed4ff23 to your computer and use it in GitHub Desktop.
[FOR DISCUSSION ONLY] Untested CLI script using pyODK
import argparse
import json
import re
import sys
import urllib.parse
from mimetypes import guess_type
from pathlib import Path
from pyodk.client import Client
parser = argparse.ArgumentParser()
parser.add_argument('url')
parser.add_argument('--config')
parser.add_argument('--cache')
parser.add_argument('-X', '--request', default='get')
parser.add_argument('--json', nargs='?', const=True)
parser.add_argument('--xml', nargs='?', const=True)
parser.add_argument('-f', '--file')
parser.add_argument('-H', '--header', nargs=2, action='append')
parser.add_argument('-e', '--extended', action='store_true')
parser.add_argument('-i', '--include', action='store_true')
parser.add_argument('-o', '--output', nargs='?', const=True)
args = parser.parse_args()
# Use --config and --cache to create the client.
client = Client(config_path=args.config, cache_path=args.cache)
# --request
method = args.request.lower()
if method not in ['get', 'post', 'put', 'patch', 'delete']:
raise Exception('--request: invalid method')
# Request data: --json, --xml, and --file
data = None
headers = {}
if args.json is not None and args.xml is not None:
raise Exception('--json and --xml cannot both be specified')
if args.json is not None:
data = args.json
headers['content-type'] = 'application/json'
elif args.xml is not None:
data = args.xml
headers['content-type'] = 'application/xml'
headers['x-openrosa-version'] = '1.0'
if args.file is not None:
if isinstance(args.json, str):
raise Exception('cannot pass a string to --json and specify --file')
if isinstance(args.xml, str):
raise Exception('cannot pass a string to --xml and specify --file')
# TODO. Do we need to distinguish between ASCII and binary files? request()
# seems to want bytes, never a string.
with open(args.file, 'rb') as f: data = f.read()
if data is not None and method in ['get', 'delete']:
raise Exception(f'data not allowed for {method.upper()} request')
if data is True: data = sys.stdin.read()
# XLSForms
if args.file is not None:
file_path = Path(args.file)
if file_path.suffix.lower() in ['.xlsx', '.xls']:
headers['content-type'] = guess_type(args.file)
headers['x-xlsform-formid-fallback'] = file_path.stem
# --header and --extended
if args.extended: headers['x-extended-metadata'] = 'true'
# Headers specified using --header take precedence over other headers.
if args.header is not None:
# We need to call lower() to ensure that an existing header is overwritten.
for name, value in args.header: headers[name.lower()] = value
# Encode Central custom headers.
for name in ['x-xlsform-formid-fallback', 'x-action-notes']:
if name in headers: headers[name] = urllib.parse.quote(headers[name])
# Send the request.
response = client.session.request(method, args.url, data=data, headers=headers)
# --include
# If --output was also specified, the status and headers are still printed, not
# written to file.
if args.include:
print(f'{response.status_code} {response.reason}')
for name, value in response.headers.items(): print(f'{name}: {value}')
if args.output is None: print('')
# Print the response data or, if --output was specified, save it to disk.
if args.output is None:
if re.match(r'^attachment(;|$)', response.headers.get('Content-Disposition', '')):
print('The response is an attachment. Use --output to save it to disk.')
elif re.match(r'^application/json(;|$)', response.headers['Content-Type']):
print(json.dumps(response.json(), indent=4))
# Probably XML?
else:
print(response.content.decode('utf-8'))
else:
# Determine the file path at which to write the response data (output_path).
if args.output is not True:
output_path = args.output
else:
if 'Content-Disposition' not in response.headers:
print('The response did not return a Content-Disposition header. You must specify a filename to --output.')
sys.exit(0)
content_disposition = response.headers['Content-Disposition']
if re.match(r'^attachment(;|$)', content_disposition) is None:
print('The response did not return an attachment Content-Disposition header. You must specify a filename to --output.')
sys.exit(0)
match = re.match(r"; filename\*=UTF-8''([^; ]+)", content_disposition)
if match is None:
raise Exception('unexpected Content-Disposition header')
output_path = urllib.parse.unquote(match.group(1))
with open(output_path, 'wb') as f: f.write(response.content)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment