0001-migrate-to-python3-39430.patch
debian/control | ||
---|---|---|
2 | 2 |
Section: python |
3 | 3 |
Priority: optional |
4 | 4 |
Maintainer: Benjamin Dauvergne <bdauvergne@entrouvert.com> |
5 |
Build-Depends: python-setuptools (>= 0.6b3), python-all (>= 2.6), debhelper (>= 9), dh-python |
|
6 |
Standards-Version: 3.9.1 |
|
7 |
X-Python-Version: >= 2.7 |
|
8 |
Homepage: http://dev.entrouvert.org/projects/publik-bi/ |
|
5 |
Build-Depends: python3-setuptools, python3-all, debhelper (>= 9), dh-python |
|
6 |
Standards-Version: 3.9.6 |
|
7 |
Homepage: http://dev.entrouvert.org/projects/wcs-olap/ |
|
9 | 8 | |
10 | 9 |
Package: wcs-olap |
11 | 10 |
Architecture: all |
12 |
Depends: ${python:Depends} |
|
13 |
XB-Python-Version: ${python:Versions} |
|
11 |
Depends: ${python3:Depends} |
|
14 | 12 |
Description: Export w.c.s. datas into a snowflake schema built on PostgreSQL |
debian/pydist-overrides | ||
---|---|---|
1 |
isodate python-isodate |
|
2 |
psycopg2 python-psycopg2 |
|
3 |
cached_property python-cached-property |
|
1 |
isodate python3-isodate |
|
2 |
psycopg2 python3-psycopg2 |
|
3 |
cached_property python3-cached-property |
debian/rules | ||
---|---|---|
1 | 1 |
#!/usr/bin/make -f |
2 | 2 | |
3 |
export PYBUILD_NAME=wcs-olap |
|
4 | ||
3 | 5 |
%: |
4 |
dh $@ --with python2
|
|
6 |
dh $@ --with python3 --buildsystem=pybuild
|
|
5 | 7 | |
6 | 8 |
setup.py | ||
---|---|---|
9 | 9 | |
10 | 10 |
class eo_sdist(sdist): |
11 | 11 |
def run(self): |
12 |
print "creating VERSION file" |
|
13 | 12 |
if os.path.exists('VERSION'): |
14 | 13 |
os.remove('VERSION') |
15 | 14 |
version = get_version() |
16 |
version_file = open('VERSION', 'w') |
|
17 |
version_file.write(version) |
|
18 |
version_file.close() |
|
15 |
with open('VERSION', 'w') as fd: |
|
16 |
fd.write(version) |
|
19 | 17 |
sdist.run(self) |
20 |
print "removing VERSION file" |
|
21 | 18 |
if os.path.exists('VERSION'): |
22 | 19 |
os.remove('VERSION') |
23 | 20 | |
24 | 21 | |
25 | 22 |
def get_version(): |
26 | 23 |
'''Use the VERSION, if absent generates a version with git describe, if not |
27 |
tag exists, take 0.0.0- and add the length of the commit log.
|
|
24 |
tag exists, take 0.0- and add the length of the commit log. |
|
28 | 25 |
''' |
29 | 26 |
if os.path.exists('VERSION'): |
30 | 27 |
with open('VERSION', 'r') as v: |
31 | 28 |
return v.read() |
32 | 29 |
if os.path.exists('.git'): |
33 |
p = subprocess.Popen(['git', 'describe', '--dirty', '--match=v*'], stdout=subprocess.PIPE, |
|
34 |
stderr=subprocess.PIPE) |
|
30 |
p = subprocess.Popen( |
|
31 |
['git', 'describe', '--dirty=.dirty', '--match=v*'], |
|
32 |
stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
|
35 | 33 |
result = p.communicate()[0] |
36 | 34 |
if p.returncode == 0: |
37 |
result = result.split()[0][1:] |
|
35 |
result = result.decode('ascii').strip()[1:] # strip spaces/newlines and initial v |
|
36 |
if '-' in result: # not a tagged version |
|
37 |
real_number, commit_count, commit_hash = result.split('-', 2) |
|
38 |
version = '%s.post%s+%s' % (real_number, commit_count, commit_hash) |
|
39 |
else: |
|
40 |
version = result |
|
41 |
return version |
|
38 | 42 |
else: |
39 |
result = '0.0.0-%s' % len(subprocess.check_output( |
|
40 |
['git', 'rev-list', 'HEAD']).splitlines()) |
|
41 |
return result.replace('-', '.').replace('.g', '+g') |
|
42 |
return '0.0.0' |
|
43 |
return '0.0.post%s' % len(subprocess.check_output(['git', 'rev-list', 'HEAD']).splitlines()) |
|
44 |
return '0.0' |
|
43 | 45 | |
44 | 46 | |
45 | 47 |
setup(name="wcs-olap", |
... | ... | |
54 | 56 |
maintainer_email="bdauvergne@entrouvert.com", |
55 | 57 |
packages=find_packages(), |
56 | 58 |
include_package_data=True, |
57 |
install_requires=['requests', 'psycopg2', 'isodate', 'six', 'cached-property'], |
|
59 |
install_requires=[ |
|
60 |
'requests', |
|
61 |
'psycopg2', |
|
62 |
'isodate', |
|
63 |
'six', |
|
64 |
'cached-property' |
|
65 |
], |
|
58 | 66 |
entry_points={ |
59 | 67 |
'console_scripts': ['wcs-olap=wcs_olap.cmd:main'], |
60 | 68 |
}, |
tests/test_wcs.py | ||
---|---|---|
1 |
from __future__ import unicode_literals |
|
2 | ||
3 | 1 |
import json |
4 | ||
5 | 2 |
import pytest |
3 |
import pathlib |
|
6 | 4 | |
7 | 5 |
import requests |
8 |
import pathlib2 |
|
9 |
import mock |
|
6 |
import httmock |
|
10 | 7 | |
11 | 8 |
import utils |
12 | 9 | |
... | ... | |
90 | 87 | |
91 | 88 |
# verify JSON schema |
92 | 89 |
with (olap_cmd.model_dir / 'olap.model').open() as fd, \ |
93 |
(pathlib2.Path(__file__).parent / 'olap.model').open() as fd2:
|
|
94 |
json_schema = json.load(fd)
|
|
95 |
expected_json_schema = json.load(fd2)
|
|
96 |
expected_json_schema['pg_dsn'] = postgres_db.dsn
|
|
97 |
assert json_schema == expected_json_schema
|
|
90 |
(pathlib.Path(__file__).parent / 'olap.model').open() as fd2: |
|
91 |
json_schema = json.load(fd) |
|
92 |
expected_json_schema = json.load(fd2) |
|
93 |
expected_json_schema['pg_dsn'] = postgres_db.dsn |
|
94 |
assert json_schema == expected_json_schema |
|
98 | 95 | |
99 | 96 | |
100 | 97 |
def test_requests_exception(wcs, postgres_db, tmpdir, olap_cmd, caplog): |
101 |
with mock.patch('requests.get', side_effect=requests.RequestException('wat!')): |
|
98 |
@httmock.urlmatch() |
|
99 |
def requests_raise(url, request): |
|
100 |
raise requests.RequestException('wat!') |
|
101 | ||
102 |
with httmock.HTTMock(requests_raise): |
|
102 | 103 |
with pytest.raises(SystemExit): |
103 | 104 |
olap_cmd(no_log_errors=False) |
104 | 105 |
assert 'wat!' in caplog.text |
105 | 106 | |
106 | 107 | |
107 | 108 |
def test_requests_not_ok(wcs, postgres_db, tmpdir, olap_cmd, caplog): |
108 |
with mock.patch('requests.get') as mocked_get: |
|
109 |
mocked_get.return_value.ok = False |
|
110 |
mocked_get.return_value.status_code = 401 |
|
111 |
mocked_get.return_value.text = '{"err": 1, "err_desc": "invalid signature"}' |
|
109 |
@httmock.urlmatch() |
|
110 |
def return_401(url, request): |
|
111 |
return {'status_code': 401, 'content': {"err": 1, "err_desc": "invalid signature"}} |
|
112 | ||
113 |
with httmock.HTTMock(return_401): |
|
112 | 114 |
with pytest.raises(SystemExit): |
113 | 115 |
olap_cmd(no_log_errors=False) |
114 | 116 |
assert 'invalid signature' in caplog.text |
115 | 117 | |
116 | 118 | |
117 | 119 |
def test_requests_not_json(wcs, postgres_db, tmpdir, olap_cmd, caplog): |
118 |
with mock.patch('requests.get') as mocked_get: |
|
119 |
mocked_get.return_value.ok = True |
|
120 |
mocked_get.return_value.json.side_effect = ValueError('invalid JSON') |
|
120 |
@httmock.urlmatch() |
|
121 |
def return_invalid_json(url, request): |
|
122 |
return 'x' |
|
123 | ||
124 |
with httmock.HTTMock(return_invalid_json): |
|
121 | 125 |
with pytest.raises(SystemExit): |
122 | 126 |
olap_cmd(no_log_errors=False) |
123 | 127 |
assert 'Invalid JSON content' in caplog.text |
tox.ini | ||
---|---|---|
5 | 5 | |
6 | 6 |
[tox] |
7 | 7 |
toxworkdir = {env:TMPDIR:/tmp}/tox-{env:USER}/wcs-olap/{env:BRANCH_NAME:} |
8 |
envlist = py2-coverage
|
|
8 |
envlist = py3-coverage
|
|
9 | 9 | |
10 | 10 |
[testenv] |
11 | 11 |
usedevelop = true |
... | ... | |
17 | 17 |
pytest |
18 | 18 |
pytest-cov |
19 | 19 |
pytest-random |
20 |
quixote<3.0
|
|
20 |
quixote>=3
|
|
21 | 21 |
psycopg2-binary |
22 | 22 |
vobject |
23 | 23 |
pyproj |
24 | 24 |
django-ratelimit<3 |
25 | 25 |
gadjo |
26 |
mock |
|
26 |
httmock
|
|
27 | 27 |
django>=1.11,<1.12 |
28 | 28 |
commands = |
29 |
./get_wcs.sh |
|
29 |
#./get_wcs.sh
|
|
30 | 30 |
py.test {env:COVERAGE:} {posargs:--random-group tests} |
31 | ||
32 |
[pytest] |
|
33 |
junit_family=xunit2 |
wcs_olap/cmd.py | ||
---|---|---|
1 |
import sys |
|
2 | 1 |
import argparse |
3 |
import ConfigParser
|
|
4 |
import os
|
|
2 |
import configparser
|
|
3 |
import locale
|
|
5 | 4 |
import logging |
6 | 5 |
import logging.config |
7 |
from . import wcs_api |
|
8 |
from .feeder import WcsOlapFeeder |
|
9 |
import locale |
|
6 |
import os |
|
7 |
import sys |
|
10 | 8 | |
11 |
from . import tb
|
|
9 |
from . import wcs_api, feeder
|
|
12 | 10 | |
13 | 11 | |
14 | 12 |
def main(): |
... | ... | |
16 | 14 |
main2() |
17 | 15 |
except SystemExit: |
18 | 16 |
raise |
19 |
except: |
|
20 |
tb.print_tb() |
|
21 |
raise SystemExit(1) |
|
22 | 17 | |
23 | 18 | |
24 | 19 |
def get_config(path=None): |
25 |
config = ConfigParser.ConfigParser()
|
|
20 |
config = configparser.ConfigParser()
|
|
26 | 21 |
global_config_path = '/etc/wcs-olap/config.ini' |
27 | 22 |
user_config_path = os.path.expanduser('~/.wcs-olap.ini') |
28 | 23 |
if not path: |
... | ... | |
60 | 55 |
fake = args.fake |
61 | 56 |
config = get_config(path=args.config_path) |
62 | 57 |
# list all known urls |
63 |
urls = [url for url in config.sections() if url.startswith('http://') or
|
|
64 |
url.startswith('https://')] |
|
58 |
urls = [url for url in config.sections() if url.startswith('http://') |
|
59 |
or url.startswith('https://')]
|
|
65 | 60 |
defaults = {} |
66 | 61 |
if not args.all: |
67 | 62 |
try: |
68 | 63 |
url = args.url or urls[0] |
69 | 64 |
except IndexError: |
70 |
print 'no url found'
|
|
65 |
print('no url found')
|
|
71 | 66 |
raise SystemExit(1) |
72 | 67 |
urls = [url] |
73 | 68 |
if config.has_section(args.url): |
... | ... | |
97 | 92 |
pg_dsn = defaults['pg_dsn'] |
98 | 93 |
slugs = defaults.get('slugs', '').strip().split() or getattr(args, 'slug', []) |
99 | 94 |
batch_size = int(defaults.get('batch_size', 500)) |
100 |
except KeyError, e:
|
|
95 |
except KeyError as e:
|
|
101 | 96 |
failure = True |
102 | 97 |
logger.error('configuration incomplete for %s: %s', url, e) |
103 | 98 |
else: |
104 | 99 |
try: |
105 |
api = wcs_api.WcsApi(url=url, orig=orig, key=key, slugs=slugs,
|
|
106 |
verify=defaults.get('verify', 'True') == 'True',
|
|
107 |
batch_size=batch_size)
|
|
100 |
api = wcs_api.WcsApi(url=url, orig=orig, key=key, |
|
101 |
batch_size=batch_size,
|
|
102 |
verify=(defaults.get('verify', 'True') == 'True'))
|
|
108 | 103 |
logger.info('starting synchronizing w.c.s. at %r with PostgreSQL at %s', url, |
109 | 104 |
pg_dsn) |
110 |
feeder = WcsOlapFeeder(api=api, schema=schema, pg_dsn=pg_dsn, logger=logger, |
|
111 |
config=defaults, do_feed=feed, fake=fake) |
|
112 |
feeder.feed() |
|
105 |
olap_feeder = feeder.WcsOlapFeeder( |
|
106 |
api=api, schema=schema, pg_dsn=pg_dsn, logger=logger, |
|
107 |
config=defaults, do_feed=feed, fake=fake, slugs=slugs) |
|
108 |
olap_feeder.feed() |
|
113 | 109 |
logger.info('finished') |
114 | 110 |
feed_result = False |
115 |
except: |
|
111 |
except Exception:
|
|
116 | 112 |
if args.no_log_errors: |
117 | 113 |
raise |
118 | 114 |
feed_result = True |
wcs_olap/feeder.py | ||
---|---|---|
2 | 2 | |
3 | 3 |
from __future__ import unicode_literals |
4 | 4 | |
5 |
from collections import OrderedDict, Counter
|
|
5 |
from collections import OrderedDict |
|
6 | 6 |
import datetime |
7 | 7 |
import six |
8 | 8 |
import copy |
... | ... | |
10 | 10 |
import os |
11 | 11 |
import json |
12 | 12 |
import hashlib |
13 |
from utils import Whatever |
|
13 |
from .utils import Whatever
|
|
14 | 14 |
import psycopg2 |
15 | 15 | |
16 | 16 |
from cached_property import cached_property |
... | ... | |
78 | 78 |
status_to_id = dict((c[1], c[0]) for c in channels) |
79 | 79 |
id_to_status = dict((c[0], c[1]) for c in channels) |
80 | 80 | |
81 |
def __init__(self, api, pg_dsn, schema, logger=None, config=None, do_feed=True, fake=False): |
|
81 |
def __init__(self, api, pg_dsn, schema, logger=None, config=None, do_feed=True, fake=False, slugs=None):
|
|
82 | 82 |
self.api = api |
83 |
self.slugs = slugs |
|
83 | 84 |
self.fake = fake |
84 | 85 |
self.logger = logger or Whatever() |
85 | 86 |
self.schema = schema |
... | ... | |
291 | 292 | |
292 | 293 |
@cached_property |
293 | 294 |
def formdefs(self): |
294 |
return self.api.formdefs
|
|
295 |
return [formdef for formdef in self.api.formdefs if not self.slugs or formdef.slug in self.slugs]
|
|
295 | 296 | |
296 | 297 |
@cached_property |
297 | 298 |
def roles(self): |
... | ... | |
441 | 442 |
if isinstance(o, six.string_types): |
442 | 443 |
return o.format(**ctx) |
443 | 444 |
elif isinstance(o, dict): |
444 |
return dict((k, helper(v)) for k, v in o.iteritems())
|
|
445 |
return dict((k, helper(v)) for k, v in o.items()) |
|
445 | 446 |
elif isinstance(o, list): |
446 | 447 |
return [helper(v) for v in o] |
447 | 448 |
elif isinstance(o, (bool, int, float)): |
... | ... | |
466 | 467 | |
467 | 468 |
# categories |
468 | 469 |
tmp_cat_map = self.create_labeled_table( |
469 |
'category', enumerate(c.name for c in self.categories), comment='catégorie')
|
|
470 |
self.categories_mapping = dict((c.id, tmp_cat_map[c.name]) for c in self.categories)
|
|
470 |
'category', enumerate(c.title for c in self.categories), comment='catégorie')
|
|
471 |
self.categories_mapping = dict((c.slug, tmp_cat_map[c.title]) for c in self.categories)
|
|
471 | 472 | |
472 | 473 |
self.create_labeled_table('hour', zip(range(0, 24), map(str, range(0, 24))), |
473 | 474 |
comment='heures') |
... | ... | |
506 | 507 |
'geolocation_base': 'position géographique', |
507 | 508 |
} |
508 | 509 |
self.create_table('{generic_formdata_table}', self.columns) |
509 |
for at, comment in self.comments.iteritems():
|
|
510 |
for at, comment in self.comments.items(): |
|
510 | 511 |
self.ex('COMMENT ON COLUMN {generic_formdata_table}.%s IS %%s' % at, vars=(comment,)) |
511 | 512 |
self.ex('COMMENT ON TABLE {generic_formdata_table} IS %s', vars=('tous les formulaires',)) |
512 | 513 |
# evolutions |
... | ... | |
663 | 664 |
} |
664 | 665 | |
665 | 666 |
# add function fields |
666 |
for function, name in self.formdef.schema.workflow.functions.iteritems():
|
|
667 |
for function, name in self.formdef.schema.workflow.functions.items(): |
|
667 | 668 |
at = 'function_%s' % slugify(function) |
668 | 669 |
columns[at] = { |
669 | 670 |
'sql_col_name': at, |
... | ... | |
746 | 747 |
values = [] |
747 | 748 |
generic_evolution_values = [] |
748 | 749 |
evolution_values = [] |
749 |
for data in self.formdef.datas:
|
|
750 |
for data in self.formdef.formdatas.anonymized.full:
|
|
750 | 751 |
json_data = {} |
751 | 752 | |
752 | 753 |
# ignore formdata without status |
... | ... | |
818 | 819 |
v = '(%.6f, %.6f)' % (v.get('lon'), v.get('lat')) |
819 | 820 |
row['geolocation_%s' % geolocation] = v |
820 | 821 |
# add function fields value |
821 |
for function, name in self.formdef.schema.workflow.functions.iteritems():
|
|
822 |
for function, name in self.formdef.schema.workflow.functions.items(): |
|
822 | 823 |
try: |
823 | 824 |
v = data.functions[function] |
824 | 825 |
except KeyError: |
... | ... | |
949 | 950 |
}) |
950 | 951 | |
951 | 952 |
# add dimension for function |
952 |
for function, name in self.formdef.schema.workflow.functions.iteritems():
|
|
953 |
for function, name in self.formdef.schema.workflow.functions.items(): |
|
953 | 954 |
at = 'function_%s' % slugify(function) |
954 | 955 |
cube['joins'].append({ |
955 | 956 |
'name': at, |
wcs_olap/signature.py | ||
---|---|---|
1 |
import urllib.parse as urlparse |
|
1 | 2 |
import datetime |
2 | 3 |
import base64 |
3 | 4 |
import hmac |
4 | 5 |
import hashlib |
5 |
import urllib |
|
6 | 6 |
import random |
7 |
import urlparse |
|
8 | 7 | |
9 | 8 |
'''Simple signature scheme for query strings''' |
9 |
# from http://repos.entrouvert.org/portail-citoyen.git/tree/portail_citoyen/apps/data_source_plugin/signature.py |
|
10 | 10 | |
11 | 11 | |
12 | 12 |
def sign_url(url, key, algo='sha256', timestamp=None, nonce=None): |
... | ... | |
20 | 20 |
timestamp = datetime.datetime.utcnow() |
21 | 21 |
timestamp = timestamp.strftime('%Y-%m-%dT%H:%M:%SZ') |
22 | 22 |
if nonce is None: |
23 |
nonce = hex(random.SystemRandom().getrandbits(128))[2:-1]
|
|
23 |
nonce = hex(random.getrandbits(128))[2:]
|
|
24 | 24 |
new_query = query |
25 | 25 |
if new_query: |
26 | 26 |
new_query += '&' |
27 |
new_query += urllib.urlencode((
|
|
27 |
new_query += urlparse.urlencode((
|
|
28 | 28 |
('algo', algo), |
29 | 29 |
('timestamp', timestamp), |
30 | 30 |
('nonce', nonce))) |
31 | 31 |
signature = base64.b64encode(sign_string(new_query, key, algo=algo)) |
32 |
new_query += '&signature=' + urllib.quote(signature)
|
|
32 |
new_query += '&signature=' + urlparse.quote(signature)
|
|
33 | 33 |
return new_query |
34 | 34 | |
35 | 35 | |
36 | 36 |
def sign_string(s, key, algo='sha256', timedelta=30): |
37 | 37 |
digestmod = getattr(hashlib, algo) |
38 |
if isinstance(key, unicode):
|
|
38 |
if isinstance(key, str):
|
|
39 | 39 |
key = key.encode('utf-8') |
40 |
if isinstance(s, str): |
|
41 |
s = s.encode('utf-8') |
|
40 | 42 |
hash = hmac.HMAC(key, digestmod=digestmod, msg=s) |
41 | 43 |
return hash.digest() |
42 | 44 | |
... | ... | |
48 | 50 | |
49 | 51 |
def check_query(query, key, known_nonce=None, timedelta=30): |
50 | 52 |
parsed = urlparse.parse_qs(query) |
53 |
if not ('signature' in parsed and 'algo' in parsed |
|
54 |
and 'timestamp' in parsed and 'nonce' in parsed): |
|
55 |
return False |
|
56 |
unsigned_query, signature_content = query.split('&signature=', 1) |
|
57 |
if '&' in signature_content: |
|
58 |
return False # signature must be the last parameter |
|
51 | 59 |
signature = base64.b64decode(parsed['signature'][0]) |
52 | 60 |
algo = parsed['algo'][0] |
53 | 61 |
timestamp = parsed['timestamp'][0] |
54 | 62 |
timestamp = datetime.datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%SZ') |
55 | 63 |
nonce = parsed['nonce'] |
56 |
unsigned_query = query.split('&signature=')[0] |
|
57 | 64 |
if known_nonce is not None and known_nonce(nonce): |
58 | 65 |
return False |
59 | 66 |
if abs(datetime.datetime.utcnow() - timestamp) > datetime.timedelta(seconds=timedelta): |
... | ... | |
68 | 75 |
return False |
69 | 76 |
res = 0 |
70 | 77 |
for a, b in zip(signature, signature2): |
71 |
res |= ord(a) ^ ord(b)
|
|
78 |
res |= a ^ b
|
|
72 | 79 |
return res == 0 |
80 | ||
81 | ||
82 |
if __name__ == '__main__': |
|
83 |
key = '12345' |
|
84 |
signed_query = sign_query('NameId=_12345&orig=montpellier', key) |
|
85 |
assert check_query(signed_query, key, timedelta=0) is False |
|
86 |
assert check_query(signed_query, key) is True |
wcs_olap/tb.py | ||
---|---|---|
1 |
from StringIO import StringIO |
|
2 |
import sys |
|
3 |
import linecache |
|
4 | ||
5 | ||
6 |
def print_tb(): |
|
7 |
exc_type, exc_value, tb = sys.exc_info() |
|
8 |
if exc_value: |
|
9 |
exc_value = unicode(str(exc_value), errors='ignore') |
|
10 |
error_file = StringIO() |
|
11 | ||
12 |
limit = None |
|
13 |
if hasattr(sys, 'tracebacklimit'): |
|
14 |
limit = sys.tracebacklimit |
|
15 |
print >>error_file, "Exception:" |
|
16 |
print >>error_file, " type = '%s', value = '%s'" % (exc_type, exc_value) |
|
17 |
print >>error_file |
|
18 | ||
19 |
# format the traceback |
|
20 |
print >>error_file, 'Stack trace (most recent call first):' |
|
21 |
n = 0 |
|
22 |
while tb is not None and (limit is None or n < limit): |
|
23 |
frame = tb.tb_frame |
|
24 |
function = frame.f_code.co_name |
|
25 |
filename = frame.f_code.co_filename |
|
26 |
exclineno = frame.f_lineno |
|
27 |
locals = frame.f_locals.items() |
|
28 | ||
29 |
print >>error_file, ' File "%s", line %s, in %s' % (filename, exclineno, function) |
|
30 |
linecache.checkcache(filename) |
|
31 |
for lineno in range(exclineno - 2, exclineno + 3): |
|
32 |
line = linecache.getline(filename, lineno, frame.f_globals) |
|
33 |
if line: |
|
34 |
if lineno == exclineno: |
|
35 |
print >>error_file, '>%5s %s' % (lineno, line.rstrip()) |
|
36 |
else: |
|
37 |
print >>error_file, ' %5s %s' % (lineno, line.rstrip()) |
|
38 |
print >>error_file |
|
39 |
if locals: |
|
40 |
print >>error_file, " locals: " |
|
41 |
for key, value in locals: |
|
42 |
print >>error_file, " %s =" % key, |
|
43 |
try: |
|
44 |
repr_value = repr(value) |
|
45 |
if len(repr_value) > 10000: |
|
46 |
repr_value = repr_value[:10000] + ' [...]' |
|
47 |
print >>error_file, repr_value, |
|
48 |
except: |
|
49 |
print >>error_file, "<ERROR WHILE PRINTING VALUE>", |
|
50 |
print >>error_file |
|
51 |
print >>error_file |
|
52 |
tb = tb.tb_next |
|
53 |
n = n + 1 |
|
54 | ||
55 |
print >>sys.stderr, error_file.getvalue() |
wcs_olap/wcs_api.py | ||
---|---|---|
1 |
import six |
|
1 |
# wcs_olap |
|
2 |
# Copyright (C) 2020 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
import collections |
|
18 |
import base64 |
|
19 |
import copy |
|
20 |
import logging |
|
21 |
import datetime |
|
22 |
import contextlib |
|
23 |
import json |
|
24 | ||
2 | 25 |
import requests |
3 |
import urlparse |
|
4 |
import urllib |
|
5 | 26 |
import isodate |
6 |
import logging |
|
7 | 27 | |
28 |
import urllib.parse as urlparse |
|
8 | 29 | |
9 | 30 |
from . import signature |
10 | 31 | |
11 | 32 | |
12 |
logger = logging.getLogger(__name__) |
|
33 |
class WcsApiError(Exception): |
|
34 |
pass |
|
13 | 35 | |
14 | 36 | |
15 |
def exception_to_text(e): |
|
16 |
try: |
|
17 |
return six.text_type(e) |
|
18 |
except Exception: |
|
19 |
pass |
|
37 |
class JSONFile(object): |
|
38 |
def __init__(self, d): |
|
39 |
self.d = d |
|
20 | 40 | |
21 |
try: |
|
22 |
return six.text_type(e.decode('utf8')) |
|
23 |
except Exception: |
|
24 |
pass |
|
41 |
@property |
|
42 |
def filename(self): |
|
43 |
return self.d.get('filename', '') |
|
25 | 44 | |
26 |
try: |
|
27 |
return six.text_type(repr(e)) |
|
28 |
except Exception: |
|
29 |
pass |
|
45 |
@property |
|
46 |
def content_type(self): |
|
47 |
return self.d.get('content_type', 'application/octet-stream') |
|
30 | 48 | |
31 |
try: |
|
32 |
args = e.args |
|
33 |
try: |
|
34 |
content = six.text_type(repr(args)) if args != [] else '' |
|
35 |
except Exception: |
|
36 |
content = '<exception-while-rendering-args>' |
|
37 |
except AttributeError: |
|
38 |
content = '' |
|
39 |
return u'%s(%s)' % (e.__class__.__name__, content) |
|
49 |
@property |
|
50 |
def content(self): |
|
51 |
return base64.b64decode(self.d['content']) |
|
40 | 52 | |
41 | 53 | |
42 |
class WcsApiError(Exception): |
|
43 |
def __init__(self, message, **kwargs): |
|
44 |
super(WcsApiError, self).__init__(message) |
|
45 |
self.kwargs = kwargs |
|
46 | ||
47 |
def __str__(self): |
|
48 |
kwargs = self.kwargs.copy() |
|
49 |
if 'exception' in kwargs: |
|
50 |
kwargs['exception'] = exception_to_text(kwargs['exception']) |
|
51 |
return '%s: %s' % (self.args[0], ' '.join('%s=%s' % (key, value) for key, value in kwargs.items())) |
|
54 |
def to_dict(o): |
|
55 |
if hasattr(o, 'to_dict'): |
|
56 |
return o.to_dict() |
|
57 |
elif isinstance(o, dict): |
|
58 |
return {k: to_dict(v) for k, v in o.items()} |
|
59 |
elif isinstance(o, (list, tuple)): |
|
60 |
return [to_dict(v) for v in o] |
|
61 |
else: |
|
62 |
return o |
|
52 | 63 | |
53 | 64 | |
54 | 65 |
class BaseObject(object): |
55 | 66 |
def __init__(self, wcs_api, **kwargs): |
56 |
self.__wcs_api = wcs_api
|
|
67 |
self._wcs_api = wcs_api |
|
57 | 68 |
self.__dict__.update(**kwargs) |
58 | 69 | |
70 |
def to_dict(self): |
|
71 |
d = collections.OrderedDict() |
|
72 |
for key, value in self.__dict__.items(): |
|
73 |
if key[0] == '_': |
|
74 |
continue |
|
75 |
d[key] = to_dict(value) |
|
76 |
return d |
|
77 | ||
59 | 78 | |
60 | 79 |
class FormDataWorkflow(BaseObject): |
61 | 80 |
status = None |
... | ... | |
92 | 111 |
class FormData(BaseObject): |
93 | 112 |
geolocations = None |
94 | 113 |
evolution = None |
114 |
submission = None |
|
115 |
workflow = None |
|
116 |
roles = None |
|
117 |
with_files = False |
|
95 | 118 | |
96 |
def __init__(self, wcs_api, **kwargs): |
|
119 |
def __init__(self, wcs_api, forms, **kwargs): |
|
120 |
self.forms = forms |
|
97 | 121 |
super(FormData, self).__init__(wcs_api, **kwargs) |
98 | 122 |
self.receipt_time = isodate.parse_datetime(self.receipt_time) |
99 |
self.submission = BaseObject(wcs_api, **self.submission) |
|
100 |
self.workflow = FormDataWorkflow(wcs_api, **self.workflow) |
|
123 |
if self.submission: |
|
124 |
self.submission = BaseObject(wcs_api, **self.submission) |
|
125 |
if self.workflow: |
|
126 |
self.workflow = FormDataWorkflow(wcs_api, **self.workflow) |
|
101 | 127 |
self.evolution = [Evolution(wcs_api, **evo) for evo in self.evolution or []] |
102 | 128 |
self.functions = {} |
103 | 129 |
self.concerned_roles = [] |
104 | 130 |
self.action_roles = [] |
105 |
for function in self.roles: |
|
131 |
for function in self.roles or []:
|
|
106 | 132 |
roles = [Role(wcs_api, **r) for r in self.roles[function]] |
107 | 133 |
if function == 'concerned': |
108 | 134 |
self.concerned_roles.extend(roles) |
... | ... | |
113 | 139 |
self.functions[function] = roles[0] |
114 | 140 |
except IndexError: |
115 | 141 |
self.functions[function] = None |
116 |
del self.roles |
|
142 |
if 'roles' in self.__dict__: |
|
143 |
del self.roles |
|
117 | 144 | |
118 |
def __repr__(self): |
|
119 |
return '<{klass} {display_id!r}>'.format(klass=self.__class__.__name__, |
|
120 |
display_id=self.id) |
|
145 |
def __str__(self): |
|
146 |
return '{self.formdef} - {self.id}'.format(self=self) |
|
147 | ||
148 |
@property |
|
149 |
def full(self): |
|
150 |
if self.with_files: |
|
151 |
return self |
|
152 |
if not hasattr(self, '_full'): |
|
153 |
self._full = self.forms[self.id] |
|
154 |
return self._full |
|
155 | ||
156 |
@property |
|
157 |
def anonymized(self): |
|
158 |
return self.forms.anonymized[self.id] |
|
121 | 159 | |
122 | 160 |
@property |
123 | 161 |
def endpoint_delay(self): |
... | ... | |
140 | 178 |
else: |
141 | 179 |
return |
142 | 180 | |
181 |
def __getitem__(self, key): |
|
182 |
value = self.full.fields.get(key) |
|
183 |
# unserialize files |
|
184 |
if isinstance(value, dict) and 'content' in value: |
|
185 |
return JSONFile(value) |
|
186 |
return value |
|
187 | ||
143 | 188 | |
144 | 189 |
class Workflow(BaseObject): |
145 | 190 |
statuses = None |
... | ... | |
148 | 193 |
def __init__(self, wcs_api, **kwargs): |
149 | 194 |
super(Workflow, self).__init__(wcs_api, **kwargs) |
150 | 195 |
self.statuses = [BaseObject(wcs_api, **v) for v in (self.statuses or [])] |
151 |
if self.statuses: |
|
152 |
assert not hasattr(self.statuses[0], 'startpoint'), 'startpoint is exported by w.c.s. FIXME' |
|
153 |
for status in self.statuses: |
|
154 |
status.startpoint = False |
|
155 |
self.statuses[0].startpoint = True |
|
196 |
assert not hasattr(self.statuses[0], 'startpoint'), 'startpoint is exported by w.c.s. FIXME' |
|
197 |
for status in self.statuses: |
|
198 |
status.startpoint = False |
|
199 |
self.statuses[0].startpoint = True |
|
156 | 200 |
self.statuses_map = dict((s.id, s) for s in self.statuses) |
157 | 201 |
self.fields = [Field(wcs_api, **field) for field in (self.fields or [])] |
158 | 202 | |
... | ... | |
177 | 221 |
self.geolocations = sorted((k, v) for k, v in (self.geolocations or {}).items()) |
178 | 222 | |
179 | 223 | |
224 |
class FormDatas(object): |
|
225 |
def __init__(self, wcs_api, formdef, full=False, anonymize=False, batch=1000): |
|
226 |
self.wcs_api = wcs_api |
|
227 |
self.formdef = formdef |
|
228 |
self._full = full |
|
229 |
self.anonymize = anonymize |
|
230 |
self.batch = batch |
|
231 | ||
232 |
def __getitem__(self, slice_or_id): |
|
233 |
# get batch of forms |
|
234 |
if isinstance(slice_or_id, slice): |
|
235 |
def helper(): |
|
236 |
if slice_or_id.stop <= slice_or_id.start or slice_or_id.step: |
|
237 |
raise ValueError('invalid slice %s' % slice_or_id) |
|
238 |
offset = slice_or_id.start |
|
239 |
limit = slice_or_id.stop - slice_or_id.start |
|
240 | ||
241 |
url_parts = ['api/forms/{self.formdef.slug}/list'.format(self=self)] |
|
242 |
query = {} |
|
243 |
query['full'] = 'on' if self._full else 'off' |
|
244 |
if offset: |
|
245 |
query['offset'] = str(offset) |
|
246 |
if limit: |
|
247 |
query['limit'] = str(limit) |
|
248 |
if self.anonymize: |
|
249 |
query['anonymise'] = 'on' |
|
250 |
url_parts.append('?%s' % urlparse.urlencode(query)) |
|
251 |
for d in self.wcs_api.get_json(*url_parts): |
|
252 |
# w.c.s. had a bug where some formdata lost their draft status, skip them |
|
253 |
if not d.get('receipt_time'): |
|
254 |
continue |
|
255 |
yield FormData(wcs_api=self.wcs_api, forms=self, formdef=self.formdef, **d) |
|
256 |
return helper() |
|
257 |
# or get one form |
|
258 |
else: |
|
259 |
url_parts = ['api/forms/{formdef.slug}/{id}/'.format(formdef=self.formdef, id=slice_or_id)] |
|
260 |
if self.anonymize: |
|
261 |
url_parts.append('?anonymise=true') |
|
262 |
d = self.wcs_api.get_json(*url_parts) |
|
263 |
return FormData(wcs_api=self.wcs_api, forms=self, formdef=self.formdef, with_files=True, **d) |
|
264 | ||
265 |
@property |
|
266 |
def full(self): |
|
267 |
forms = copy.copy(self) |
|
268 |
forms._full = True |
|
269 |
return forms |
|
270 | ||
271 |
@property |
|
272 |
def anonymized(self): |
|
273 |
forms = copy.copy(self) |
|
274 |
forms.anonymize = True |
|
275 |
return forms |
|
276 | ||
277 |
def batched(self, batch): |
|
278 |
forms = copy.copy(self) |
|
279 |
forms.batch = batch |
|
280 |
return forms |
|
281 | ||
282 |
def __iter__(self): |
|
283 |
start = 0 |
|
284 |
while True: |
|
285 |
empty = True |
|
286 |
for formdef in self[start:start + self.batch]: |
|
287 |
empty = False |
|
288 |
yield formdef |
|
289 |
if empty: |
|
290 |
break |
|
291 |
start += self.batch |
|
292 | ||
293 |
def __len__(self): |
|
294 |
return len(list((o for o in self))) |
|
295 | ||
296 | ||
297 |
class CancelSubmitError(Exception): |
|
298 |
pass |
|
299 | ||
300 | ||
301 |
class FormDefSubmit(object): |
|
302 |
formdef = None |
|
303 |
data = None |
|
304 |
user_email = None |
|
305 |
user_name_id = None |
|
306 |
backoffice_submission = False |
|
307 |
submission_channel = None |
|
308 |
submission_context = None |
|
309 |
draft = False |
|
310 | ||
311 |
def __init__(self, wcs_api, formdef, **kwargs): |
|
312 |
self.wcs_api = wcs_api |
|
313 |
self.formdef = formdef |
|
314 |
self.data = {} |
|
315 |
self.__dict__.update(kwargs) |
|
316 | ||
317 |
def payload(self): |
|
318 |
d = { |
|
319 |
'data': self.data.copy(), |
|
320 |
} |
|
321 |
if self.draft: |
|
322 |
d.setdefault('meta', {})['draft'] = True |
|
323 |
if self.backoffice_submission: |
|
324 |
d.setdefault('meta', {})['backoffice-submission'] = True |
|
325 |
if self.submission_context: |
|
326 |
d['context'] = self.submission_context |
|
327 |
if self.submission_channel: |
|
328 |
d.setdefault('context', {})['channel'] = self.submission_channel |
|
329 |
if self.user_email: |
|
330 |
d.setdefault('user', {})['email'] = self.user_email |
|
331 |
if self.user_name_id: |
|
332 |
d.setdefault('user', {})['NameID'] = self.user_name_id |
|
333 |
return d |
|
334 | ||
335 |
def set(self, field, value, **kwargs): |
|
336 |
if isinstance(field, Field): |
|
337 |
varname = field.varname |
|
338 |
if not varname: |
|
339 |
raise ValueError('field has no varname, submit is impossible') |
|
340 |
else: |
|
341 |
varname = field |
|
342 |
try: |
|
343 |
field = [f for f in self.formdef.schema.fields if f.varname == varname][0] |
|
344 |
except IndexError: |
|
345 |
raise ValueError('no field for varname %s' % varname) |
|
346 | ||
347 |
if value is None or value == {} or value == []: |
|
348 |
self.data.pop(varname, None) |
|
349 |
elif hasattr(self, '_set_type_%s' % field.type): |
|
350 |
getattr(self, '_set_type_%s' % field.type)( |
|
351 |
varname=varname, |
|
352 |
field=field, |
|
353 |
value=value, **kwargs) |
|
354 |
else: |
|
355 |
self.data[varname] = value |
|
356 | ||
357 |
def _set_type_item(self, varname, field, value, **kwargs): |
|
358 |
if isinstance(value, dict): |
|
359 |
if not set(value).issuperset(set(['id', 'text'])): |
|
360 |
raise ValueError('item field value must have id and text value') |
|
361 |
# clean previous values |
|
362 |
self.data.pop(varname, None) |
|
363 |
self.data.pop(varname + '_raw', None) |
|
364 |
self.data.pop(varname + '_structured', None) |
|
365 |
if isinstance(value, dict): |
|
366 |
# structured & display values |
|
367 |
self.data[varname + '_raw'] = value['id'] |
|
368 |
self.data[varname] = value['text'] |
|
369 |
if len(value) > 2: |
|
370 |
self.data[varname + '_structured'] = value |
|
371 |
else: |
|
372 |
# raw id in varname |
|
373 |
self.data[varname] = value |
|
374 | ||
375 |
def _set_type_items(self, varname, field, value, **kwargs): |
|
376 |
if not isinstance(value, list): |
|
377 |
raise TypeError('%s is an ItemsField it needs a list as value' % varname) |
|
378 | ||
379 |
has_dict = False |
|
380 |
for choice in value: |
|
381 |
if isinstance(value, dict): |
|
382 |
if not set(value).issuperset(set(['id', 'text'])): |
|
383 |
raise ValueError('items field values must have id and text value') |
|
384 |
has_dict = True |
|
385 |
if has_dict: |
|
386 |
if not all(isinstance(choice, dict) for choice in value): |
|
387 |
raise ValueError('ItemsField value must be all structured or none') |
|
388 |
# clean previous values |
|
389 |
self.data.pop(varname, None) |
|
390 |
self.data.pop(varname + '_raw', None) |
|
391 |
self.data.pop(varname + '_structured', None) |
|
392 |
if has_dict: |
|
393 |
raw = self.data[varname + '_raw'] = [] |
|
394 |
display = self.data[varname] = [] |
|
395 |
structured = self.data[varname + '_structured'] = [] |
|
396 |
for choice in value: |
|
397 |
raw.append(choice['id']) |
|
398 |
display.append(choice['text']) |
|
399 |
structured.append(choice) |
|
400 |
else: |
|
401 |
self.data[varname] = value[:] |
|
402 | ||
403 |
def _set_type_file(self, varname, field, value, **kwargs): |
|
404 |
filename = kwargs.get('filename') |
|
405 |
content_type = kwargs.get('content_type', 'application/octet-stream') |
|
406 |
if hasattr(value, 'read'): |
|
407 |
content = base64.b64encode(value.read()).decode('ascii') |
|
408 |
elif isinstance(value, bytes): |
|
409 |
content = base64.b64encode(value).decode('ascii') |
|
410 |
elif isinstance(value, dict): |
|
411 |
if not set(value).issuperset(set(['filename', 'content'])): |
|
412 |
raise ValueError('file field needs a dict value with filename and content') |
|
413 |
content = value['content'] |
|
414 |
filename = value['filename'] |
|
415 |
content_type = value.get('content_type', content_type) |
|
416 |
if not filename: |
|
417 |
raise ValueError('missing filename') |
|
418 |
self.data[varname] = { |
|
419 |
'filename': filename, |
|
420 |
'content': content, |
|
421 |
'content_type': content_type, |
|
422 |
} |
|
423 | ||
424 |
def _set_type_date(self, varname, field, value): |
|
425 |
if isinstance(value, str): |
|
426 |
value = datetime.datetime.strptime(value, '%Y-%m-%d').date() |
|
427 |
if isinstance(value, datetime.datetime): |
|
428 |
value = value.date() |
|
429 |
if isinstance(value, datetime.date): |
|
430 |
value = value.strftime('%Y-%m-%d') |
|
431 |
self.data[varname] = value |
|
432 | ||
433 |
def _set_type_map(self, varname, field, value): |
|
434 |
if not isinstance(value, dict): |
|
435 |
raise TypeError('value must be a dict for a map field') |
|
436 |
if set(value) != set(['lat', 'lon']): |
|
437 |
raise ValueError('map field expect keys lat and lon') |
|
438 |
self.data[varname] = value |
|
439 | ||
440 |
def _set_type_bool(self, varname, field, value): |
|
441 |
if isinstance(value, str): |
|
442 |
value = value.lower().strip() in ['yes', 'true', 'on'] |
|
443 |
if not isinstance(value, bool): |
|
444 |
raise TypeError('value must be a boolean or a string true, yes, on, false, no, off') |
|
445 |
self.data[varname] = value |
|
446 | ||
447 |
def cancel(self): |
|
448 |
raise CancelSubmitError |
|
449 | ||
450 | ||
180 | 451 |
class FormDef(BaseObject): |
181 | 452 |
geolocations = None |
182 | 453 | |
183 | 454 |
def __init__(self, wcs_api, **kwargs): |
184 |
self.__wcs_api = wcs_api
|
|
455 |
self._wcs_api = wcs_api |
|
185 | 456 |
self.__dict__.update(**kwargs) |
186 | 457 | |
187 |
def __unicode__(self):
|
|
458 |
def __str__(self):
|
|
188 | 459 |
return self.title |
189 | 460 | |
190 | 461 |
@property |
191 |
def datas(self): |
|
192 |
datas = self.__wcs_api.get_formdata(self.slug) |
|
193 |
for data in datas: |
|
194 |
data.formdef = self |
|
195 |
yield data |
|
462 |
def formdatas(self): |
|
463 |
return FormDatas(wcs_api=self._wcs_api, formdef=self) |
|
196 | 464 | |
197 | 465 |
@property |
198 | 466 |
def schema(self): |
199 |
return self.__wcs_api.get_schema(self.slug) |
|
200 | ||
201 |
def __repr__(self): |
|
202 |
return '<{klass} {slug!r}>'.format(klass=self.__class__.__name__, slug=self.slug) |
|
467 |
if not hasattr(self, '_schema'): |
|
468 |
d = self._wcs_api.get_json('api/formdefs/{self.slug}/schema'.format(self=self)) |
|
469 |
self._schema = Schema(self._wcs_api, **d) |
|
470 |
return self._schema |
|
471 | ||
472 |
@contextlib.contextmanager |
|
473 |
def submit(self, **kwargs): |
|
474 |
submitter = FormDefSubmit( |
|
475 |
wcs_api=self._wcs_api, |
|
476 |
formdef=self, |
|
477 |
**kwargs) |
|
478 |
try: |
|
479 |
yield submitter |
|
480 |
except CancelSubmitError: |
|
481 |
return |
|
482 |
payload = submitter.payload() |
|
483 |
d = self._wcs_api.post_json(payload, 'api/formdefs/{self.slug}/submit'.format(self=self)) |
|
484 |
if d['err'] != 0: |
|
485 |
raise WcsApiError('submited returned an error: %s' % d) |
|
486 |
submitter.result = BaseObject(self._wcs_api, **d['data']) |
|
203 | 487 | |
204 | 488 | |
205 | 489 |
class Role(BaseObject): |
... | ... | |
210 | 494 |
pass |
211 | 495 | |
212 | 496 | |
497 |
class WcsObjects(object): |
|
498 |
url = None |
|
499 |
object_class = None |
|
500 | ||
501 |
def __init__(self, wcs_api): |
|
502 |
self.wcs_api = wcs_api |
|
503 | ||
504 |
def __getitem__(self, slug): |
|
505 |
if isinstance(slug, self.object_class): |
|
506 |
slug = slug.slug |
|
507 |
for instance in self: |
|
508 |
if instance.slug == slug: |
|
509 |
return instance |
|
510 |
raise KeyError('no instance with slug %r' % slug) |
|
511 | ||
512 |
def __iter__(self): |
|
513 |
for d in self.wcs_api.get_json(self.url)['data']: |
|
514 |
yield self.object_class(wcs_api=self.wcs_api, **d) |
|
515 | ||
516 |
def __len__(self): |
|
517 |
return len(list((o for o in self))) |
|
518 | ||
519 | ||
520 |
class Roles(WcsObjects): |
|
521 |
# Paths are not coherent :/ |
|
522 |
url = 'api/roles' |
|
523 |
object_class = Role |
|
524 | ||
525 | ||
526 |
class FormDefs(WcsObjects): |
|
527 |
url = 'api/formdefs/?include-count=on' |
|
528 |
object_class = FormDef |
|
529 | ||
530 | ||
531 |
class Categories(WcsObjects): |
|
532 |
url = 'api/categories/' |
|
533 |
object_class = Category |
|
534 | ||
535 | ||
213 | 536 |
class WcsApi(object): |
214 |
def __init__(self, url, orig, key, verify=True, slugs=None, batch_size=500): |
|
537 |
def __init__(self, url, email=None, name_id=None, batch_size=1000, |
|
538 |
session=None, logger=None, orig=None, key=None, verify=True): |
|
215 | 539 |
self.url = url |
540 |
self.batch_size = batch_size |
|
541 |
self.email = email |
|
542 |
self.name_id = name_id |
|
543 |
self.requests = session or requests.Session() |
|
544 |
self.logger = logger or logging.getLogger(__name__) |
|
216 | 545 |
self.orig = orig |
217 | 546 |
self.key = key |
218 | 547 |
self.verify = verify |
219 |
self.cache = {} |
|
220 |
self.slugs = slugs or [] |
|
221 |
self.batch_size = batch_size |
|
222 | 548 | |
223 |
@property |
|
224 |
def formdefs_url(self): |
|
225 |
return urlparse.urljoin(self.url, 'api/formdefs/') |
|
226 | ||
227 |
@property |
|
228 |
def forms_url(self): |
|
229 |
return urlparse.urljoin(self.url, 'api/forms/') |
|
230 | ||
231 |
@property |
|
232 |
def roles_url(self): |
|
233 |
return urlparse.urljoin(self.url, 'api/roles') |
|
549 |
def _build_url(self, url_parts): |
|
550 |
url = self.url |
|
551 |
for url_part in url_parts: |
|
552 |
url = urlparse.urljoin(url, url_part) |
|
553 |
return url |
|
234 | 554 | |
235 | 555 |
def get_json(self, *url_parts): |
236 |
url = reduce(lambda x, y: urlparse.urljoin(x, y), url_parts) |
|
237 |
params = {'orig': self.orig} |
|
238 |
query_string = urllib.urlencode(params) |
|
239 |
presigned_url = url + ('&' if '?' in url else '?') + query_string |
|
240 |
if presigned_url in self.cache: |
|
241 |
return self.cache[presigned_url] |
|
242 |
signed_url = signature.sign_url(presigned_url, self.key) |
|
556 |
url = self._build_url(url_parts) |
|
557 |
params = {} |
|
558 |
if self.email: |
|
559 |
params['email'] = self.email |
|
560 |
if self.name_id: |
|
561 |
params['NameID'] = self.name_id |
|
562 |
if self.orig: |
|
563 |
params['orig'] = self.orig |
|
564 |
query_string = urlparse.urlencode(params) |
|
565 |
complete_url = url + ('&' if '?' in url else '?') + query_string |
|
566 |
final_url = complete_url |
|
567 |
if self.key: |
|
568 |
final_url = signature.sign_url(final_url, self.key) |
|
243 | 569 |
try: |
244 |
response = requests.get(signed_url, verify=self.verify) |
|
570 |
response = self.requests.get(final_url, verify=self.verify) |
|
571 |
response.raise_for_status() |
|
245 | 572 |
except requests.RequestException as e: |
246 |
raise WcsApiError('GET request failed', url=signed_url, exception=e) |
|
573 |
content = getattr(getattr(e, 'response', None), 'content', None) |
|
574 |
raise WcsApiError('GET request failed', final_url, e, content) |
|
575 |
else: |
|
576 |
try: |
|
577 |
return response.json() |
|
578 |
except ValueError as e: |
|
579 |
raise WcsApiError('Invalid JSON content', final_url, e) |
|
580 | ||
581 |
def post_json(self, data, *url_parts): |
|
582 |
url = self._build_url(url_parts) |
|
583 |
params = {} |
|
584 |
if self.email: |
|
585 |
params['email'] = self.email |
|
586 |
if self.name_id: |
|
587 |
params['NameID'] = self.name_id |
|
588 |
if self.orig: |
|
589 |
params['orig'] = self.orig |
|
590 |
query_string = urlparse.urlencode(params) |
|
591 |
complete_url = url + ('&' if '?' in url else '?') + query_string |
|
592 |
final_url = complete_url |
|
593 |
if self.key: |
|
594 |
final_url = signature.sign_url(final_url, self.key) |
|
595 |
try: |
|
596 |
response = self.requests.post( |
|
597 |
final_url, |
|
598 |
data=json.dumps(data), |
|
599 |
headers={'content-type': 'application/json'}, |
|
600 |
verify=self.verify) |
|
601 |
response.raise_for_status() |
|
602 |
except requests.RequestException as e: |
|
603 |
content = getattr(getattr(e, 'response', None), 'content', None) |
|
604 |
raise WcsApiError('POST request failed', final_url, e, content) |
|
247 | 605 |
else: |
248 |
if not response.ok: |
|
249 |
try: |
|
250 |
text = response.text |
|
251 |
except UnicodeError: |
|
252 |
text = '<undecodable>' + repr(response.content) |
|
253 |
raise WcsApiError('GET response is not 200', |
|
254 |
url=signed_url, |
|
255 |
status_code=response.status_code, |
|
256 |
content=text) |
|
257 | 606 |
try: |
258 |
content = response.json() |
|
259 |
self.cache[presigned_url] = content |
|
260 |
return content |
|
607 |
return response.json() |
|
261 | 608 |
except ValueError as e: |
262 |
raise WcsApiError('Invalid JSON content', url=signed_url, exception=e)
|
|
609 |
raise WcsApiError('Invalid JSON content', final_url, e)
|
|
263 | 610 | |
264 | 611 |
@property |
265 | 612 |
def roles(self): |
266 |
return [Role(wcs_api=self, **d) for d in self.get_json(self.roles_url)['data']]
|
|
613 |
return Roles(self)
|
|
267 | 614 | |
268 | 615 |
@property |
269 | 616 |
def formdefs(self): |
270 |
result = self.get_json(self.formdefs_url + '?include-count=on') |
|
271 |
if isinstance(result, dict): |
|
272 |
if result['err'] == 0: |
|
273 |
data = result['data'] |
|
274 |
else: |
|
275 |
logger.error(u'could not retrieve formdefs from %s, err_desc: %s', |
|
276 |
self.formdefs_url, result.get('err_desc')) |
|
277 |
return [] |
|
278 |
else: |
|
279 |
data = result |
|
280 |
return [FormDef(wcs_api=self, **d) for d in data |
|
281 |
if not self.slugs or d['slug'] in self.slugs] |
|
617 |
return FormDefs(self) |
|
282 | 618 | |
283 | 619 |
@property |
284 | 620 |
def categories(self): |
285 |
d = {} |
|
286 |
for f in self.formdefs: |
|
287 |
if hasattr(f.schema, 'category'): |
|
288 |
d[f.schema.category_id] = f.schema.category |
|
289 |
return [Category(wcs_api=self, id=k, name=v) for k, v in d.items()] |
|
290 | ||
291 |
def get_formdata(self, slug): |
|
292 |
offset = 0 |
|
293 |
limit = self.batch_size |
|
294 |
while True: |
|
295 |
data = self.get_json(self.forms_url, |
|
296 |
slug + '/list?anonymise&full=on&offset=%d&limit=%d' % (offset, limit)) |
|
297 |
for d in data: |
|
298 |
# w.c.s. had a bug where some formdata lost their draft status, skip them |
|
299 |
if not d.get('receipt_time'): |
|
300 |
continue |
|
301 |
yield FormData(wcs_api=self, **d) |
|
302 |
if len(data) < limit: |
|
303 |
break |
|
304 |
offset += limit |
|
305 | ||
306 |
def get_schema(self, slug): |
|
307 |
json_schema = self.get_json(self.formdefs_url, slug + '/', 'schema?anonymise') |
|
308 |
return Schema(wcs_api=self, **json_schema) |
|
621 |
return Categories(self) |
|
309 |
- |