Skip to content

Commit 302a365

Browse files
author
Dario Varotto
committed
Tox, Pytest, API methods for models and json endpoint support
1 parent a71667e commit 302a365

22 files changed

+581
-117
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@
77
/*.sh
88
/dist
99
/build
10+
/.*/

ravenpackapi/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
from .models.dataset import Dataset
2+
from .core import RPApi

ravenpackapi/core.py

Lines changed: 55 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,35 @@
11
import json
22
import logging
33
import os
4-
from time import sleep
54

65
import requests
76

87
from ravenpackapi import Dataset
98
from ravenpackapi.exceptions import APIException
10-
from ravenpackapi.models.dataset import DatasetList
11-
from ravenpackapi.models.job import Job
9+
from ravenpackapi.models.dataset_list import DatasetList
10+
from ravenpackapi.models.results import Results
1211
from ravenpackapi.util import to_curl
12+
from ravenpackapi.utils.constants import JSON_AVAILABLE_FIELDS
1313

1414
_VALID_METHODS = ('get', 'post', 'put', 'delete')
1515

1616
logger = logging.getLogger("ravenpack.core")
1717

1818

1919
class RPApi(object):
20-
_BASE_URL = os.environ.get('RP_API_ENDPOINT', 'https://api.ravenpack.com/1.0')
21-
_FILE_AVAILABILIY_SECONDS_DELAY = 5.0
22-
_CHUNK_SIZE = 1024 * 32
20+
_BASE_URL = os.environ.get('RP_API_ENDPOINT',
21+
'https://api.ravenpack.com/1.0')
2322

24-
def __init__(self, api_key):
23+
def __init__(self, api_key=None):
2524
api_key = api_key or os.environ.get('RP_API_KEY')
2625
if api_key is None:
2726
raise ValueError(
2827
"Please initialize with an api_key "
29-
"or set your environment RP_API_KEY with a permanent token"
28+
"or set your environment RP_API_KEY with your API KEY."
3029
)
3130
self.api_key = api_key
3231

33-
def request(self, endpoint, data=None, method='get'):
32+
def request(self, endpoint, data=None, params=None, method='get'):
3433
assert method in _VALID_METHODS, 'Method {used} not accepted. Please use {valid_methods}'
3534
logger.debug("Request to %s" % endpoint)
3635
requests_call = getattr(requests, method)
@@ -40,79 +39,70 @@ def request(self, endpoint, data=None, method='get'):
4039
url=self._BASE_URL + endpoint,
4140
headers=dict(API_KEY=self.api_key),
4241
data=json.dumps(data) if data else None,
42+
params=params,
4343
)
4444
if response.status_code != 200:
4545
logger.error("Error calling the API, we tried: %s" % to_curl(response.request))
46-
raise APIException('Got an error {status}: {error_message}'.format(
47-
status=response.status_code, error_message=response.text
48-
), response=response)
46+
raise APIException(
47+
'Got an error {status}: body was \'{error_message}\''.format(
48+
status=response.status_code, error_message=response.text
49+
), response=response)
4950
return response
5051

