forked from TheClimateCorporation/api-example
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathclimate.py
235 lines (199 loc) · 8.47 KB
/
climate.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
"""
Climate API demo code. This module shows how to:
- Log in with Climate
- Refresh the access_token
- Fetch fields
- Fetch field boundaries
- Upload files
License:
Copyright © 2017 The Climate Corporation
"""
import requests
import file
from base64 import b64encode
from urllib.parse import urlencode
from curlify import to_curl
from logger import Logger
json_content_type = 'application/json'
binary_content_type = 'application/octet-stream'
base_login_uri = 'https://climate.com/static/app-login/index.html'
token_uri = 'https://api.climate.com/api/oauth/token'
api_uri = 'https://platform.climate.com'
def login_uri(client_id, redirect_uri):
"""
URI for 'Log In with FieldView' link. The redirect_uri is a uri on your system (this app) that will handle the
authorization once the user has authenticated with FieldView.
"""
params = {
'scope': 'openid+platform+partnerapis',
'page': 'oidcauthn',
'mobile': 'true',
'response_type': 'code',
'client_id': client_id,
'redirect_uri': redirect_uri
}
return '{}?{}'.format(base_login_uri, urlencode(params))
def authorization_header(client_id, client_secret):
"""
Builds the authorization header unique to your company or application.
:param client_id: Provided by Climate.
:param client_secret: Provided by Climate.
:return: Basic authorization header.
"""
pair = '{}:{}'.format(client_id, client_secret)
encoded = b64encode(pair.encode('ascii')).decode('ascii')
return 'Basic {}'.format(encoded)
def authorize(login_code, client_id, client_secret, redirect_uri):
"""
Exchanges the login code provided on the redirect request for an access_token and refresh_token. Also gets user
data.
:param login_code: Authorization code returned from Log In with FieldView on redirect uri.
:param client_id: Provided by Climate.
:param client_secret: Provided by Climate.
:param redirect_uri: Uri to your redirect page. Needs to be the same as the redirect uri provided in the initial
Log In with FieldView request.
:return: Object containing user data, access_token and refresh_token.
"""
headers = {
'authorization': authorization_header(client_id, client_secret),
'content-type': 'application/x-www-form-urlencoded',
'accept': 'application/json'
}
data = {
'grant_type': 'authorization_code',
'redirect_uri': redirect_uri,
'code': login_code
}
res = requests.post(token_uri, headers=headers, data=urlencode(data))
Logger().info(to_curl(res.request))
if res.status_code == 200:
return res.json()
else:
Logger().error("Auth failed: %s" % res.status_code)
Logger().error("Auth failed: %s" % res.json())
def reauthorize(refresh_token, client_id, client_secret):
"""
Access_tokens expire after 4 hours. At any point before the end of that period you may request a new access_token
(and refresh_token) by submitting a POST request to the /api/oauth/token end-point. Note that the data submitted
is slightly different than on initial authorization. Refresh tokens are good for 30 days from their date of issue.
Once this end-point is called, the refresh token that is passed to this call is immediately set to expired one
hour from "now" and the newly issues refresh token will expire 30 days from "now". Make sure to store the new
refresh token so you can use it in the future to get a new auth tokens as needed. If you lose the refresh token
there is no effective way to retrieve a new refresh token without having the user log in again.
:param refresh_token: refresh_token supplied by initial (or subsequent refresh) call.
:param client_id: Provided by Climate.
:param client_secret: Provided by Climate.
:return: Object containing user data, access_token and refresh_token.
"""
headers = {
'authorization': authorization_header(client_id, client_secret),
'content-type': 'application/x-www-form-urlencoded',
'accept': 'application/json'
}
data = {
'grant_type': 'refresh_token',
'refresh_token': refresh_token
}
res = requests.post(token_uri, headers=headers, data=urlencode(data))
Logger().info(to_curl(res.request))
if res.status_code == 200:
return res.json()
def bearer_token(token):
"""
Returns content of authorization header to be provided on all non-auth API calls.
:param token: access_token returned from authorization call.
:return: Formatted header.
"""
return 'Bearer {}'.format(token)
def get_fields(token, api_key, next_token=None):
"""
Retrieve a user's field list from Climate. Note that fields (like most data) is paginated to support very large
data sets. If the status code returned is 206 (partial content), then there is more data to get. The x-next-token
header provides a "marker" that can be used on another request to get the next page of data. Continue fetching
data until the status is 200. Note that x-next-token is based on date modified, so storing x-next-token can used
as a method to fetch updates over longer periods of time (though also note that this will not result in fetching
deleted objects since they no longer appear in lists regardless of their modified date).
:param token: access_token
:param api_key: Provided by Climate.
:param next_token: Pagination token from previous request, or None.
:return: A (possibly empty) list of fields.
"""
uri = '{}/v4/fields'.format(api_uri)
headers = {
'authorization': bearer_token(token),
'accept': json_content_type,
'x-api-key': api_key,
'x-next-token': next_token
}
res = requests.get(uri, headers=headers)
Logger().info(to_curl(res.request))
if res.status_code == 200:
return res.json()['results']
if res.status_code == 206:
next_token = res.headers['x-next-token']
return res.json()['results'] + get_fields(token, api_key, next_token)
else:
return []
def get_boundary(boundary_id, token, api_key):
"""
Retrieve field boundary from Climate. Note that boundary objects are immutable, so whenever a field's boundary is
updated the boundaryId property of the field will change and you will need to fetch the updated boundary.
:param boundary_id: UUID of field boundary to retrieve.
:param token: access_token
:param api_key: Provided by Climate
:return: geojson object representing the boundary of the field.
"""
uri = '{}/v4/boundaries/{}'.format(api_uri, boundary_id)
headers = {
'authorization': bearer_token(token),
'accept': json_content_type,
'x-api-key': api_key
}
res = requests.get(uri, headers=headers)
Logger().info(to_curl(res.request))
if res.status_code == 200:
return res.json()
else:
return None
def upload(f, content_type, token, api_key):
"""Upload a file with the given content type to Climate
This example supports files up to 5 MiB (5,242,880 bytes).
Returns The upload id if the upload is successful, False otherwise.
"""
CHUNK_SIZE = 5 * 1024 * 1024
uri = '{}/v4/uploads'.format(api_uri)
headers = {
'authorization': bearer_token(token),
'x-api-key': api_key
}
md5 = file.md5(f)
length = file.length(f)
data = {
'md5': md5,
'length': length,
'contentType': content_type
}
# initiate upload
res = requests.post(uri, headers=headers, json=data)
Logger().info(to_curl(res.request))
if res.status_code == 201:
upload_id = res.json()
Logger().info("Upload Id: %s" % upload_id)
put_uri = '{}/{}'.format(uri, upload_id)
# for this example, size is assumed to be small enough for a
# single upload (less than or equal to 5 MiB)
headers['content-range'] = 'bytes {}-{}/{}'.format(0, (length - 1), length)
headers['content-type'] = binary_content_type
f.seek(0)
# send image
for position in range(0, length, CHUNK_SIZE):
buf = f.read(CHUNK_SIZE)
headers['content-range'] = 'bytes {}-{}/{}'.format(position, position + len(buf) - 1, length)
try:
res = requests.put(put_uri, headers=headers, data=buf)
Logger().info(headers)
except Exception as e:
Logger().error("Exception: %s" % e)
if res.status_code == 204:
return upload_id
return False