apccapi 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- apccapi/__init__.py +5 -0
- apccapi/apccapi.py +285 -0
- apccapi-0.0.1.dist-info/LICENSE +9 -0
- apccapi-0.0.1.dist-info/METADATA +84 -0
- apccapi-0.0.1.dist-info/RECORD +7 -0
- apccapi-0.0.1.dist-info/WHEEL +5 -0
- apccapi-0.0.1.dist-info/top_level.txt +1 -0
apccapi/__init__.py
ADDED
apccapi/apccapi.py
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import time
|
|
3
|
+
import os
|
|
4
|
+
import requests
|
|
5
|
+
import logging
|
|
6
|
+
import urllib3
|
|
7
|
+
|
|
8
|
+
def read_config(path):
|
|
9
|
+
config = {}
|
|
10
|
+
with open(path) as f:
|
|
11
|
+
for l in f.readlines():
|
|
12
|
+
if '=' in l:
|
|
13
|
+
k, v = l.strip().split('=')
|
|
14
|
+
if k in ('request_url', 'key', 'status_url'):
|
|
15
|
+
config[k] = v.strip()
|
|
16
|
+
return config
|
|
17
|
+
|
|
18
|
+
def download_MME_3MONTH(request, output_zipfile_name):
|
|
19
|
+
client = Client()
|
|
20
|
+
client.retrieve(
|
|
21
|
+
{
|
|
22
|
+
'jobtype': 'MME',
|
|
23
|
+
'dataset': 'MME_3MONTH',
|
|
24
|
+
'type': request['type'],
|
|
25
|
+
'method': request['method'],
|
|
26
|
+
'variable': request['variable'],
|
|
27
|
+
'period': request['period'],
|
|
28
|
+
'yearmonth': request['yearmonth']
|
|
29
|
+
},
|
|
30
|
+
output_zipfile_name
|
|
31
|
+
)
|
|
32
|
+
return output_zipfile_name
|
|
33
|
+
|
|
34
|
+
def download_MME_6MONTH(request, output_zipfile_name):
|
|
35
|
+
client = Client()
|
|
36
|
+
client.retrieve(
|
|
37
|
+
{
|
|
38
|
+
'jobtype': 'MME',
|
|
39
|
+
'dataset': 'MME_6MONTH',
|
|
40
|
+
'type': request['type'],
|
|
41
|
+
'method': request['method'],
|
|
42
|
+
'variable': request['variable'],
|
|
43
|
+
'period': request['period'],
|
|
44
|
+
'yearmonth': request['yearmonth']
|
|
45
|
+
},
|
|
46
|
+
output_zipfile_name
|
|
47
|
+
)
|
|
48
|
+
return output_zipfile_name
|
|
49
|
+
|
|
50
|
+
def download_MME_MODEL(request, output_zipfile_name):
|
|
51
|
+
client = Client()
|
|
52
|
+
client.retrieve(
|
|
53
|
+
{
|
|
54
|
+
'jobtype': 'MODEL',
|
|
55
|
+
'dataset': 'MODEL',
|
|
56
|
+
'type': request['type'],
|
|
57
|
+
'institute': request['institute'],
|
|
58
|
+
'model': request['model'],
|
|
59
|
+
'variable': request['variable'],
|
|
60
|
+
'yearmonth': request['yearmonth']
|
|
61
|
+
},
|
|
62
|
+
output_zipfile_name
|
|
63
|
+
)
|
|
64
|
+
return output_zipfile_name
|
|
65
|
+
|
|
66
|
+
def download_CMIP5(request, output_zipfile_name):
|
|
67
|
+
client = Client()
|
|
68
|
+
client.retrieve(
|
|
69
|
+
{
|
|
70
|
+
'jobtype': 'CMIP5',
|
|
71
|
+
'dataset': 'CMIP5',
|
|
72
|
+
'code': request['code']
|
|
73
|
+
},
|
|
74
|
+
output_zipfile_name
|
|
75
|
+
)
|
|
76
|
+
return output_zipfile_name
|
|
77
|
+
|
|
78
|
+
class Client(object):
|
|
79
|
+
logger = logging.getLogger('apccapi')
|
|
80
|
+
|
|
81
|
+
def __init__(self,
|
|
82
|
+
debug=False,
|
|
83
|
+
sleep_max=120,
|
|
84
|
+
retry_max=500,
|
|
85
|
+
timeout=60):
|
|
86
|
+
|
|
87
|
+
self.sleep_max = sleep_max
|
|
88
|
+
self.retry_max = retry_max
|
|
89
|
+
self.timeout = timeout
|
|
90
|
+
self.last_status = None
|
|
91
|
+
self.target = None
|
|
92
|
+
|
|
93
|
+
if debug:
|
|
94
|
+
level = logging.DEBUG
|
|
95
|
+
else:
|
|
96
|
+
level = logging.INFO
|
|
97
|
+
|
|
98
|
+
logging.basicConfig(level=level,
|
|
99
|
+
format='[%(asctime)s] [%(levelname)s] %(message)s')
|
|
100
|
+
|
|
101
|
+
self.requesturl = None
|
|
102
|
+
self.key = None
|
|
103
|
+
self.statusurl = None
|
|
104
|
+
|
|
105
|
+
urllib3.disable_warnings()
|
|
106
|
+
|
|
107
|
+
propertyFile = os.path.join(os.path.expanduser('~'), 'apccapi.properties')
|
|
108
|
+
self.debug(propertyFile)
|
|
109
|
+
|
|
110
|
+
if self.requesturl is None or self.key is None or self.statusurl is None:
|
|
111
|
+
if os.path.exists(propertyFile):
|
|
112
|
+
config = read_config(propertyFile)
|
|
113
|
+
|
|
114
|
+
self.key = config.get('key')
|
|
115
|
+
self.requesturl = config.get('request_url')
|
|
116
|
+
self.statusurl = config.get('status_url')
|
|
117
|
+
|
|
118
|
+
if self.requesturl is None or self.key is None or self.statusurl is None:
|
|
119
|
+
raise Exception('Missing/incomplete properties file: %s' % (propertyFile))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def retrieve(self, details, target):
|
|
123
|
+
headers = {'Content-Type': 'application/json; charset-utf-8'}
|
|
124
|
+
request = {}
|
|
125
|
+
request['key'] = self.key
|
|
126
|
+
request['details'] = details
|
|
127
|
+
|
|
128
|
+
self.target = target
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
response = self.post_api(self.requesturl, request)
|
|
132
|
+
print(response)
|
|
133
|
+
reply = response.json()
|
|
134
|
+
status_code = reply['status']
|
|
135
|
+
message = reply['message']
|
|
136
|
+
except Exception:
|
|
137
|
+
# self.debug(response.text)
|
|
138
|
+
raise
|
|
139
|
+
|
|
140
|
+
if status_code > 202:
|
|
141
|
+
info = reply['data']['info']
|
|
142
|
+
self.error(message + "-" + info)
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
sleep = 0
|
|
146
|
+
jobid = None
|
|
147
|
+
|
|
148
|
+
while True:
|
|
149
|
+
|
|
150
|
+
self.debug("REPLY %s", reply)
|
|
151
|
+
|
|
152
|
+
if sleep == 0:
|
|
153
|
+
jobid = reply['data']['id']
|
|
154
|
+
userid = reply['data']['userid']
|
|
155
|
+
self.info('Hello ' + userid + ".")
|
|
156
|
+
self.info('Your job id is ' + jobid)
|
|
157
|
+
|
|
158
|
+
if reply['data']['status'] != self.last_status:
|
|
159
|
+
self.info("Request is %s" % (reply['data']['status'],))
|
|
160
|
+
self.last_status = reply['data']['status']
|
|
161
|
+
|
|
162
|
+
if reply['data']['status'] == 'Complete':
|
|
163
|
+
self.debug("Done")
|
|
164
|
+
result = self.download(reply['data']['download_url'], self.target)
|
|
165
|
+
if result is False:
|
|
166
|
+
self.error('Failed to download result file.')
|
|
167
|
+
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
if reply['data']['status'] == 'Queued' or reply['data']['status'] == 'Running':
|
|
171
|
+
self.debug("Request ID is %s, sleep %s", jobid, sleep)
|
|
172
|
+
time.sleep(3)
|
|
173
|
+
sleep += 3;
|
|
174
|
+
|
|
175
|
+
if sleep > self.sleep_max:
|
|
176
|
+
sleep = self.sleep_max
|
|
177
|
+
|
|
178
|
+
status_url = '%s/%s' % (self.statusurl, jobid)
|
|
179
|
+
self.debug("GET %s", status_url)
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
response = self.get_api(status_url)
|
|
183
|
+
reply = response.json()
|
|
184
|
+
except Exception:
|
|
185
|
+
self.debug("Failed to get response. Retrying");
|
|
186
|
+
|
|
187
|
+
continue
|
|
188
|
+
|
|
189
|
+
if reply['data']['status'] == "Failed":
|
|
190
|
+
print(reply['data']['message'])
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
raise Exception('Unknown API state [%s]' % reply['data']['status'])
|
|
194
|
+
|
|
195
|
+
def download(self, url, target):
|
|
196
|
+
self.info('Start to save file - ' + target)
|
|
197
|
+
|
|
198
|
+
tries = 0
|
|
199
|
+
|
|
200
|
+
while tries < self.retry_max:
|
|
201
|
+
try:
|
|
202
|
+
response = requests.get(url, verify=False, allow_redirects=True)
|
|
203
|
+
open(target, 'wb').write(response.content)
|
|
204
|
+
except requests.exceptions.ConnectionError as e:
|
|
205
|
+
self.warning("Recovering from connection error [%s], attemps %s of %s",
|
|
206
|
+
response.status_code, response.reason, tries, self.retry_max)
|
|
207
|
+
|
|
208
|
+
if os.path.exists(target):
|
|
209
|
+
self.info('Done')
|
|
210
|
+
return True
|
|
211
|
+
|
|
212
|
+
tries += 1
|
|
213
|
+
|
|
214
|
+
self.warning("Retrying in %s seconds", self.sleep_max)
|
|
215
|
+
time.sleep(self.sleep_max)
|
|
216
|
+
self.info("Retrying now...")
|
|
217
|
+
|
|
218
|
+
return False
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def get_api(self, url):
|
|
222
|
+
tries = 0
|
|
223
|
+
|
|
224
|
+
while tries < self.retry_max:
|
|
225
|
+
try:
|
|
226
|
+
response = requests.get(url, verify=False)
|
|
227
|
+
except requests.exceptions.ConnectionError as e:
|
|
228
|
+
response = None
|
|
229
|
+
self.warning("Recovering from connection error [%s], attemps %s of %s",
|
|
230
|
+
response.status_code, response.reason, tries, self.retry_max)
|
|
231
|
+
|
|
232
|
+
if response is not None:
|
|
233
|
+
if response.status_code == 202 or response.status_code == 200:
|
|
234
|
+
return response
|
|
235
|
+
else:
|
|
236
|
+
self.error('Failed to get status of the job.')
|
|
237
|
+
self.error(response['data']['info'])
|
|
238
|
+
|
|
239
|
+
tries += 1
|
|
240
|
+
|
|
241
|
+
self.warning("Retrying in %s seconds", self.sleep_max)
|
|
242
|
+
time.sleep(self.sleep_max)
|
|
243
|
+
self.info("Retrying now...")
|
|
244
|
+
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def post_api(self, url, request):
|
|
249
|
+
headers = {'Content-Type': 'application/json; charset-utf-8'}
|
|
250
|
+
tries = 0
|
|
251
|
+
|
|
252
|
+
while tries < self.retry_max:
|
|
253
|
+
try:
|
|
254
|
+
response = requests.post(url, verify=False, headers=headers, data=json.dumps(request))
|
|
255
|
+
except requests.exceptions.ConnectionError as e:
|
|
256
|
+
response = None
|
|
257
|
+
self.warning("Recovering from connection error [%s], attemps %s of %s",
|
|
258
|
+
e, tries, self.retry_max)
|
|
259
|
+
|
|
260
|
+
if response is not None:
|
|
261
|
+
if response.status_code > 202:
|
|
262
|
+
self.warning("Recovering from HTTP error [%s %s], attemps %s of %s",
|
|
263
|
+
response.status_code, response.reason, tries, self.retry_max)
|
|
264
|
+
else:
|
|
265
|
+
return response
|
|
266
|
+
|
|
267
|
+
tries += 1
|
|
268
|
+
|
|
269
|
+
self.warning("Retrying in %s seconds", self.sleep_max)
|
|
270
|
+
time.sleep(self.sleep_max)
|
|
271
|
+
self.info("Retrying now...")
|
|
272
|
+
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
def info(self, *args, **kwargs):
|
|
276
|
+
self.logger.info(*args, **kwargs)
|
|
277
|
+
|
|
278
|
+
def warning(self, *args, **kwargs):
|
|
279
|
+
self.logger.warning(*args, **kwargs)
|
|
280
|
+
|
|
281
|
+
def error(self, *args, **kwargs):
|
|
282
|
+
self.logger.error(*args, **kwargs)
|
|
283
|
+
|
|
284
|
+
def debug(self, *args, **kwargs):
|
|
285
|
+
self.logger.debug(*args, **kwargs)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2019 - 2024, APEC Climate Center
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: apccapi
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: APCC Climate Information toolKit API package
|
|
5
|
+
Home-page: https://www.apcc21.org
|
|
6
|
+
Author: APEC Climate Center
|
|
7
|
+
Author-email: joohyung@apcc21.org
|
|
8
|
+
Project-URL: CLIK, https://cliks.apcc21.org
|
|
9
|
+
Keywords: APCC,apcc,Climate,Climate Information,Climate API
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.6
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: requests
|
|
17
|
+
Requires-Dist: urllib3
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
Install via pip with:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
$ pip install apccapi
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Configure (Linux)
|
|
28
|
+
|
|
29
|
+
Obtain your API key from the CLIK (Climate Information toolKit) at https://cliks.apcc21.org and enter it into the apccapi.properties file, formatting it as follows:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
$ vi ~/.apccapi.properties
|
|
33
|
+
|
|
34
|
+
key={your API key}
|
|
35
|
+
request_url=https://request.apcc21.org/apccdata
|
|
36
|
+
status_url=https://request.apcc21.org/status
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Configure (Windows)
|
|
40
|
+
|
|
41
|
+
Obtain your API key from the CLIK (Climate Information toolKit) at https://cliks.apcc21.org, and create the apccapi.properties file in your Windows Home directory (typically C:\Users\YourUserName) with the following format:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
key={your API key}
|
|
45
|
+
request_url=https://request.apcc21.org/apccdata
|
|
46
|
+
status_url=https://request.apcc21.org/status
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Test
|
|
50
|
+
|
|
51
|
+
Perform a small test retrieve of MME data:
|
|
52
|
+
```
|
|
53
|
+
$ python
|
|
54
|
+
>>> import apccapi
|
|
55
|
+
>>> c = apccapi.Client()
|
|
56
|
+
>>> c.retrieve(
|
|
57
|
+
{
|
|
58
|
+
'jobtype': 'MME',
|
|
59
|
+
'dataset': 'MME_3MONTH',
|
|
60
|
+
'type': 'FORECAST',
|
|
61
|
+
'method': 'SCM',
|
|
62
|
+
'variable': ['prec', 't2m'],
|
|
63
|
+
'period': ['Monthly mean'],
|
|
64
|
+
'yearmonth': ['201909']
|
|
65
|
+
},
|
|
66
|
+
'mme3.zip'
|
|
67
|
+
)
|
|
68
|
+
>>>
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## License
|
|
73
|
+
|
|
74
|
+
MIT License
|
|
75
|
+
|
|
76
|
+
Copyright (c) 2019 - 2024, APEC Climate Center
|
|
77
|
+
|
|
78
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
79
|
+
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
80
|
+
|
|
81
|
+
The above copyright notice and this permission notice shall be included in all
|
|
82
|
+
copies or substantial portions of the Software.
|
|
83
|
+
|
|
84
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
apccapi/__init__.py,sha256=-w95Jv18HG14Pz0EI7PNybeqv3qN2LguI2ahq-na8GU,122
|
|
2
|
+
apccapi/apccapi.py,sha256=OxCjGbz0kXYxHtryo8clWHoRok6_Uzm8fmW6IuhOpdo,7072
|
|
3
|
+
apccapi-0.0.1.dist-info/LICENSE,sha256=bJx5OeJ0W0ARQFMPTTojbOWSGaq6QqW1kquNZs-NY28,1091
|
|
4
|
+
apccapi-0.0.1.dist-info/METADATA,sha256=-ZFflXZMOuwhHT3hSbkHFvrDplzDF9Lel6wObbqhLmc,2877
|
|
5
|
+
apccapi-0.0.1.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92
|
|
6
|
+
apccapi-0.0.1.dist-info/top_level.txt,sha256=TbdOgbug3IVPe2wuPgmutaHU62n1TC4xmnqdC1rkLH0,8
|
|
7
|
+
apccapi-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
apccapi
|