51-
def list_datasets(self, tags=None, scope=None):
52+
def list_datasets(self, scope=None, tags=None):
5253
""" Return a DataSetList of datasets in the scope """
53-
response = self.request('/datasets', data=dict(
54-
tags=tags or [],
54+
response = self.request('/datasets', params=dict(
55+
tags=tags or None,
5556
scope=scope or 'private',
5657
))
57-
return DatasetList(map(Dataset.from_dict, response.json()['datasets']))
58+
return DatasetList(
59+
map(lambda item: Dataset.from_dict(item, api=self),
60+
response.json()['datasets'])
61+
)
5862

5963
def create_dataset(self, dataset):
6064
response = self.request(endpoint="/datasets",
6165
data=dataset.as_dict(),
6266
method='post')
6367
dataset_id = response.json()['dataset_uuid']
6468
logger.info("Created dataset %s" % dataset_id)
65-
return dataset_id
6669

67-
def download_dataset(self, dataset_id, start_date, end_date,
68-
output_format='csv', compressed=False, notify=False):
69-
response = self.request(
70-
endpoint="/datafile/%s" % dataset_id,
71-
data={
72-
"start_date": start_date,
73-
"end_date": end_date,
74-
"format": output_format,
75-
"compressed": compressed,
76-
"notify": notify,
77-
},
78-
method='post',
70+
# we return the Dataset object just created
71+
dataset.api = self
72+
new_dataset_data = dataset.as_dict()
73+
new_dataset_data['uuid'] = dataset_id
74+
new_dataset = Dataset(api=self,
75+
**new_dataset_data)
76+
return new_dataset
77+
78+
def get_dataset(self, dataset_id):
79+
return Dataset(
80+
api=self,
81+
uuid=dataset_id,
7982
)
80-
job = Job(response.json()['token']) # an undefined job, has just the token
81-
return job
8283

83-
def get_job_status(self, job):
84-
token = job.token
84+
def json(self,
85+
start_date,
86+
end_date,
87+
fields,
88+
filters=None,
89+
time_zone=None,
90+
frequency='granular',
91+
having=None,
92+
product='rpa',
93+
product_version='1.0',
94+
):
95+
# let's build the body, with all the defined fields
96+
body = {}
97+
for k in JSON_AVAILABLE_FIELDS:
98+
if locals().get(k) is not None:
99+
body[k] = locals().get(k)
100+
85101
response = self.request(
86-
endpoint="/jobs/%s" % token,
87-
data={
88-
"token": token,
89-
},
90-
method='get',
102+
endpoint="/json",
103+
method='post',
104+
data=body,
91105
)
92-
return Job(token, **response.json())
93-
94-
def wait_job_to_be_ready(self, job):
95-
logger.info("Waiting for the job to be ready...")
96-
while True:
97-
try:
98-
job = self.get_job_status(job)
99-
except APIException: # keep waiting if API raises exceptions
100-
sleep(self._FILE_AVAILABILIY_SECONDS_DELAY)
101-
continue
102-
if not job.is_processing:
103-
return job
104-
sleep(self._FILE_AVAILABILIY_SECONDS_DELAY)
105-
106-
def save_job_to_file(self, job, filename):
107-
with open(filename, 'wb') as output:
108-
job = self.wait_job_to_be_ready(job)
109-
logger.info(u"Writing to %s" % filename)
110-
111-
r = requests.get(job.url,
112-
headers=dict(API_KEY=self.api_key),
113-
stream=True,
114-
)
115-
116-
for chunk in r.iter_content(chunk_size=self._CHUNK_SIZE):
117-
if chunk:
118-
output.write(chunk)
106+
data = response.json()
107+
return Results(data['records'],
108+
name='Ad-hoc JSON query')

ravenpackapi/examples/__init__.py

Whitespace-only changes.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from ravenpackapi import RPApi
2+
import logging
3+
4+
logging.basicConfig(level=logging.DEBUG)
5+
# initialize the API (here we use the RP_API_KEY in os.environ)
6+
api = RPApi()
7+
8+
# query the json endpoint for a dataset ***
9+
# use the public dataset `us30`
10+
ds = api.get_dataset(dataset_id='us500')
11+
# query the dataset analytics with the json endpoint
12+
data = ds.json(
13+
start_date='2018-01-05 18:00:00',
14+
end_date='2018-01-05 18:01:00',
15+
)
16+
17+
for record in data:
18+
print(record)
19+
20+
# query the ad-hoc json endpoint ***
21+
adhoc_data = api.json(
22+
start_date='2018-01-05 18:00:00',
23+
end_date='2018-01-05 18:01:00',
24+
fields=ds.fields,
25+
filters=ds.filters,
26+
)
27+
print(adhoc_data)
28+
for record in adhoc_data:
29+
print(record)

ravenpackapi/exceptions.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,28 @@
1-
class APIException(Exception):
1+
from functools import wraps
2+
23

3-
def __init__(self, *args: object, response=None) -> None:
4+
class APIException(Exception):
5+
def __init__(self, *args, **kwargs):
6+
response = kwargs.pop('response', None)
47
super(APIException, self).__init__(*args)
58
self.response = response
9+
10+
11+
class MissingAPIException(Exception):
12+
def __str__(self):
13+
return "Define the api parameter with your own APIKEY " \
14+
"to access the API methods from a model"
15+
16+
17+
class DataFileTimeout(Exception):
18+
pass
19+
20+
21+
def api_method(func):
22+
@wraps(func)
23+
def decorated_func(instance, *args, **kwargs):
24+
if not getattr(instance, 'api'):
25+
raise MissingAPIException()
26+
return func(instance, *args, **kwargs)
27+
28+
return decorated_func

0 commit comments

Comments
 (0)