-
Notifications
You must be signed in to change notification settings - Fork 20
/
Copy pathclient.py
1712 lines (1434 loc) · 66.3 KB
/
client.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
from __future__ import print_function
from functools import partial
import itertools
import json
import os
import time
# Python3 support
try:
# Python3
from urllib.parse import urlencode
except ImportError:
# Python2
from urllib import urlencode
from groclient import cfg, lib
from groclient.constants import DATA_SERIES_UNIQUE_TYPES_ID, ENTITY_KEY_TO_TYPE
from groclient.utils import intersect, zip_selections, dict_unnest, str_snake_to_camel
from groclient.lib import APIError
import pandas
from tornado import gen
from tornado.escape import json_decode
from tornado.httpclient import AsyncHTTPClient, HTTPRequest, HTTPError
from tornado.ioloop import IOLoop
from tornado.queues import Queue
class BatchError(APIError):
"""Replicate the APIError interface given a Tornado HTTPError."""
def __init__(self, response, retry_count, url, params):
self.response = response
self.retry_count = retry_count
self.url = url
self.params = params
self.status_code = (
self.response.code if hasattr(self.response, "code") else None
)
try:
json_content = json_decode(self.response.body)
# 'error' should be something like 'Not Found' or 'Bad Request'
self.message = json_content.get("error", "")
# Some error responses give additional info.
# For example, a 400 Bad Request might say "metricId is required"
if "message" in json_content:
self.message += ": {}".format(json_content["message"])
except Exception:
# If the error message can't be parsed, fall back to a generic "giving up" message.
self.message = "Giving up on {} after {} {}: {}".format(
self.url,
self.retry_count,
"retry" if self.retry_count == 1 else "retries",
response,
)
class GroClient(object):
"""API client with stateful authentication for lib functions and extra convenience methods."""
def __init__(self, api_host=cfg.API_HOST, access_token=None):
"""Construct a GroClient instance.
Parameters
----------
api_host : string, optional
The API server hostname.
access_token : string, optional
Your Gro API authentication token. If not specified, the
:code:`$GROAPI_TOKEN` environment variable is used. See
:doc:`authentication`.
Raises
------
RuntimeError
Raised when neither the :code:`access_token` parameter nor
:code:`$GROAPI_TOKEN` environment variable are set.
Examples
--------
>>> client = GroClient() # token stored in $GROAPI_TOKEN
>>> client = GroClient(access_token="your_token_here")
"""
# Initialize early since they're referenced in the destructor and
# access_token checking may cause constructor to exit early.
self._async_http_client = None
self._ioloop = None
if access_token is None:
access_token = os.environ.get("GROAPI_TOKEN")
if access_token is None:
raise RuntimeError("$GROAPI_TOKEN environment variable must be set when "
"GroClient is constructed without the access_token argument")
self.api_host = api_host
self.access_token = access_token
self._logger = lib.get_default_logger()
self._data_series_list = set() # all that have been added
self._data_series_queue = [] # added but not loaded in data frame
self._data_frame = pandas.DataFrame()
try:
# Each GroClient has its own IOLoop and AsyncHTTPClient.
self._ioloop = IOLoop()
# Note: force_instance is needed to disable Tornado's
# pseudo-singleton AsyncHTTPClient caching behavior.
self._async_http_client = AsyncHTTPClient(force_instance=True)
except Exception as e:
self._logger.warning(
"Unable to initialize event loop, async methods disabled: {}".format(e)
)
def __del__(self):
if self._async_http_client is not None:
self._async_http_client.close()
if self._ioloop is not None:
self._ioloop.stop()
self._ioloop.close()
def get_logger(self):
return self._logger
@gen.coroutine
def async_get_data(self, url, headers, params=None):
base_log_record = dict(route=url, params=params)
def log_request(start_time, retry_count, msg, status_code):
elapsed_time = time.time() - start_time
log_record = dict(base_log_record)
log_record["elapsed_time_in_ms"] = 1000 * elapsed_time
log_record["retry_count"] = retry_count
log_record["status_code"] = status_code
if status_code == 200:
self._logger.debug(msg, extra=log_record)
else:
self._logger.warning(msg, extra=log_record)
"""General 'make api request' function.
Assigns headers and builds in retries and logging.
"""
self._logger.debug(url)
# append version info
headers.update(lib.get_version_info())
# Initialize to -1 so first attempt will be retry 0
retry_count = -1
while retry_count <= cfg.MAX_RETRIES:
retry_count += 1
start_time = time.time()
http_request = HTTPRequest(
"{url}?{params}".format(url=url, params=urlencode(params)),
method="GET",
headers=headers,
request_timeout=cfg.TIMEOUT,
connect_timeout=cfg.TIMEOUT,
)
try:
try:
response = yield self._async_http_client.fetch(http_request)
status_code = response.code
except HTTPError as e:
# Catch non-200 codes that aren't errors
status_code = e.code if hasattr(e, "code") else None
if status_code in [204, 206]:
log_msg = {204: "No Content", 206: "Partial Content"}[
status_code
]
response = e.response
log_request(start_time, retry_count, log_msg, status_code)
# Do not retry.
elif status_code == 301:
redirected_ids = json.loads(e.response.body.decode("utf-8"))[
"data"
][0]
new_params = lib.redirect(params, redirected_ids)
log_request(
start_time,
retry_count,
"Redirecting {} to {}".format(params, new_params),
status_code,
)
params = new_params
continue # retry
else: # Otherwise, propagate to error handling
raise e
except Exception as e:
# HTTPError raised when there's a non-200 status code
# socket.gaio error raised when there's a connection error
response = e.response if hasattr(e, "response") else e
status_code = e.code if hasattr(e, "code") else None
error_msg = (
e.response.error
if (hasattr(e, "response") and hasattr(e.response, "error"))
else e
)
log_request(start_time, retry_count, error_msg, status_code)
if status_code in [429, 500, 502, 503, 504]:
# First retry is immediate.
# After that, exponential backoff before retrying.
if retry_count > 0:
time.sleep(2 ** retry_count)
continue
elif status_code in [400, 401, 402, 404]:
break # Do not retry. Go right to raising an Exception.
# Request was successful
log_request(start_time, retry_count, "OK", status_code)
raise gen.Return(
json_decode(response.body) if hasattr(response, "body") else None
)
# Retries failed. Raise exception
raise BatchError(response, retry_count, url, params)
def batch_async_queue(self, func, batched_args, output_list, map_result):
"""Asynchronously call func.
Parameters
----------
func : function
The function to be batched. Typically a Client method.
batched_args : list of dicts
Inputs
output_list : any, optional
A custom accumulator to use in map_result. For example: may pass in a non-empty list
to append results to it, or may pass in a pandas dataframe, etc. By default, is a list
of n 0s, where n is the length of batched_args.
map_result : function, optional
Function to apply changes to individual requests' responses before returning. Must
return an accumulator, like a map() function.
Takes 4 params:
1. the index in batched_args
2. the element from batched_args
3. the result from that input
4. `output_list`. The accumulator of all results
"""
assert (
type(batched_args) is list
), "Only argument to a batch async decorated function should be a \
list of a list of the individual non-keyword arguments being \
passed to the original function."
# Wrap output_list in an object so it can be modified within inner functions' scope
# In Python 3, can accomplish the same thing with `nonlocal` keyword.
output_data = {}
if output_list is None:
output_data["result"] = [0] * len(batched_args)
else:
output_data["result"] = output_list
if not map_result:
# Default map_result function separates output by index of the query. For example:
# batched_args: [exports of corn, exports of soybeans]
# accumulator: [[corn datapoint, corn datapoint],
# [soybean data point, soybean data point]]
def map_result(idx, query, response, accumulator):
accumulator[idx] = response
return accumulator
q = Queue()
@gen.coroutine
def consumer():
"""Execute func on all items in queue asynchronously."""
while q.qsize():
try:
idx, item = q.get().result()
self._logger.debug("Doing work on {}".format(idx))
if type(item) is dict:
# Assume that dict types should be unpacked as kwargs
result = yield func(**item)
elif type(item) is list:
# Assume that list types should be unpacked as positional args
result = yield func(*item)
else:
result = yield func(item)
output_data["result"] = map_result(
idx, item, result, output_data["result"]
)
self._logger.debug("Done with {}".format(idx))
q.task_done()
except Exception:
# Cease processing
# IOLoop raises "Operation timed out after None seconds"
self._ioloop.stop()
self._ioloop.close()
def producer():
"""Immediately enqueue the whole batch of requests."""
lasttime = time.time()
for idx, item in enumerate(batched_args):
q.put((idx, item))
elapsed = time.time() - lasttime
self._logger.info("Queued {} requests in {}".format(q.qsize(), elapsed))
@gen.coroutine
def main():
# Start consumer without waiting (since it never finishes).
for i in range(cfg.MAX_QUERIES_PER_SECOND):
self._ioloop.spawn_callback(consumer)
producer() # Wait for producer to put all tasks.
yield q.join() # Wait for consumer to finish all tasks.
self._ioloop.run_sync(main)
return output_data["result"]
# TODO: deprecate the following two methods, standardize on one
# approach with get_data_points and get_df
@gen.coroutine
def get_data_points_generator(self, **selection):
headers = {"authorization": "Bearer " + self.access_token}
url = "/".join(["https:", "", self.api_host, "v2/data"])
params = lib.get_data_call_params(**selection)
required_params = [str_snake_to_camel(type_id) for type_id in DATA_SERIES_UNIQUE_TYPES_ID if type_id != 'partner_region_id']
missing_params = list(required_params - params.keys())
if len(missing_params):
message = 'API request cannot be processed because {} not specified.'.format(missing_params[0] + ' is' if len(missing_params) == 1 else ', '.join(missing_params[:-1]) + ' and ' + missing_params[-1] + ' are')
self._logger.error(message)
raise ValueError(message)
try:
list_of_series_points = yield self.async_get_data(url, headers, params)
include_historical = selection.get("include_historical", True)
points = lib.list_of_series_to_single_series(
list_of_series_points, False, include_historical
)
# Apply unit conversion if a unit is specified
if "unit_id" in selection:
raise gen.Return(list(
map(
partial(self.convert_unit, target_unit_id=selection["unit_id"]),
points,
)
))
raise gen.Return(points)
except BatchError as b:
raise gen.Return(b)
def batch_async_get_data_points(
self, batched_args, output_list=None, map_result=None
):
"""Make many :meth:`~get_data_points` requests asynchronously.
Parameters
----------
batched_args : list of dicts
Each dict should be a `selections` object like would be passed to
:meth:`~get_data_points`.
Example::
input_list = [
{'metric_id': 860032, 'item_id': 274, 'region_id': 1215, 'frequency_id': 9, 'source_id': 2},
{'metric_id': 860032, 'item_id': 270, 'region_id': 1215, 'frequency_id': 9, 'source_id': 2}
]
output_list : any, optional
A custom accumulator to use in map_result. For example: may pass in a non-empty list
to append results to it, or may pass in a pandas dataframe, etc. By default, is a list
of n 0s, where n is the length of batched_args.
map_result : function, optional
Function to apply changes to individual requests' responses before returning.
Takes 4 params:
1. the index in batched_args
2. the element from batched_args
3. the result from that input
4. `output_list`. The accumulator of all results
Example::
output_list = []
# Merge all responses into a single list
def map_response(inputIndex, inputObject, response, output_list):
output_list += response
return output_list
batch_output = client.batch_async_get_data_points(input_list,
output_list=output_list,
map_result=map_response)
Returns
-------
any
By default, returns a list of lists of data points. Likely either objects or lists of
dictionaries. If using a custom map_result function, can return any type.
Example of the default output format::
[
[
{'metric_id': 1, 'item_id': 2, 'start_date': 2000-01-01, 'value': 41, ...},
{'metric_id': 1, 'item_id': 2, 'start_date': 2001-01-01, 'value': 39, ...},
{'metric_id': 1, 'item_id': 2, 'start_date': 2002-01-01, 'value': 50, ...},
],
[
{'metric_id': 1, 'item_id': 6, 'start_date': 2000-01-01, 'value': 12, ...},
{'metric_id': 1, 'item_id': 6, 'start_date': 2001-01-01, 'value': 13, ...},
{'metric_id': 1, 'item_id': 6, 'start_date': 2002-01-01, 'value': 4, ...},
],
]
"""
return self.batch_async_queue(
self.get_data_points_generator, batched_args, output_list, map_result
)
@gen.coroutine
def async_rank_series_by_source(self, *selections_list):
"""Get all sources, in ranked order, for a given selection."""
response = self.rank_series_by_source(selections_list)
raise gen.Return(list(response))
def batch_async_rank_series_by_source(
self, batched_args, output_list=None, map_result=None
):
"""Perform multiple rank_series_by_source requests asynchronously.
Parameters
----------
batched_args : list of lists of dicts
See :meth:`~.rank_series_by_source` `selections_list`. A list of those lists.
"""
return self.batch_async_queue(
self.async_rank_series_by_source, batched_args, output_list, map_result
)
def get_available(self, entity_type):
"""List the first 5000 available entities of the given type.
Parameters
----------
entity_type : {'metrics', 'items', 'regions'}
Returns
-------
data : list of dicts
Example::
[ { 'id': 0, 'contains': [1, 2, 3], 'name': 'World', 'level': 1},
{ 'id': 1, 'contains': [4, 5, 6], 'name': 'Asia', 'level': 2},
... ]
"""
return lib.get_available(self.access_token, self.api_host, entity_type)
def list_available(self, selected_entities):
"""List available entities given some selected entities.
Given one or more selections, return entities combinations that have
data for the given selections.
Parameters
----------
selected_entities : dict
Example::
{ 'metric_id': 123, 'item_id': 456, 'source_id': 7 }
Keys may include: metric_id, item_id, region_id, partner_region_id,
source_id, frequency_id
Returns
-------
list of dicts
Example::
[ { 'metric_id': 11078, 'metric_name': 'Export Value (currency)',
'item_id': 274, 'item_name': 'Corn',
'region_id': 1215, 'region_name': 'United States',
'source_id': 15, 'source_name': 'USDA GATS' },
{ ... },
... ]
"""
return lib.list_available(self.access_token, self.api_host, selected_entities)
def lookup(self, entity_type, entity_ids):
"""Retrieve details about a given id or list of ids of type entity_type.
https://developers.gro-intelligence.com/gro-ontology.html
Parameters
----------
entity_type : { 'metrics', 'items', 'regions', 'frequencies', 'sources', 'units' }
entity_ids : int or list of ints
Returns
-------
dict or dict of dicts
A dict with entity details is returned if an integer is given for entity_ids.
A dict of dicts with entity details, keyed by id, is returned if a list of integers is
given for entity_ids.
Example::
{ 'id': 274,
'contains': [779, 780, ...]
'name': 'Corn',
'definition': 'The seeds of the widely cultivated corn plant <i>Zea mays</i>,'
' which is one of the world\'s most popular grains.' }
Example::
{ '274': {
'id': 274,
'contains': [779, 780, ...],
'belongsTo': [4138, 8830, ...],
'name': 'Corn',
'definition': 'The seeds of the widely cultivated corn plant'
' <i>Zea mays</i>, which is one of the world\'s most popular'
' grains.'
},
'270': {
'id': 270,
'contains': [1737, 7401, ...],
'belongsTo': [8830, 9053, ...],
'name': 'Soybeans',
'definition': 'The seeds and harvested crops of plants belonging to the'
' species <i>Glycine max</i> that are used in the production'
' of oil and both human and livestock consumption.'
}
}
"""
return lib.lookup(self.access_token, self.api_host, entity_type, entity_ids)
def lookup_unit_abbreviation(self, unit_id):
return self.lookup("units", unit_id)["abbreviation"]
def get_allowed_units(self, metric_id, item_id=None):
"""Get a list of unit that can be used with the given metric (and
optionally, item).
Parameters
----------
metric_id: int
item_id: int, optional.
Returns
-------
list of unit ids
"""
return lib.get_allowed_units(
self.access_token, self.api_host, metric_id, item_id
)
def get_data_series(self, **selection):
"""Get available data series for the given selections.
https://developers.gro-intelligence.com/data-series-definition.html
Parameters
----------
metric_id : integer, optional
item_id : integer, optional
region_id : integer, optional
partner_region_id : integer, optional
source_id : integer, optional
frequency_id : integer, optional
Returns
-------
list of dicts
Example::
[{ 'metric_id': 2020032, 'metric_name': 'Seed Use',
'item_id': 274, 'item_name': 'Corn',
'region_id': 1215, 'region_name': 'United States',
'source_id': 24, 'source_name': 'USDA FEEDGRAINS',
'frequency_id': 7,
'start_date': '1975-03-01T00:00:00.000Z',
'end_date': '2018-05-31T00:00:00.000Z'
}, { ... }, ... ]
"""
return lib.get_data_series(self.access_token, self.api_host, **selection)
def stream_data_series(self, chunk_size=10000, **selection):
"""Retrieve available data series for the given selections.
Similar to :meth:`~.get_data_series`, but API will stream data in chunk of given size
Parameters
----------
chunk_size : integer, optional
Number of data series to be returned in each chunk. Defaults to 10000
metric_id : integer, optional
item_id : integer, optional
region_id : integer, optional
partner_region_id : integer, optional
source_id : integer, optional
frequency_id : integer, optional
Yields
-------
list of dicts
Example::
[{ 'metric_id': 2020032, 'metric_name': 'Seed Use',
'item_id': 274, 'item_name': 'Corn',
'region_id': 1215, 'region_name': 'United States',
'source_id': 24, 'source_name': 'USDA FEEDGRAINS',
'frequency_id': 7,
'start_date': '1975-03-01T00:00:00.000Z',
'end_date': '2018-05-31T00:00:00.000Z'
}, { ... }, ... ]
"""
return lib.stream_data_series(self.access_token, self.api_host, chunk_size, **selection)
def search(self, entity_type, search_terms):
"""Search for the given search term. Better matches appear first.
Parameters
----------
entity_type : { 'metrics', 'items', 'regions', 'sources' }
search_terms : string
Returns
-------
list of dicts
Example::
[{'id': 5604}, {'id': 10204}, {'id': 10210}, ....]
"""
return lib.search(self.access_token, self.api_host, entity_type, search_terms)
def search_and_lookup(self, entity_type, search_terms, num_results=10):
"""Search for the given search terms and look up their details.
For each result, yield a dict of the entity and it's properties.
Parameters
----------
entity_type : { 'metrics', 'items', 'regions', 'sources' }
search_terms : string
num_results: int
Maximum number of results to return. Defaults to 10.
Yields
------
dict
Result from :meth:`~.search` passed to :meth:`~.lookup` to get additional details.
Example::
{ 'id': 274,
'contains': [779, 780, ...],
'name': 'Corn',
'definition': 'The seeds of the widely cultivated...' }
See output of :meth:`~.lookup`. Note that as with :meth:`~.search`, the first result is
the best match for the given search term(s).
"""
return lib.search_and_lookup(
self.access_token, self.api_host, entity_type, search_terms, num_results
)
def lookup_belongs(self, entity_type, entity_id):
"""Look up details of entities containing the given entity.
Parameters
----------
entity_type : { 'metrics', 'items', 'regions' }
entity_id : int
Yields
------
dict
Result of :meth:`~.lookup` on each entity the given entity belongs to.
For example: For the region 'United States', one yielded result will be for
'North America.' The format of which matches the output of :meth:`~.lookup`::
{ 'id': 15,
'contains': [ 1008, 1009, 1012, 1215, ... ],
'name': 'North America',
'level': 2 }
"""
return lib.lookup_belongs(
self.access_token, self.api_host, entity_type, entity_id
)
def rank_series_by_source(self, selections_list):
"""Given a list of series selections, for each unique combination excluding source, expand
to all available sources and return them in ranked order. The order corresponds to how well
that source covers the selection (metrics, items, regions, and time range and frequency).
Parameters
----------
selections_list : list of dicts
See the output of :meth:`~.get_data_series`.
Yields
------
dict
The input selections_list, expanded out to each possible source, ordered by coverage.
"""
return lib.rank_series_by_source(
self.access_token, self.api_host, selections_list
)
def get_geo_centre(self, region_id):
"""Given a region ID, return the geographic centre in degrees lat/lon.
Parameters
----------
region_id : integer
Returns
-------
list of dicts
Example::
[{'centre': [ 39.8333, -98.5855 ], 'regionId': 1215, 'regionName': 'United States'}]
"""
return lib.get_geo_centre(self.access_token, self.api_host, region_id)
def get_geojsons(self, region_id, descendant_level=None, zoom_level=7):
"""Given a region ID, return shape information in geojson, for the
region and all its descendants at the given level (if specified).
Parameters
----------
region_id : integer
descendant_level : integer, admin region level (2, 3, 4 or 5)
zoom_level : integer, optional(allow 1-8)
Valid if include_geojson equals True. If zoom level is specified and it is less than 6,
simplified shapefile will be returned. Otherwise, detailed shapefile will be used by default.
Returns
-------
list of dicts
Example::
[{ 'centre': [ 39.8333, -98.5855 ],
'regionId': 1215,
'regionName': 'United States',
u'geojson': u'{"type":"GeometryCollection","geometries":[{"type":"MultiPolygon","coordinates":[[[[-155.651382446,20.1647224430001], ...]]]}]}'
}]
"""
return lib.get_geojsons(
self.access_token, self.api_host, region_id, descendant_level, zoom_level)
def get_geojson(self, region_id, zoom_level=7):
"""Given a region ID, return shape information in geojson.
Parameters
----------
region_id : integer
zoom_level : integer, optional(allow 1-8)
Valid if include_geojson equals True. If zoom level is specified and it is less than 6,
simplified shapefile will be returned. Otherwise, detailed shapefile will be used by default.
Returns
-------
a geojson object or None
Example::
{ 'type': 'GeometryCollection',
'geometries': [{'type': 'MultiPolygon',
'coordinates': [[[[-38.394, -4.225], ...]]]}, ...]}
"""
return lib.get_geojson(self.access_token, self.api_host, region_id, zoom_level)
def get_ancestor(
self,
entity_type,
entity_id,
distance=None,
include_details=True,
ancestor_level=None,
include_historical=True,
):
"""Given an item, metric, or region, returns all its ancestors i.e.
entities that "contain" in the given entity.
The `distance` parameter controls how many levels of ancestor entities you want to be
returned. Additionally, if you are getting the ancestors of a given region, you can
specify the `ancestor_level`, which will return only the ancestors of the given
`ancestor_level`. However, if both parameters are specified, `distance` takes precedence
over `ancestor_level`.
Parameters
----------
entity_type : { 'metrics', 'items', 'regions' }
entity_id : integer
distance: integer, optional
Return all entities that contain the entity_id at maximum distance. If provided along
with `ancestor_level`, this will take precedence over `ancestor_level`.
If not provided, get all ancestors.
include_details : boolean, optional
True by default. Will perform a lookup() on each ancestor to find name,
definition, etc. If this option is set to False, only ids of ancestor
entities will be returned, which makes execution significantly faster.
ancestor_level : integer, optional
The region level of interest. See REGION_LEVELS constant. This should only be specified
if the `entity_type` is 'regions'. If provided along with `distance`, `distance` will
take precedence. If not provided, and `distance` not provided, get all ancestors.
include_historical : boolean, optional
True by default. If False is specified, regions that only exist in historical data
(e.g. the Soviet Union) will be excluded.
Returns
-------
list of dicts
Example::
[{
'id': 134,
'name': 'Cattle hides, wet-salted',
'definition': 'Hides and skins of domesticated cattle-animals ...',
} , {
'id': 382,
'name': 'Calf skins, wet-salted',
'definition': 'Wet-salted hides and skins of calves-animals of ...'
}, ...]
See output of :meth:`~.lookup`
"""
return lib.get_ancestor(
self.access_token,
self.api_host,
entity_type,
entity_id,
distance,
include_details,
ancestor_level,
include_historical,
)
def get_descendant(
self,
entity_type,
entity_id,
distance=None,
include_details=True,
descendant_level=None,
include_historical=True,
):
"""Given an item, metric or region, returns all its descendants i.e.
entities that are "contained" in the given entity
The `distance` parameter controls how many levels of child entities you want to be returned.
Additionally, if you are getting the descendants of a given region, you can specify the
`descendant_level`, which will return only the descendants of the given `descendant_level`.
However, if both parameters are specified, `distance` takes precedence over
`descendant_level`.
Parameters
----------
entity_type : { 'metrics', 'items', 'regions' }
entity_id : integer
distance: integer, optional
Return all entities that contain the entity_id at maximum distance. If provided along
with `descendant_level`, this will take precedence over `descendant_level`.
If not provided, get all ancestors.
include_details : boolean, optional
True by default. Will perform a lookup() on each descendant to find name,
definition, etc. If this option is set to False, only ids of descendant
entities will be returned, which makes execution significantly faster.
descendant_level : integer, optional
The region level of interest. See REGION_LEVELS constant. This should only be specified
if the `entity_type` is 'regions'. If provided along with `distance`, `distance` will
take precedence. If not provided, and `distance` not provided, get all descendants.
include_historical : boolean, optional
True by default. If False is specified, regions that only exist in historical data
(e.g. the Soviet Union) will be excluded.
Returns
-------
list of dicts
Example::
[{
'id': 134,
'name': 'Cattle hides, wet-salted',
'definition': 'Hides and skins of domesticated cattle-animals ...',
} , {
'id': 382,
'name': 'Calf skins, wet-salted',
'definition': 'Wet-salted hides and skins of calves-animals of ...'
}, ...]
See output of :meth:`~.lookup`
"""
return lib.get_descendant(
self.access_token,
self.api_host,
entity_type,
entity_id,
distance,
include_details,
descendant_level,
include_historical,
)
def get_descendant_regions(
self,
region_id,
descendant_level=None,
include_historical=True,
include_details=True,
distance=None,
):
"""Look up details of all regions of the given level contained by a region.
This method is deprecated, and should be replaced by :meth:`~.get_descendant` which has
been updated to include the `descendant_level` and `include_historical` parameters.
Given any region by id, get all the descendant regions that are of the specified level.
Parameters
----------
region_id : integer
descendant_level : integer, optional
The region level of interest. See REGION_LEVELS constant. If not provided, get all
descendants.
include_historical : boolean, optional
True by default. If False is specified, regions that only exist in historical data
(e.g. the Soviet Union) will be excluded.
include_details : boolean, optional
True by default. Will perform a lookup() on each descendant region to find name,
latitude, longitude, etc. If this option is set to False, only ids of descendant
regions will be returned, which makes execution significantly faster.
distance: integer, optional
Return all entity contained to entity_id at maximum distance.
If provided, it will take precedence over `descendant_level`.
If not provided, get all descendants.
Returns
-------
list of dicts
Example::
[{
'id': 13100,
'contains': [139839, 139857, ...],
'name': 'Wisconsin',
'level': 4
} , {
'id': 13101,
'contains': [139891, 139890, ...],
'name': 'Wyoming',
'level': 4
}, ...]
See output of :meth:`~.lookup`
"""
return lib.get_descendant(
self.access_token,
self.api_host,
'regions',
region_id,
distance,
include_details,
descendant_level,
include_historical,
)
def get_available_timefrequency(self, **selection):
"""Given a selection, return a list of frequencies and time ranges.
The results are ordered by coverage-optimized ranking.
Parameters
----------
metric_id : integer, optional
item_id : integer, optional
region_id : integer, optional
partner_region_id : integer, optional
Returns
-------