p2api 1.0.6__py2.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.
- p2api/__about__.py +1 -0
- p2api/__init__.py +5 -0
- p2api/p2api.py +1808 -0
- p2api/utils/__init__.py +1 -0
- p2api/utils/add_absolute_time_intervals.py +399 -0
- p2api/utils/generate_finding_charts.py +260 -0
- p2api/utils/p2api_utils.py +304 -0
- p2api-1.0.6.dist-info/METADATA +61 -0
- p2api-1.0.6.dist-info/RECORD +12 -0
- p2api-1.0.6.dist-info/WHEEL +5 -0
- p2api-1.0.6.dist-info/entry_points.txt +3 -0
- p2api-1.0.6.dist-info/licenses/LICENSE +21 -0
p2api/p2api.py
ADDED
|
@@ -0,0 +1,1808 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This file provides an example binding to ESO's RESTful phase 2 programmatic interface.
|
|
3
|
+
The data required and returned by many of the API calls is instrument-specific.
|
|
4
|
+
In order to fully understand the data and behaviour of the API please consult
|
|
5
|
+
the documentation at https://www.eso.org/copdemo/apidoc/
|
|
6
|
+
The api URL specified with each API should help to locate the corresponding
|
|
7
|
+
API documentation at the above URL.
|
|
8
|
+
|
|
9
|
+
Unless otherwise mentioned, each API call consistently returns a tuple (data, version):
|
|
10
|
+
* data - returned data in JSON format
|
|
11
|
+
* version - version of the data, required for future modifications (HTTP ETags are used for this versioning)
|
|
12
|
+
|
|
13
|
+
___________________________________________________________________________
|
|
14
|
+
MIT License
|
|
15
|
+
___________________________________________________________________________
|
|
16
|
+
Copyright (c) 2021 Thomas Bierwirth, European Southern Observatory
|
|
17
|
+
|
|
18
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
19
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
20
|
+
in the Software without restriction, including without limitation the rights
|
|
21
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
22
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
23
|
+
furnished to do so, subject to the following conditions:
|
|
24
|
+
|
|
25
|
+
The above copyright notice and this permission notice shall be included in all
|
|
26
|
+
copies or substantial portions of the Software.
|
|
27
|
+
|
|
28
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
29
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
30
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
31
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
32
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
33
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
34
|
+
SOFTWARE.
|
|
35
|
+
"""
|
|
36
|
+
from __future__ import print_function
|
|
37
|
+
import os
|
|
38
|
+
import io
|
|
39
|
+
import json
|
|
40
|
+
import requests
|
|
41
|
+
import re
|
|
42
|
+
|
|
43
|
+
## The hatchling way...
|
|
44
|
+
try:
|
|
45
|
+
from p2api.__about__ import __version__
|
|
46
|
+
except:
|
|
47
|
+
# Need this construct to run in VSCode
|
|
48
|
+
from __about__ import __version__
|
|
49
|
+
P2_API_VERSION = __version__
|
|
50
|
+
|
|
51
|
+
# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
52
|
+
API_URL = {
|
|
53
|
+
'production' : 'https://www.eso.org/cop/api/v1',
|
|
54
|
+
'production_lasilla': 'https://www.eso.org/copls/api/v1',
|
|
55
|
+
'demo' : 'https://www.eso.org/copdemo/api/v1',
|
|
56
|
+
}
|
|
57
|
+
LOGIN_URL = {
|
|
58
|
+
'production' : 'https://www.eso.org/cop/api/login',
|
|
59
|
+
'production_lasilla': 'https://www.eso.org/copls/api/login',
|
|
60
|
+
'demo' : 'https://www.eso.org/copdemo/api/login',
|
|
61
|
+
}
|
|
62
|
+
P2FC_URL = {
|
|
63
|
+
'production' : 'https://www.eso.org/p2fc/',
|
|
64
|
+
'production_lasilla': 'https://www.eso.org/p2fc/',
|
|
65
|
+
'demo' : 'https://www.eso.org/p2fcdemo/',
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
class P2Error(Exception):
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
class ApiConnection(object):
|
|
72
|
+
# ---------- PUBLIC API ----------
|
|
73
|
+
def __init__(self, *args, **kwargs) :
|
|
74
|
+
#def __init__(self, environment, username, password, debug=False):
|
|
75
|
+
"""
|
|
76
|
+
Set environment, log in to the API and obtain access token for further
|
|
77
|
+
calls. Returns authenticated api connection required to invoke other
|
|
78
|
+
API calls.
|
|
79
|
+
|
|
80
|
+
Supported environments are
|
|
81
|
+
- 'production' : for Paranal Observation Preparation
|
|
82
|
+
- 'production_lasilla' : for La Silla Observation Preparation
|
|
83
|
+
- 'demo' : to experiment with both Paranal and La Silla
|
|
84
|
+
|
|
85
|
+
usage:
|
|
86
|
+
::
|
|
87
|
+
|
|
88
|
+
api = p2api.ApiConnection('demo', '52052', 'tutorial')
|
|
89
|
+
api = p2api.ApiConnection('demo', '52052', 'tutorial', True)
|
|
90
|
+
api = p2api.ApiConnection(environment='demo', username='52052', password='tutorial', debug=True)
|
|
91
|
+
or
|
|
92
|
+
api = p2api.ApiConnection(url='https://www.eso.org/copdemo/api/v1', access_token='<access_token>')
|
|
93
|
+
api = p2api.ApiConnection(url='https://www.eso.org/copdemo/api/v1/obsBlocks/{obId}', access_token='<access_token>')
|
|
94
|
+
api = p2api.ApiConnection(url='https://www.eso.org/copdemo/api/v1/containers/{containerId}', access_token='<access_token>')
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
"""
|
|
98
|
+
if 'environment' in kwargs or len(args) > 0 :
|
|
99
|
+
# Define parameters...
|
|
100
|
+
param_values={}
|
|
101
|
+
required_params=[
|
|
102
|
+
'environment',
|
|
103
|
+
'username',
|
|
104
|
+
'password',
|
|
105
|
+
]
|
|
106
|
+
optional_params=[
|
|
107
|
+
'debug',
|
|
108
|
+
]
|
|
109
|
+
optional_param_defaults=[
|
|
110
|
+
False,
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
# Set parameter values from provided arguments...
|
|
114
|
+
for i,k in enumerate(args) :
|
|
115
|
+
param_values[(required_params+optional_params)[i]]=args[i]
|
|
116
|
+
|
|
117
|
+
# Check all required parameters have been specified
|
|
118
|
+
required_params_not_specified=[]
|
|
119
|
+
for k in required_params :
|
|
120
|
+
if k not in param_values.keys() :
|
|
121
|
+
if k in kwargs.keys() :
|
|
122
|
+
param_values[k]=kwargs[k]
|
|
123
|
+
else :
|
|
124
|
+
required_params_not_specified+=['required parameter %s not specified' %(k),]
|
|
125
|
+
if required_params_not_specified != [] :
|
|
126
|
+
raise P2Error(
|
|
127
|
+
500,
|
|
128
|
+
'POST',
|
|
129
|
+
', '.join(required_params_not_specified)
|
|
130
|
+
,
|
|
131
|
+
)
|
|
132
|
+
# Set default values for optional parameters not specified
|
|
133
|
+
for i,k in enumerate(optional_params) :
|
|
134
|
+
if k not in param_values.keys() :
|
|
135
|
+
param_values[k]=optional_param_defaults[i]
|
|
136
|
+
# Parameters now set...
|
|
137
|
+
|
|
138
|
+
self.debug=param_values['debug']
|
|
139
|
+
self.request_count = 0
|
|
140
|
+
if param_values['environment'] not in API_URL.keys() :
|
|
141
|
+
raise P2Error(500, 'POST', param_values['environment'], 'environment not supported')
|
|
142
|
+
self.apiUrl = API_URL[param_values['environment']]
|
|
143
|
+
self.loginUrl = LOGIN_URL[param_values['environment']]
|
|
144
|
+
self.session = self.requests_retry_session()
|
|
145
|
+
|
|
146
|
+
r = requests.post(
|
|
147
|
+
self.loginUrl,
|
|
148
|
+
data={
|
|
149
|
+
'username': param_values['username'],
|
|
150
|
+
'password': param_values['password'],
|
|
151
|
+
}
|
|
152
|
+
)
|
|
153
|
+
if r.status_code == requests.codes.ok:
|
|
154
|
+
body = r.json()
|
|
155
|
+
self.access_token = body['access_token']
|
|
156
|
+
else:
|
|
157
|
+
raise P2Error(r.status_code, 'POST', self.loginUrl, 'cannot login')
|
|
158
|
+
|
|
159
|
+
elif 'url' in kwargs :
|
|
160
|
+
self.debug = kwargs.get('debug') or False
|
|
161
|
+
self.request_count = 0
|
|
162
|
+
url_is_valid=False
|
|
163
|
+
for environment in API_URL.keys() :
|
|
164
|
+
if API_URL[environment] in kwargs['url'] :
|
|
165
|
+
url_is_valid=True
|
|
166
|
+
break
|
|
167
|
+
if not url_is_valid :
|
|
168
|
+
raise P2Error(500, 'POST', kwargs['url'], 'Invalid URL')
|
|
169
|
+
self.apiUrl = API_URL[environment]
|
|
170
|
+
self.loginUrl = LOGIN_URL[environment]
|
|
171
|
+
self.access_token = kwargs['access_token']
|
|
172
|
+
self.session = self.requests_retry_session()
|
|
173
|
+
else :
|
|
174
|
+
raise P2Error(500, 'POST', LOGIN_URL[kwargs.get('environment','production')], 'Unknown API init method')
|
|
175
|
+
|
|
176
|
+
# ---------- RUN-LEVEL APIs ----------
|
|
177
|
+
"""
|
|
178
|
+
Still pending implementation:
|
|
179
|
+
New end points for various requests:
|
|
180
|
+
/obsRuns/{runId}/request/instrumentChange
|
|
181
|
+
/obsRuns/{runId}/request/targetChange
|
|
182
|
+
/obsRuns/{runId}/request/waiver
|
|
183
|
+
"""
|
|
184
|
+
def getRuns(self):
|
|
185
|
+
"""Retrieve observing runs owned by or delegated to logged-in user.
|
|
186
|
+
|
|
187
|
+
api: GET /obsRuns
|
|
188
|
+
|
|
189
|
+
usage:
|
|
190
|
+
::
|
|
191
|
+
|
|
192
|
+
runs, _ = api.getRuns()
|
|
193
|
+
"""
|
|
194
|
+
return self.get('/obsRuns')
|
|
195
|
+
|
|
196
|
+
def getRun(self, runId):
|
|
197
|
+
"""Get a single observing run.
|
|
198
|
+
|
|
199
|
+
api: GET /obsRuns/{runId}
|
|
200
|
+
|
|
201
|
+
usage:
|
|
202
|
+
::
|
|
203
|
+
|
|
204
|
+
run, _ = api.getRun(runId)
|
|
205
|
+
"""
|
|
206
|
+
return self.get('/obsRuns/%d' % runId)
|
|
207
|
+
|
|
208
|
+
def getPhase1Targets(self, runId):
|
|
209
|
+
"""Retrieve the list of targets defined in phase 1.
|
|
210
|
+
|
|
211
|
+
api: GET /obsRuns/{runId}/phase1/targets
|
|
212
|
+
|
|
213
|
+
usage:
|
|
214
|
+
::
|
|
215
|
+
|
|
216
|
+
targets, _ = api.getPhase1Targets(runId)
|
|
217
|
+
"""
|
|
218
|
+
return self.get('/obsRuns/%d/phase1/targets' % runId)
|
|
219
|
+
|
|
220
|
+
def getBlockedTimes(self, runId):
|
|
221
|
+
"""Retrieve a list of blocked times on the telescope in the
|
|
222
|
+
scheduled period of this observing run.
|
|
223
|
+
|
|
224
|
+
api: GET /obsRuns/{runId}/phase1/blockedTimes
|
|
225
|
+
|
|
226
|
+
usage:
|
|
227
|
+
::
|
|
228
|
+
|
|
229
|
+
targets, _ = api.getBlockedTimes(runId)
|
|
230
|
+
"""
|
|
231
|
+
return self.get('/obsRuns/%d/phase1/blockedTimes' % runId)
|
|
232
|
+
|
|
233
|
+
def submitRun(self, runId):
|
|
234
|
+
"""Submit the observing run to ESO for review.
|
|
235
|
+
|
|
236
|
+
api : POST /obsRuns/{runId}/submit
|
|
237
|
+
|
|
238
|
+
usage:
|
|
239
|
+
::
|
|
240
|
+
|
|
241
|
+
api.submitRun(runId)
|
|
242
|
+
"""
|
|
243
|
+
return self.post('/obsRuns/%d/submit' % runId)
|
|
244
|
+
|
|
245
|
+
def getReadmeSchema(self, runId):
|
|
246
|
+
"""Retrieve the schema definition for the ReadMe file of this observing run.
|
|
247
|
+
|
|
248
|
+
api: GET /obsRuns/{runId}/readme/schema
|
|
249
|
+
|
|
250
|
+
usage:
|
|
251
|
+
::
|
|
252
|
+
|
|
253
|
+
readmeSchema, _ = api.getReadmeSchema(runId)
|
|
254
|
+
"""
|
|
255
|
+
return self.get('/obsRuns/%d/readme/schema' % runId)
|
|
256
|
+
|
|
257
|
+
def getReadme(self, runId):
|
|
258
|
+
"""Retrieve the ReadMe file of this observing run.
|
|
259
|
+
|
|
260
|
+
api: GET /obsRuns/{runId}/readme
|
|
261
|
+
|
|
262
|
+
usage:
|
|
263
|
+
::
|
|
264
|
+
|
|
265
|
+
readme, readmeVersion = api.getReadme(runId)
|
|
266
|
+
"""
|
|
267
|
+
return self.get('/obsRuns/%d/readme' % runId)
|
|
268
|
+
|
|
269
|
+
def saveReadme(self, runId, readme, version):
|
|
270
|
+
"""
|
|
271
|
+
Update the ReadMe file.
|
|
272
|
+
|
|
273
|
+
api: PUT /obsRuns/{runId}/readme.
|
|
274
|
+
|
|
275
|
+
usage:
|
|
276
|
+
::
|
|
277
|
+
|
|
278
|
+
readme, readmeVersion = api.saveReadme(runId, readme, version)
|
|
279
|
+
"""
|
|
280
|
+
return self.put('/obsRuns/%d/readme' % runId, readme, version)
|
|
281
|
+
|
|
282
|
+
# ---------- CONTAINER-LEVEL APIs ----------
|
|
283
|
+
def getContainer(self, containerId):
|
|
284
|
+
"""Get container information.
|
|
285
|
+
|
|
286
|
+
api: GET /containers/{containerId}
|
|
287
|
+
|
|
288
|
+
usage:
|
|
289
|
+
::
|
|
290
|
+
|
|
291
|
+
container, containerVersion = api.getContainer(containerId)
|
|
292
|
+
"""
|
|
293
|
+
return self.get('/containers/%d' % containerId)
|
|
294
|
+
|
|
295
|
+
def saveContainer(self, container, version):
|
|
296
|
+
"""Update container information.
|
|
297
|
+
|
|
298
|
+
api: PUT /containers/{containerId}
|
|
299
|
+
|
|
300
|
+
usage:
|
|
301
|
+
::
|
|
302
|
+
|
|
303
|
+
container, containerVersion = api.getContainer(containerId)
|
|
304
|
+
# modify some container properties ...
|
|
305
|
+
container['userPriority'] = 10
|
|
306
|
+
# save changes
|
|
307
|
+
container, containerVersion = api.saveContainer(container, containerVersion)
|
|
308
|
+
"""
|
|
309
|
+
return self.put('/containers/%d' % container['containerId'], container, version)
|
|
310
|
+
|
|
311
|
+
def deleteContainer(self, containerId, version):
|
|
312
|
+
"""Delete container. The container must be empty.
|
|
313
|
+
|
|
314
|
+
api: DELETE /containers/{containerId}
|
|
315
|
+
|
|
316
|
+
usage:
|
|
317
|
+
::
|
|
318
|
+
|
|
319
|
+
container, containerVersion = api.getContainer(container)
|
|
320
|
+
api.deleteContainer(container['containerId'], containerVersion)
|
|
321
|
+
"""
|
|
322
|
+
return self.delete('/containers/%d' % containerId, etag=version)
|
|
323
|
+
|
|
324
|
+
def createItem(self, itemType, containerId, name):
|
|
325
|
+
"""Create a new OB, CB, Folder, Group, Concatenation or TimeLink
|
|
326
|
+
at the end of the given container.
|
|
327
|
+
|
|
328
|
+
The initial values for the observing constraints of a new OB are those
|
|
329
|
+
specified during phase 1, or the instrument defaults.
|
|
330
|
+
|
|
331
|
+
Visitor Mode: Groups, Concatenations and TimeLinks cannot be created.
|
|
332
|
+
|
|
333
|
+
Service Mode: Groups, Concatenations and TimeLinks cannot contain other
|
|
334
|
+
containers, only OBs and CBs.
|
|
335
|
+
|
|
336
|
+
See also wrappers below to create specific items by name.
|
|
337
|
+
|
|
338
|
+
api: POST /containers/{containerId}/items
|
|
339
|
+
|
|
340
|
+
usage:
|
|
341
|
+
::
|
|
342
|
+
|
|
343
|
+
ob, obVersion = api.createItem('OB', containerId, 'OliOB')
|
|
344
|
+
"""
|
|
345
|
+
return self.post('/containers/%d/items' % containerId, {'itemType': itemType, 'name': name})
|
|
346
|
+
|
|
347
|
+
def createOB(self, containerId, name):
|
|
348
|
+
"""Create a new Observing Block.
|
|
349
|
+
|
|
350
|
+
api: POST /containers/{containerId}/items
|
|
351
|
+
|
|
352
|
+
usage:
|
|
353
|
+
::
|
|
354
|
+
|
|
355
|
+
ob, obVersion = api.createOB(containerId, 'OliOB')
|
|
356
|
+
"""
|
|
357
|
+
return self.createItem('OB', containerId, name)
|
|
358
|
+
|
|
359
|
+
def createCB(self, containerId, name):
|
|
360
|
+
"""Create a new Calibration Block.
|
|
361
|
+
|
|
362
|
+
api : POST /containers/{containerId}/items
|
|
363
|
+
|
|
364
|
+
usage:
|
|
365
|
+
::
|
|
366
|
+
|
|
367
|
+
cb, cbVersion = api.createCB(containerId, 'OliCB')
|
|
368
|
+
"""
|
|
369
|
+
return self.createItem('CB', containerId, name)
|
|
370
|
+
|
|
371
|
+
def createFolder(self, containerId, name):
|
|
372
|
+
"""Create a new Folder.
|
|
373
|
+
|
|
374
|
+
api: POST /containers/{containerId}/items
|
|
375
|
+
|
|
376
|
+
usage:
|
|
377
|
+
::
|
|
378
|
+
|
|
379
|
+
fld, fldVersion = api.createFolder(containerId, 'OliFolder')
|
|
380
|
+
"""
|
|
381
|
+
return self.createItem('Folder', containerId, name)
|
|
382
|
+
|
|
383
|
+
def createGroup(self, containerId, name):
|
|
384
|
+
"""Create a new Group.
|
|
385
|
+
|
|
386
|
+
api: POST /containers/{containerId}/items
|
|
387
|
+
|
|
388
|
+
usage:
|
|
389
|
+
::
|
|
390
|
+
|
|
391
|
+
grp, grpVersion = api.createGroup(containerId, 'OliGroup')
|
|
392
|
+
"""
|
|
393
|
+
return self.createItem('Group', containerId, name)
|
|
394
|
+
|
|
395
|
+
def createConcatenation(self, containerId, name):
|
|
396
|
+
"""Create a new Concatenation.
|
|
397
|
+
|
|
398
|
+
api: POST /containers/{containerId}/items
|
|
399
|
+
|
|
400
|
+
usage:
|
|
401
|
+
::
|
|
402
|
+
|
|
403
|
+
con, conVersion = api.createConcatenation(containerId, 'OliConcatenation')
|
|
404
|
+
"""
|
|
405
|
+
return self.createItem('Concatenation', containerId, name)
|
|
406
|
+
|
|
407
|
+
def createTimeLink(self, containerId, name):
|
|
408
|
+
"""Create a new TimeLink
|
|
409
|
+
|
|
410
|
+
api: POST /containers/{containerId}/items
|
|
411
|
+
|
|
412
|
+
usage:
|
|
413
|
+
::
|
|
414
|
+
|
|
415
|
+
tl, tlVersion = api.createTimeLink(containerId, 'OliTimeLink')
|
|
416
|
+
"""
|
|
417
|
+
return self.createItem('TimeLink', containerId, name)
|
|
418
|
+
|
|
419
|
+
def getItems(self, containerId):
|
|
420
|
+
""" Get ordered list of OBs and containers inside a container.
|
|
421
|
+
|
|
422
|
+
api: GET /containers/{containerId}/items
|
|
423
|
+
|
|
424
|
+
usage:
|
|
425
|
+
::
|
|
426
|
+
|
|
427
|
+
items, itemsVersion = api.getItems(containerId)
|
|
428
|
+
"""
|
|
429
|
+
return self.get('/containers/%d/items' % containerId)
|
|
430
|
+
|
|
431
|
+
def reorderItems(self, containerId, items, version):
|
|
432
|
+
"""Change order of OBs and containers inside a container.
|
|
433
|
+
|
|
434
|
+
api: PUT /containers/{containerId}/items
|
|
435
|
+
|
|
436
|
+
usage:
|
|
437
|
+
::
|
|
438
|
+
|
|
439
|
+
items, itemsVersion = api.getItems(containerId)
|
|
440
|
+
# modify items order ...
|
|
441
|
+
# save changes
|
|
442
|
+
items, itemsVersion = api.reorderItems(containerId, items, itemsVersion)
|
|
443
|
+
"""
|
|
444
|
+
return self.put('/containers/%d/items' % containerId, items, version)
|
|
445
|
+
|
|
446
|
+
def moveFolder(self, folderId, containerId):
|
|
447
|
+
"""Move existing folder into destination container.
|
|
448
|
+
|
|
449
|
+
api: POST /containers/{containerId}/items/append
|
|
450
|
+
|
|
451
|
+
usage:
|
|
452
|
+
::
|
|
453
|
+
|
|
454
|
+
api.moveFolder(folderId, containerId)
|
|
455
|
+
"""
|
|
456
|
+
return self.post('/containers/%d/items/append' % containerId,
|
|
457
|
+
{'itemType': 'Folder', 'containerId': folderId})
|
|
458
|
+
|
|
459
|
+
def moveGroup(self, groupId, containerId):
|
|
460
|
+
"""Move existing group into destination container.
|
|
461
|
+
|
|
462
|
+
api: POST /containers/{containerId}/items/append
|
|
463
|
+
|
|
464
|
+
usage:
|
|
465
|
+
::
|
|
466
|
+
|
|
467
|
+
api.moveGroup(groupId, containerId)
|
|
468
|
+
"""
|
|
469
|
+
return self.post('/containers/%d/items/append' % containerId,
|
|
470
|
+
{'itemType': 'Group', 'containerId': groupId})
|
|
471
|
+
|
|
472
|
+
def moveConcatenation(self, concatenationId, containerId):
|
|
473
|
+
"""Move existing concatenation into destination container.
|
|
474
|
+
|
|
475
|
+
api: POST /containers/{containerId}/items/append
|
|
476
|
+
|
|
477
|
+
usage:
|
|
478
|
+
::
|
|
479
|
+
|
|
480
|
+
api.moveConcatenation(concatenationId, containerId)
|
|
481
|
+
"""
|
|
482
|
+
return self.post('/containers/%d/items/append' % containerId,
|
|
483
|
+
{'itemType': 'Concatenation', 'containerId': concatenationId})
|
|
484
|
+
|
|
485
|
+
def moveTimeLink(self, timeLinkId, containerId):
|
|
486
|
+
"""Move existing timelink into destination container.
|
|
487
|
+
|
|
488
|
+
api: POST /containers/{containerId}/items/append
|
|
489
|
+
|
|
490
|
+
usage:
|
|
491
|
+
::
|
|
492
|
+
|
|
493
|
+
api.moveTimeLink(folderId, containerId)
|
|
494
|
+
"""
|
|
495
|
+
return self.post('/containers/%d/items/append' % containerId,
|
|
496
|
+
{'itemType': 'TimeLink', 'containerId': timeLinkId})
|
|
497
|
+
|
|
498
|
+
def moveOB(self, obId, containerId):
|
|
499
|
+
"""Move existing OB into destination container.
|
|
500
|
+
|
|
501
|
+
api: POST /containers/{containerId}/items/append
|
|
502
|
+
|
|
503
|
+
usage:
|
|
504
|
+
::
|
|
505
|
+
|
|
506
|
+
api.moveOB(obId, containerId)
|
|
507
|
+
"""
|
|
508
|
+
return self.post('/containers/%d/items/append' % containerId,
|
|
509
|
+
{'itemType': 'OB', 'obId': obId})
|
|
510
|
+
|
|
511
|
+
def moveCB(self, obId, containerId):
|
|
512
|
+
"""Move existing CB into destination container.
|
|
513
|
+
|
|
514
|
+
api: POST /containers/{containerId}/items/append
|
|
515
|
+
|
|
516
|
+
usage:
|
|
517
|
+
::
|
|
518
|
+
|
|
519
|
+
api.moveCB(obId, containerId)
|
|
520
|
+
"""
|
|
521
|
+
return self.post('/containers/%d/items/append' % containerId,
|
|
522
|
+
{'itemType': 'CB', 'obId': obId})
|
|
523
|
+
|
|
524
|
+
def getOBSummary(self, containerId):
|
|
525
|
+
"""Get more detailed summary of all OBs in a container
|
|
526
|
+
|
|
527
|
+
api: GET /containers/{containerId}/items/summary
|
|
528
|
+
|
|
529
|
+
usage:
|
|
530
|
+
::
|
|
531
|
+
|
|
532
|
+
api.getOBSummary(containerId)
|
|
533
|
+
"""
|
|
534
|
+
return self.get('/containers/%d/items/summary' % containerId)
|
|
535
|
+
|
|
536
|
+
def verifyContainer(self, containerId, submit):
|
|
537
|
+
"""Verify a Group, Concatenation or TimeLink.
|
|
538
|
+
|
|
539
|
+
If submit is false, a Group, Concatenation, or TimeLink can be verified
|
|
540
|
+
in any container status. If submit is true, the container must have
|
|
541
|
+
container status "-" or "P".
|
|
542
|
+
|
|
543
|
+
OBs inside the container must be verified first, i.e. they have a status
|
|
544
|
+
of "D" or later.
|
|
545
|
+
|
|
546
|
+
The instrument-specific verification scripts for a container are executed.
|
|
547
|
+
If submit is true and verification is successful, the container status
|
|
548
|
+
is changed to "D".
|
|
549
|
+
|
|
550
|
+
A change of the container status will invalidate the ETag of the container.
|
|
551
|
+
|
|
552
|
+
api: POST /containers/{containerId}/verify
|
|
553
|
+
|
|
554
|
+
usage:
|
|
555
|
+
::
|
|
556
|
+
|
|
557
|
+
response, _ = api.verifyContainer(containerId, True)
|
|
558
|
+
if response['observable']:
|
|
559
|
+
print('Your container is observable!')
|
|
560
|
+
else:
|
|
561
|
+
print('Your container is >>not observable<<. See messages below.')
|
|
562
|
+
print(' ')
|
|
563
|
+
print(' '.join(response['messages']))
|
|
564
|
+
"""
|
|
565
|
+
return self.post('/containers/%d/verify' % containerId, {'submit': submit})
|
|
566
|
+
|
|
567
|
+
def reviseContainer(self, containerId):
|
|
568
|
+
"""Open Group/Concatenation/TimeLink for further changes.
|
|
569
|
+
|
|
570
|
+
Change the container status back to "P".
|
|
571
|
+
This is the counterpart to verification with submission.
|
|
572
|
+
|
|
573
|
+
api: POST /containers/{containerId}/revise
|
|
574
|
+
|
|
575
|
+
usage:
|
|
576
|
+
::
|
|
577
|
+
|
|
578
|
+
api.reviseContainer(containerId)
|
|
579
|
+
"""
|
|
580
|
+
return self.post('/containers/%d/revise' % containerId)
|
|
581
|
+
|
|
582
|
+
def exportContainer(self, containerId, filename):
|
|
583
|
+
"""Export this container into a ZIP file.
|
|
584
|
+
|
|
585
|
+
api: GET /containers/{containerId}/export
|
|
586
|
+
|
|
587
|
+
usage:
|
|
588
|
+
::
|
|
589
|
+
|
|
590
|
+
api.exportContainer(containerId, 'myContainer.zip')
|
|
591
|
+
"""
|
|
592
|
+
self.downloadBinaryFile('/containers/%d/export' % containerId, filename)
|
|
593
|
+
|
|
594
|
+
# ---------- OB-LEVEL APIs ----------
|
|
595
|
+
def getOB(self, obId):
|
|
596
|
+
"""Get existing OB or CB.
|
|
597
|
+
|
|
598
|
+
api: GET /obsBlocks/{obId}
|
|
599
|
+
|
|
600
|
+
usage:
|
|
601
|
+
::
|
|
602
|
+
|
|
603
|
+
ob, obVersion = api.getOB(obId)
|
|
604
|
+
"""
|
|
605
|
+
return self.get('/obsBlocks/%d' % obId)
|
|
606
|
+
|
|
607
|
+
def saveOB(self, ob, version):
|
|
608
|
+
"""Update existing OB or CB.
|
|
609
|
+
|
|
610
|
+
The format of an OB is instrument-specific and differs for OBs and CBs.
|
|
611
|
+
The instrument defines the applicable observingConstraints.
|
|
612
|
+
A CB does not have a target. The properties itemType, obId, obStatus,
|
|
613
|
+
ipVersion, exposureTime and executionTime cannot be changed.
|
|
614
|
+
The OB must have the latest ipVersion. Refer to the API spec
|
|
615
|
+
for further updating constraints.
|
|
616
|
+
|
|
617
|
+
api: PUT /obsBlocks/{obId}
|
|
618
|
+
|
|
619
|
+
usage:
|
|
620
|
+
::
|
|
621
|
+
|
|
622
|
+
ob, obVersion = api.getOB(obId)
|
|
623
|
+
# modify some OB properties ...
|
|
624
|
+
ob['userPriority'] = 42
|
|
625
|
+
ob['target']['name'] = 'M 32 -- Interacting Galaxies'
|
|
626
|
+
ob['target']['ra'] = '00:42:41.825'
|
|
627
|
+
ob['target']['dec'] = '-60:51:54.610'
|
|
628
|
+
ob['constraints']['name'] = 'My hardest constraints ever'
|
|
629
|
+
ob['constraints']['airmass'] = 1.3
|
|
630
|
+
# save changes
|
|
631
|
+
ob, obVersion = api.saveOB(ob, obVersion)
|
|
632
|
+
"""
|
|
633
|
+
return self.put('/obsBlocks/%d' % ob['obId'], ob, version)
|
|
634
|
+
|
|
635
|
+
def deleteOB(self, obId, version):
|
|
636
|
+
"""Delete existing OB or CB.
|
|
637
|
+
|
|
638
|
+
Visitor Mode: The OB must have never been executed.
|
|
639
|
+
|
|
640
|
+
Service Mode: The OB must have status (-), (P) or (D) and must have never been executed.
|
|
641
|
+
|
|
642
|
+
api: DELETE /obsBlocks/{obId}
|
|
643
|
+
|
|
644
|
+
usage:
|
|
645
|
+
::
|
|
646
|
+
|
|
647
|
+
ob, obVersion = api.getOB(obId)
|
|
648
|
+
api.deleteOB(ob, obVersion)
|
|
649
|
+
"""
|
|
650
|
+
return self.delete('/obsBlocks/%d' % obId, etag=version)
|
|
651
|
+
|
|
652
|
+
def verifyOB(self, obId, submit):
|
|
653
|
+
"""Verify OB and submit it for observation.
|
|
654
|
+
|
|
655
|
+
If submit is false, an OB can be verified in any status. If submit is
|
|
656
|
+
true, a loose service mode OB must be in status "P" or "-" and a visitor
|
|
657
|
+
mode OB must be in a status other than "S", "A", "X", or "+".
|
|
658
|
+
|
|
659
|
+
api: POST /obsBlocks/{obId}/verify
|
|
660
|
+
|
|
661
|
+
usage:
|
|
662
|
+
::
|
|
663
|
+
|
|
664
|
+
response, _ = api.verifyOB(obId, True)
|
|
665
|
+
if response['observable']:
|
|
666
|
+
print('Your OB' , obId, ob['name'], 'is observable!')
|
|
667
|
+
else:
|
|
668
|
+
print('OB', obId, 'is >>not observable<<. See messages below.')
|
|
669
|
+
print(' ')
|
|
670
|
+
print(' '.join(response['messages']))
|
|
671
|
+
"""
|
|
672
|
+
return self.post('/obsBlocks/%d/verify' % obId, {'submit': submit})
|
|
673
|
+
|
|
674
|
+
def reviseOB(self, obId):
|
|
675
|
+
"""Back to the drawing board.
|
|
676
|
+
|
|
677
|
+
Change the OB status back to "P", i.e. mark it as not observable.
|
|
678
|
+
This is the counterpart to verification with submission.
|
|
679
|
+
|
|
680
|
+
A visitor mode OB can be in any status. The status is changed to "P".
|
|
681
|
+
The OB will be removed from any visitor execution sequences because
|
|
682
|
+
it is no longer in an observable status; this changes the ETag of
|
|
683
|
+
the affected visitor execution sequences.
|
|
684
|
+
|
|
685
|
+
A service mode OB can only be revised in status "D". The status is
|
|
686
|
+
changed to "P". If the OB is a member of a Group, Concatenation or
|
|
687
|
+
TimeLink with container status other than "-" or "P", the container
|
|
688
|
+
status will be changed to "P".
|
|
689
|
+
|
|
690
|
+
This call invalidates the ETag of the OB, and possibly of the parent
|
|
691
|
+
container, if the container status was changed.
|
|
692
|
+
|
|
693
|
+
api: POST /obsBlocks/{obId}/revise
|
|
694
|
+
|
|
695
|
+
usage:
|
|
696
|
+
::
|
|
697
|
+
|
|
698
|
+
pi.reviseOB(obId)
|
|
699
|
+
"""
|
|
700
|
+
return self.post('/obsBlocks/%d/revise' % obId)
|
|
701
|
+
|
|
702
|
+
def duplicateOB(self, obId, containerId=0):
|
|
703
|
+
"""Duplicate OB.
|
|
704
|
+
|
|
705
|
+
Create a new OB or CB identical to this one in the same or a different
|
|
706
|
+
container. The new OB is appended at the end of the container. The
|
|
707
|
+
destination may be in a different observing run of the same instrument.
|
|
708
|
+
|
|
709
|
+
api: POST /obsBlocks/{obId}/duplicate
|
|
710
|
+
|
|
711
|
+
usage:
|
|
712
|
+
::
|
|
713
|
+
|
|
714
|
+
duplicatedOB, duplicatedOBVersion = api.duplicateOB(obId)
|
|
715
|
+
duplicatedOB, duplicatedOBVersion = api.duplicateOB(obId, containerId)
|
|
716
|
+
"""
|
|
717
|
+
data = {}
|
|
718
|
+
if containerId > 0:
|
|
719
|
+
data = {'containerId': containerId}
|
|
720
|
+
return self.post('/obsBlocks/%d/duplicate' % obId, data)
|
|
721
|
+
|
|
722
|
+
def migrateOB(self, obId, dryRun):
|
|
723
|
+
"""Migrate OB to latest instrument package.
|
|
724
|
+
|
|
725
|
+
api: POST /obsBlocks/{obId}/migrate
|
|
726
|
+
|
|
727
|
+
usage:
|
|
728
|
+
::
|
|
729
|
+
|
|
730
|
+
result, _ = api.migrateOB(obId, false)
|
|
731
|
+
result, _ = api.migrateOB(obId, true)
|
|
732
|
+
"""
|
|
733
|
+
return self.post('/obsBlocks/%d/migrate' % obId, {'dryRun': dryRun})
|
|
734
|
+
|
|
735
|
+
def executionTime(self, Id, itemType=None):
|
|
736
|
+
"""Compute the execution time for the OB, or all OBs in or below a container.
|
|
737
|
+
|
|
738
|
+
api: POST /obsBlocks/{Id}/executionTime
|
|
739
|
+
|
|
740
|
+
usage:
|
|
741
|
+
::
|
|
742
|
+
|
|
743
|
+
obInfo = api.executionTime(Id)
|
|
744
|
+
containersInfo = api.executionTime(Id)
|
|
745
|
+
containersInfo = api.executionTime(Id, itemType='Folder')
|
|
746
|
+
containersInfo = api.executionTime(Id, itemType='Group')
|
|
747
|
+
|
|
748
|
+
If Id is a an obId, a obInfo dictionary will be returned.
|
|
749
|
+
If Id is a containerId (Folder, Group, Concatenation, TimeLink) a list
|
|
750
|
+
of obInfo and/or containerInfo dictionaries will be returned. A containerInfo
|
|
751
|
+
dictionary provides info about the container, and the OBs contained in that
|
|
752
|
+
container. Folders do not generate containerInfo dictionaries.
|
|
753
|
+
"""
|
|
754
|
+
if itemType in [None, 'OB'] :
|
|
755
|
+
try :
|
|
756
|
+
return self.post('/obsBlocks/%d/executionTime' % Id)[0]
|
|
757
|
+
except :
|
|
758
|
+
# We assume it failed because Id is not an OB,
|
|
759
|
+
# thus it must be a container, so we work through the container
|
|
760
|
+
pass
|
|
761
|
+
obsInfo=[]
|
|
762
|
+
c,_ = self.getContainer(Id)
|
|
763
|
+
if c['itemType'] in ['Group','Concatenation','TimeLink',] :
|
|
764
|
+
c['exposureTime']=0
|
|
765
|
+
c['executionTime']=0
|
|
766
|
+
c['obs']=self.post('/containers/%d/items/executionTimes' % Id)[0]
|
|
767
|
+
for ob in c['obs'] :
|
|
768
|
+
c['exposureTime']+=ob['exposureTime']
|
|
769
|
+
c['executionTime']+=ob['executionTime']
|
|
770
|
+
obsInfo+=[c,]
|
|
771
|
+
else:
|
|
772
|
+
# Id must be a folder...
|
|
773
|
+
items, _ = self.getItems(Id)
|
|
774
|
+
for item in items :
|
|
775
|
+
obsInfo+=[self.executionTime(item.get('obId',item.get('containerId')), itemType=item['itemType'])]
|
|
776
|
+
return obsInfo
|
|
777
|
+
|
|
778
|
+
def exportOB(self, obId, filename):
|
|
779
|
+
"""Export this OB into a ZIP file.
|
|
780
|
+
|
|
781
|
+
api: GET /obsBlocks/{obId}/export
|
|
782
|
+
|
|
783
|
+
usage:
|
|
784
|
+
::
|
|
785
|
+
|
|
786
|
+
api.exportOB(obId, 'myOB.zip')
|
|
787
|
+
"""
|
|
788
|
+
self.downloadBinaryFile('/obsBlocks/%d/export' % obId, filename)
|
|
789
|
+
|
|
790
|
+
def getOBExecutions(self, obId, night):
|
|
791
|
+
"""Get executions during a given night.
|
|
792
|
+
|
|
793
|
+
api: GET /obsBlocks/{obId}/executions
|
|
794
|
+
|
|
795
|
+
usage:
|
|
796
|
+
::
|
|
797
|
+
|
|
798
|
+
obExecutions, _ = getOBExecutions(obId, '2018-01-01')
|
|
799
|
+
"""
|
|
800
|
+
return self.get('/obsBlocks/%d/executions?night=%s' % (obId, night))
|
|
801
|
+
|
|
802
|
+
def getNightExecutions(self, instrument, night ):
|
|
803
|
+
"""Get executions during a given night for a specific instrument
|
|
804
|
+
|
|
805
|
+
api: GET /executions/{instrument}
|
|
806
|
+
|
|
807
|
+
usage:
|
|
808
|
+
::
|
|
809
|
+
|
|
810
|
+
executions, _ = api.getNightExecutions(instrument, night)
|
|
811
|
+
executions, _ = api.getNightExecutions(instrument, '2016-01-19')
|
|
812
|
+
"""
|
|
813
|
+
return self.get('/executions/%s?night=%s' %(instrument,night))
|
|
814
|
+
|
|
815
|
+
def getOBCompletelyFilledIn(self, obId):
|
|
816
|
+
"""Check that the OB's templates are completely filled in.
|
|
817
|
+
|
|
818
|
+
Performs a quick check that there are neither missing templates
|
|
819
|
+
nor missing parameters on this OB.
|
|
820
|
+
|
|
821
|
+
This is not a full validation.
|
|
822
|
+
|
|
823
|
+
api: GET /obsBlocks/{obId}/templates/completelyFilledIn
|
|
824
|
+
|
|
825
|
+
usage:
|
|
826
|
+
::
|
|
827
|
+
|
|
828
|
+
response, _ = api.getOBCompletelyFilledIn(obId)
|
|
829
|
+
print('OB complete:', response['complete'])
|
|
830
|
+
"""
|
|
831
|
+
return self.get('/obsBlocks/%d/templates/completelyFilledIn' % obId)
|
|
832
|
+
|
|
833
|
+
def getAbsoluteTimeConstraints(self, obId):
|
|
834
|
+
"""Get list of absolute time constraints.
|
|
835
|
+
|
|
836
|
+
The list is sorted in ascending order by from.
|
|
837
|
+
Date and time are in ISO-8601 format for the UTC timezone.
|
|
838
|
+
A CB does not have absolute time constraints and returns an empty list.
|
|
839
|
+
|
|
840
|
+
api: GET /obsBlocks/{obId}/timeConstraints/absolute
|
|
841
|
+
|
|
842
|
+
usage:
|
|
843
|
+
::
|
|
844
|
+
|
|
845
|
+
absTCs, atcVersion = api.getAbsoluteTimeConstraints(obId)
|
|
846
|
+
"""
|
|
847
|
+
return self.get('/obsBlocks/%d/timeConstraints/absolute' % obId)
|
|
848
|
+
|
|
849
|
+
def saveAbsoluteTimeConstraints(self, obId, timeConstraints, version):
|
|
850
|
+
"""Set list of absolute time constraints.
|
|
851
|
+
|
|
852
|
+
Date and time are in ISO-8601 format and must use UTC timezone.
|
|
853
|
+
Time intervals must not overlap and from must be earlier than to.
|
|
854
|
+
|
|
855
|
+
api: PUT /obsBlocks/{obId}/timeConstraints/absolute
|
|
856
|
+
|
|
857
|
+
usage:
|
|
858
|
+
::
|
|
859
|
+
|
|
860
|
+
absTCs, atcVersion = api.getAbsoluteTimeConstraints(obId)
|
|
861
|
+
api.saveAbsoluteTimeConstraints(obId,[
|
|
862
|
+
{
|
|
863
|
+
'from': '2017-09-01T00:00',
|
|
864
|
+
'to': '2017-09-30T23:59'
|
|
865
|
+
},
|
|
866
|
+
{
|
|
867
|
+
'from': '2017-11-01T00:00',
|
|
868
|
+
'to': '2017-11-30T23:59'
|
|
869
|
+
}
|
|
870
|
+
], atcVersion)
|
|
871
|
+
"""
|
|
872
|
+
return self.put('/obsBlocks/%d/timeConstraints/absolute' % obId, timeConstraints, version)
|
|
873
|
+
|
|
874
|
+
def getSiderealTimeConstraints(self, obId):
|
|
875
|
+
"""Get list of sidereal time constraints.
|
|
876
|
+
|
|
877
|
+
The list is sorted in ascending order by from.
|
|
878
|
+
A CB does not have sidereal time constraints and returns an empty list.
|
|
879
|
+
|
|
880
|
+
api: GET /obsBlocks/{obId}/timeConstraints/sidereal
|
|
881
|
+
|
|
882
|
+
usage:
|
|
883
|
+
::
|
|
884
|
+
|
|
885
|
+
sidTCs, stcVersion = api.getSiderealTimeConstraints(obId)
|
|
886
|
+
"""
|
|
887
|
+
return self.get('/obsBlocks/%d/timeConstraints/sidereal' % obId)
|
|
888
|
+
|
|
889
|
+
def saveSiderealTimeConstraints(self, obId, timeConstraints, version):
|
|
890
|
+
"""Set list of sidereal time constraints.
|
|
891
|
+
|
|
892
|
+
Time intervals must not overlap. A time interval ending at the starting time
|
|
893
|
+
of another is considered an overlap; avoid this by combining the intervals
|
|
894
|
+
into a single one.
|
|
895
|
+
|
|
896
|
+
There can be a single time interval ending at 24:00; it will not overlap
|
|
897
|
+
a time interval starting at 0:00. An interval ending at 24:00 and one
|
|
898
|
+
starting at 00:00 will be combined into a single interval.
|
|
899
|
+
|
|
900
|
+
24:00 cannot be used as starting time, and 00:00 cannot be used
|
|
901
|
+
as ending time.
|
|
902
|
+
|
|
903
|
+
api: PUT /obsBlocks/{obId}/timeConstraints/sidereal
|
|
904
|
+
|
|
905
|
+
usage:
|
|
906
|
+
::
|
|
907
|
+
|
|
908
|
+
sidTCs, stcVersion = api.getSiderealTimeConstraints(obId)
|
|
909
|
+
api.saveSiderealTimeConstraints(obId,[
|
|
910
|
+
{
|
|
911
|
+
'from': '00:00',
|
|
912
|
+
'to': '01:00'
|
|
913
|
+
},
|
|
914
|
+
{
|
|
915
|
+
'from': '03:00',
|
|
916
|
+
'to': '05:00'
|
|
917
|
+
}
|
|
918
|
+
], stcVersion)
|
|
919
|
+
"""
|
|
920
|
+
return self.put('/obsBlocks/%d/timeConstraints/sidereal' % obId, timeConstraints, version)
|
|
921
|
+
|
|
922
|
+
def createTemplate(self, obId, name):
|
|
923
|
+
"""Attach a new template.
|
|
924
|
+
|
|
925
|
+
An OB can have only one acquisition template. Attempting to add a
|
|
926
|
+
second one will result in an error.
|
|
927
|
+
|
|
928
|
+
The OB must have the latest ipVersion.
|
|
929
|
+
|
|
930
|
+
Visitor Mode: The OB can be changed in any status.
|
|
931
|
+
|
|
932
|
+
Service Mode: The OB must have status (-) or (P).
|
|
933
|
+
|
|
934
|
+
api: POST /obsBlocks/{obId}/templates
|
|
935
|
+
|
|
936
|
+
usage:
|
|
937
|
+
::
|
|
938
|
+
|
|
939
|
+
tpl, tplVersion = api.createTemplate(obId, 'UVES_blue_acq_slit')
|
|
940
|
+
"""
|
|
941
|
+
return self.post('/obsBlocks/%d/templates' % obId, {'templateName': name})
|
|
942
|
+
|
|
943
|
+
def getTemplates(self, obId):
|
|
944
|
+
"""Get all attached templates.
|
|
945
|
+
|
|
946
|
+
api: GET /obsBlocks/{obId}/templates
|
|
947
|
+
|
|
948
|
+
usage:
|
|
949
|
+
::
|
|
950
|
+
|
|
951
|
+
templates, templatesVersion = api.getTemplates(obId)
|
|
952
|
+
for t in templates:
|
|
953
|
+
print ('template found:', t['templateName'])
|
|
954
|
+
"""
|
|
955
|
+
return self.get('/obsBlocks/%d/templates' % obId)
|
|
956
|
+
|
|
957
|
+
def reorderTemplates(self, obId, templates, version):
|
|
958
|
+
"""Change order of attached templates.
|
|
959
|
+
|
|
960
|
+
The list must contain the same items as in the previous GET request.
|
|
961
|
+
Only the order can be changed. Templates must be explicitly added
|
|
962
|
+
with a POST request or deleted with a DELETE request. If there is
|
|
963
|
+
an acquisition template, it must be the first in the list.
|
|
964
|
+
|
|
965
|
+
The OB must have the latest ipVersion.
|
|
966
|
+
|
|
967
|
+
Visitor Mode: The OB can be changed in any status.
|
|
968
|
+
|
|
969
|
+
Service Mode: The OB must have status (-) or (P).
|
|
970
|
+
|
|
971
|
+
api: PUT /obsBlocks/{obId}/templates
|
|
972
|
+
|
|
973
|
+
usage:
|
|
974
|
+
::
|
|
975
|
+
|
|
976
|
+
tpls, tplsVersion = api.getTemplates(obId)
|
|
977
|
+
# change order of tpls ...
|
|
978
|
+
tpls, tplsVersion = api.reorderTemplates(obId, tpls, tplsVersion)
|
|
979
|
+
"""
|
|
980
|
+
return self.put('/obsBlocks/%d/templates' % obId, templates, version)
|
|
981
|
+
|
|
982
|
+
def getTemplate(self, obId, templateId):
|
|
983
|
+
"""Get existing template.
|
|
984
|
+
|
|
985
|
+
A parameter value may be null if there is no default value and a
|
|
986
|
+
value was never set.
|
|
987
|
+
|
|
988
|
+
For parameters of type 'paramfile' or 'file' a string is returned.
|
|
989
|
+
For a 'paramfile' this is the value of PAF.NAME in the PAF header;
|
|
990
|
+
for a 'file' the first few characters are returned.
|
|
991
|
+
File parameters are set with a separate call.
|
|
992
|
+
|
|
993
|
+
api: GET /obsBlocks/{obId}/templates/{templateId}
|
|
994
|
+
|
|
995
|
+
usage:
|
|
996
|
+
::
|
|
997
|
+
|
|
998
|
+
tpl, tplVersion = api.getTemplate(obId, templateId)
|
|
999
|
+
"""
|
|
1000
|
+
return self.get('/obsBlocks/%d/templates/%d' % (obId, templateId))
|
|
1001
|
+
|
|
1002
|
+
def setTemplateParams(self, obId, template, params, version):
|
|
1003
|
+
"""Helper API to make it easier to update template parameters.
|
|
1004
|
+
Uses saveTemplate() under the hood.
|
|
1005
|
+
|
|
1006
|
+
|
|
1007
|
+
usage:
|
|
1008
|
+
::
|
|
1009
|
+
|
|
1010
|
+
tpl, tplVersion = api.createTemplate(obId, 'UVES_blue_acq_slit')
|
|
1011
|
+
tpl, tplVersion = api.setTemplateParams(obId, tpl, {
|
|
1012
|
+
'TEL.GS1.ALPHA': '11:22:33.000',
|
|
1013
|
+
'INS.COLL.NAID': 'COLL_SR+7',
|
|
1014
|
+
'INS.DPOL.MODE': 'ON'
|
|
1015
|
+
}, tplVersion)
|
|
1016
|
+
"""
|
|
1017
|
+
for p in template['parameters']:
|
|
1018
|
+
p['value'] = params.get(p['name'], p['value'])
|
|
1019
|
+
return self.saveTemplate(obId, template, version)
|
|
1020
|
+
|
|
1021
|
+
def saveTemplate(self, obId, template, version):
|
|
1022
|
+
"""Update existing template.
|
|
1023
|
+
|
|
1024
|
+
The templateId, templateName and type cannot be changed.
|
|
1025
|
+
|
|
1026
|
+
When GET returns a value of null, it can be changed to a proper value
|
|
1027
|
+
(or left at null). But a proper value cannot be unset to null.
|
|
1028
|
+
|
|
1029
|
+
File parameters must be set with a separate call; the string value
|
|
1030
|
+
returned from GET cannot be changed.
|
|
1031
|
+
|
|
1032
|
+
The OB must have the latest ipVersion.
|
|
1033
|
+
|
|
1034
|
+
Visitor Mode: The OB can be changed in any status.
|
|
1035
|
+
|
|
1036
|
+
Service Mode: The OB must have status (-) or (P).
|
|
1037
|
+
|
|
1038
|
+
api: PUT /obsBlocks/{obId}/templates/{templateId}
|
|
1039
|
+
|
|
1040
|
+
usage:
|
|
1041
|
+
::
|
|
1042
|
+
|
|
1043
|
+
It is recommended to use setTemplateParams() instead
|
|
1044
|
+
"""
|
|
1045
|
+
return self.put('/obsBlocks/%d/templates/%d' % (obId, template['templateId']), template, etag=version)
|
|
1046
|
+
|
|
1047
|
+
def getFileParam(self, obId, templateId, param, filename):
|
|
1048
|
+
"""Get value of a template parameter of type "file" or "paramfile".
|
|
1049
|
+
|
|
1050
|
+
api: GET /obsBlocks/{obId}/templates/{templateId}/{fileParameter}
|
|
1051
|
+
|
|
1052
|
+
usage:
|
|
1053
|
+
::
|
|
1054
|
+
|
|
1055
|
+
_, version = api.getFileParam(obId, templateId, 'SEQ.REF.FILE1', myFile.paf')
|
|
1056
|
+
"""
|
|
1057
|
+
return self.downloadTextFile('/obsBlocks/%d/templates/%d/%s' % (obId, templateId, param), filename)
|
|
1058
|
+
|
|
1059
|
+
def saveFileParam(self, obId, templateId, param, filename, version):
|
|
1060
|
+
"""Set value of a parameter of type "file" or "paramfile".
|
|
1061
|
+
|
|
1062
|
+
api: PUT /obsBlocks/{obId}/templates/{templateId}/{fileParameter}
|
|
1063
|
+
|
|
1064
|
+
usage:
|
|
1065
|
+
::
|
|
1066
|
+
|
|
1067
|
+
_, version = api.getFileParam(obId, templateId, 'SEQ.REF.FILE1', myFile.paf')
|
|
1068
|
+
api.saveFileParam(obId, templateId, 'SEQ.REF.FILE1', newFile.paf', version)
|
|
1069
|
+
"""
|
|
1070
|
+
return self.uploadFile('PUT', '/obsBlocks/%d/templates/%d/%s' % (obId, templateId, param), filename,
|
|
1071
|
+
'text/plain', version)
|
|
1072
|
+
|
|
1073
|
+
def deleteTemplate(self, obId, templateId, version):
|
|
1074
|
+
"""Delete existing template.
|
|
1075
|
+
|
|
1076
|
+
The OB must have the latest ipVersion.
|
|
1077
|
+
|
|
1078
|
+
Visitor Mode: The OB can be changed in any status.
|
|
1079
|
+
|
|
1080
|
+
Service Mode: The OB must have status (-) or (P).
|
|
1081
|
+
|
|
1082
|
+
api: DELETE /obsBlocks/{obId}/templates/{templateId}
|
|
1083
|
+
|
|
1084
|
+
usage:
|
|
1085
|
+
::
|
|
1086
|
+
|
|
1087
|
+
_, tplVersion = api.getTemplate(obId, templateId)
|
|
1088
|
+
api.deleteTemplate(obId, templateId, tplVersion)
|
|
1089
|
+
"""
|
|
1090
|
+
return self.delete('/obsBlocks/%d/templates/%d' % (obId, templateId), etag=version)
|
|
1091
|
+
|
|
1092
|
+
def duplicateTemplate(self, obId, templateId, destinationObId=0):
|
|
1093
|
+
"""Duplicate this template.
|
|
1094
|
+
|
|
1095
|
+
Create a new template identical to this one in this or a different OB.
|
|
1096
|
+
The new template is appended at the end of the target OB's template list.
|
|
1097
|
+
|
|
1098
|
+
The new template will have a new templateId. All other fields, including
|
|
1099
|
+
any file and paramfile parameters, will be the same as in the original template.
|
|
1100
|
+
|
|
1101
|
+
An OB can have only one acquisition template. Attempting to add a second
|
|
1102
|
+
one will result in an error.
|
|
1103
|
+
|
|
1104
|
+
api: POST /obsBlocks/{obId}/templates/{templateId}/duplicate
|
|
1105
|
+
|
|
1106
|
+
usage:
|
|
1107
|
+
::
|
|
1108
|
+
|
|
1109
|
+
duplicatedTemplate, duplicatedTemplateVersion = api.duplicateTemplate(obId, templateId)
|
|
1110
|
+
duplicatedTemplate, duplicatedTemplateVersion = api.duplicateTemplate(obId, templateId, destinationObId)
|
|
1111
|
+
"""
|
|
1112
|
+
data = {}
|
|
1113
|
+
if destinationObId > 0:
|
|
1114
|
+
data = {'obId': destinationObId}
|
|
1115
|
+
return self.post('/obsBlocks/%d/templates/%d/duplicate' % (obId, templateId), data)
|
|
1116
|
+
|
|
1117
|
+
def getEphemerisFile(self, obId, filename):
|
|
1118
|
+
"""Get the ephemeris file of this OB and save it into the given filename.
|
|
1119
|
+
|
|
1120
|
+
If the ephemeris file was never saved, or was deleted, an empty file
|
|
1121
|
+
is returned.
|
|
1122
|
+
|
|
1123
|
+
api: GET /obsBlocks/{obId}/ephemeris
|
|
1124
|
+
|
|
1125
|
+
usage:
|
|
1126
|
+
::
|
|
1127
|
+
|
|
1128
|
+
_, ephVersion = api.getEphemerisFile(obId, 'ephem.txt')
|
|
1129
|
+
"""
|
|
1130
|
+
url = '/obsBlocks/%d/ephemeris' % obId
|
|
1131
|
+
return self.downloadTextFile(url, filename)
|
|
1132
|
+
|
|
1133
|
+
def saveEphemerisFile(self, obId, filename, version):
|
|
1134
|
+
"""Save the ephemeris file of this OB.
|
|
1135
|
+
|
|
1136
|
+
A CB cannot have an ephemeris file.
|
|
1137
|
+
|
|
1138
|
+
Service Mode: The OB must have status (-) or (P).
|
|
1139
|
+
|
|
1140
|
+
api: PUT /obsBlocks/{obId}/ephemeris
|
|
1141
|
+
|
|
1142
|
+
usage:
|
|
1143
|
+
::
|
|
1144
|
+
|
|
1145
|
+
_, ephVersion = api.getEphemerisFile(obId, 'delete_me.txt')
|
|
1146
|
+
_, ephVersion = api.saveEphemerisFile(obId, 'ephem.txt', ephVersion)
|
|
1147
|
+
"""
|
|
1148
|
+
return self.uploadFile('PUT', '/obsBlocks/%d/ephemeris' % obId, filename,
|
|
1149
|
+
'text/plain', version)
|
|
1150
|
+
|
|
1151
|
+
def deleteEphemerisFile(self, obId, version):
|
|
1152
|
+
"""Delete the ephemeris file of this OB.
|
|
1153
|
+
|
|
1154
|
+
Service Mode: The OB must have status (-) or (P).
|
|
1155
|
+
|
|
1156
|
+
api: DELETE /obsBlocks/{obId}/ephemeris
|
|
1157
|
+
|
|
1158
|
+
usage:
|
|
1159
|
+
::
|
|
1160
|
+
|
|
1161
|
+
_, ephVersion = api.getEphemerisFile(obId, 'delete_me.txt')
|
|
1162
|
+
api.deleteEphemerisFile(obId, ephVersion)
|
|
1163
|
+
"""
|
|
1164
|
+
return self.request('DELETE', '/obsBlocks/%d/ephemeris' % obId, etag=version)
|
|
1165
|
+
|
|
1166
|
+
def getFindingChartNames(self, obId):
|
|
1167
|
+
"""List attached finding charts.
|
|
1168
|
+
|
|
1169
|
+
Returns the list of names of the attached finding charts in the order
|
|
1170
|
+
of their index. If no finding charts are attached, an empty list will
|
|
1171
|
+
be returned.
|
|
1172
|
+
|
|
1173
|
+
api: GET /obsBlocks/{obId}/findingCharts
|
|
1174
|
+
|
|
1175
|
+
usage:
|
|
1176
|
+
::
|
|
1177
|
+
|
|
1178
|
+
fcNames, _ = api.getFindingChartNames(obId)
|
|
1179
|
+
"""
|
|
1180
|
+
return self.get('/obsBlocks/%d/findingCharts' % obId)
|
|
1181
|
+
|
|
1182
|
+
def addFindingChart(self, obId, filename):
|
|
1183
|
+
"""Attach a new finding chart in format image/jpeg.
|
|
1184
|
+
|
|
1185
|
+
Up to 5 finding charts can be attached to a single OB.
|
|
1186
|
+
The maximum file size is 1MB (2^20 bytes).
|
|
1187
|
+
|
|
1188
|
+
The OB must have the latest ipVersion.
|
|
1189
|
+
|
|
1190
|
+
Service Mode: The OB must have status (-) or (P).
|
|
1191
|
+
|
|
1192
|
+
api: POST /obsBlocks/{obId}/findingCharts
|
|
1193
|
+
|
|
1194
|
+
usage:
|
|
1195
|
+
::
|
|
1196
|
+
|
|
1197
|
+
api.addFindingChart(obId, filename)
|
|
1198
|
+
"""
|
|
1199
|
+
return self.uploadFile('POST', '/obsBlocks/%d/findingCharts' % obId, filename,
|
|
1200
|
+
'image/jpeg')
|
|
1201
|
+
|
|
1202
|
+
def generateFindingChart(
|
|
1203
|
+
self,
|
|
1204
|
+
obId,
|
|
1205
|
+
obs_date=None,
|
|
1206
|
+
stretch=None,
|
|
1207
|
+
exponent=None,
|
|
1208
|
+
pmin=None,
|
|
1209
|
+
pmax=None,
|
|
1210
|
+
GAIA_im_IQ=None,
|
|
1211
|
+
GAIA_im_noise=None,
|
|
1212
|
+
survey=None,
|
|
1213
|
+
bkg_image=None,
|
|
1214
|
+
bkg_lam=None,
|
|
1215
|
+
c_stretch=None,
|
|
1216
|
+
c_exponent=None,
|
|
1217
|
+
c_pmin=None,
|
|
1218
|
+
c_pmax=None,
|
|
1219
|
+
c_GAIA_im_IQ=None,
|
|
1220
|
+
c_GAIA_im_noise=None,
|
|
1221
|
+
):
|
|
1222
|
+
"""
|
|
1223
|
+
Generate new finding chart(s) in format image/jpeg using ESO's p2fc service.
|
|
1224
|
+
|
|
1225
|
+
If the JPG creation is successful, then, and only then will ANY and ALL
|
|
1226
|
+
existing p2fc generated FCs will be removed, the new set of 2 (or 3 if a
|
|
1227
|
+
non-default survey or bkg_image is provided) will then be attached. If
|
|
1228
|
+
for whatever reason the FC JPG creation process fails, any FC JPGs
|
|
1229
|
+
already attached to the OB are left untouched.
|
|
1230
|
+
|
|
1231
|
+
The OB must have the latest ipVersion.
|
|
1232
|
+
|
|
1233
|
+
Service Mode: The OB must have status (-) or (P).
|
|
1234
|
+
|
|
1235
|
+
obs_date: must be interpretable by: , e.g. '2019-10-01 02:03:04 UTC'
|
|
1236
|
+
|
|
1237
|
+
bkg_image: should be a local FITS file
|
|
1238
|
+
|
|
1239
|
+
bkg_lam: discription of the bandpass of the image, only needed if file
|
|
1240
|
+
Primary header does not include a keyword BANDPASS or FILTER or is not
|
|
1241
|
+
a 'standard' ESO or GEMINI image.
|
|
1242
|
+
|
|
1243
|
+
survey: a SkyView supported image survey providing FITS
|
|
1244
|
+
|
|
1245
|
+
pmin, pmax, stretch & exponent and c_pmin, c_pmax, c_stretch &
|
|
1246
|
+
c_exponent are passed directly to the APLPY FITSFigure.show_grayscale
|
|
1247
|
+
method,see
|
|
1248
|
+
https://aplpy.readthedocs.io/en/stable/api/aplpy.FITSFigure.html#aplpy.FITSFigure.show_grayscale
|
|
1249
|
+
for details.
|
|
1250
|
+
|
|
1251
|
+
pmin, pmax, stretch & exponent are applied to BOTH default FCs.
|
|
1252
|
+
|
|
1253
|
+
c_pmin, c_pmax, c_stretch & c_exponent are applied to and and all
|
|
1254
|
+
custom FCs.
|
|
1255
|
+
|
|
1256
|
+
GAIA_im_IQ [arcsec] & GAIA_im_noise affect the GAIA pseudo images used
|
|
1257
|
+
for insets and if survey='Gaia' is used.
|
|
1258
|
+
|
|
1259
|
+
GAIA_im_noise is a dimensionless factor, with the noise computed via the
|
|
1260
|
+
numpy method random.normal(loc,scale) with loc=0.0 and:
|
|
1261
|
+
scale=GAIA_im_noise*<brightest_flux_value_in_pseudo_image> The default
|
|
1262
|
+
value is (as at 2020-01): 1e-6
|
|
1263
|
+
Together with pmin and pmax this parameter gives (some) control over the
|
|
1264
|
+
relative appearance of stars of different brightness, and can thus be helpful
|
|
1265
|
+
to (for example) highlight faint, but not insignificant, field star(s).
|
|
1266
|
+
|
|
1267
|
+
GAIA_im_IQ & GAIA_im_noise are used for any and all Gaia pseudo images
|
|
1268
|
+
associated with the default FCs.
|
|
1269
|
+
|
|
1270
|
+
c_GAIA_im_IQ & c_GAIA_im_noise are the equivalents for any and all Gaia
|
|
1271
|
+
pseudo images associated with the custom FCs.
|
|
1272
|
+
|
|
1273
|
+
returns: data
|
|
1274
|
+
data : None or json structure, possibly with warning messages.
|
|
1275
|
+
|
|
1276
|
+
usage:
|
|
1277
|
+
::
|
|
1278
|
+
data = api.generateFindingChart(obId)
|
|
1279
|
+
data = api.generateFindingChart(obId, obs_date='2020-01-01 05:30:00 UTC')
|
|
1280
|
+
data = api.generateFindingChart(obId, obs_date='2020-01-01 05:30:00 UTC', pmin=2.5, pmax=99., stretch='arcsinh')
|
|
1281
|
+
data = api.generateFindingChart(obId, bkg_image='myobject.fits')
|
|
1282
|
+
data = api.generateFindingChart(obId, obs_date='2020-01-01 05:30:00 UTC', bkg_image='myobject.fits')
|
|
1283
|
+
data = api.generateFindingChart(obId, survey='2MASS-K')
|
|
1284
|
+
data = api.generateFindingChart(obId, obs_date='2020-01-01 05:30:00 UTC', survey='2MASS-K')
|
|
1285
|
+
data = api.generateFindingChart(obId, obs_date='2020-01-01 05:30:00 UTC', survey='2MASS-K', c_stretch='sqrt', c_pmin=10., c_pmax=90. )
|
|
1286
|
+
"""
|
|
1287
|
+
|
|
1288
|
+
data = {
|
|
1289
|
+
'url': self.apiUrl + '/obsBlocks/%d' % obId,
|
|
1290
|
+
'access_token': self.access_token,
|
|
1291
|
+
'obsdate': obs_date,
|
|
1292
|
+
'exponent': exponent,
|
|
1293
|
+
'pmin': pmin,
|
|
1294
|
+
'pmax': pmax,
|
|
1295
|
+
'GAIA_im_IQ': GAIA_im_IQ,
|
|
1296
|
+
'GAIA_im_noise': GAIA_im_noise,
|
|
1297
|
+
'bkg_lam': bkg_lam,
|
|
1298
|
+
'survey': survey,
|
|
1299
|
+
'c_stretch': c_stretch,
|
|
1300
|
+
'c_exponent': c_exponent,
|
|
1301
|
+
'c_pmin': c_pmin,
|
|
1302
|
+
'c_pmax': c_pmax,
|
|
1303
|
+
'c_GAIA_im_IQ': c_GAIA_im_IQ,
|
|
1304
|
+
'c_GAIA_im_noise': c_GAIA_im_noise,
|
|
1305
|
+
}
|
|
1306
|
+
files=None
|
|
1307
|
+
if bkg_image is not None :
|
|
1308
|
+
if os.path.exists(bkg_image) :
|
|
1309
|
+
files={
|
|
1310
|
+
'bkg_image_fits_file': (
|
|
1311
|
+
os.path.basename(bkg_image),
|
|
1312
|
+
open(bkg_image, 'rb'),
|
|
1313
|
+
'image/fits',
|
|
1314
|
+
data,
|
|
1315
|
+
)
|
|
1316
|
+
}
|
|
1317
|
+
data=None
|
|
1318
|
+
else :
|
|
1319
|
+
raise P2Error(404, 'POST', self.p2fc_url, "Could not find bkg_image %s" %(bkg_image))
|
|
1320
|
+
return self.p2fc_request('POST', data=data, files=files)
|
|
1321
|
+
|
|
1322
|
+
|
|
1323
|
+
def bkg_image_get_ob_info(
|
|
1324
|
+
self,
|
|
1325
|
+
obId,
|
|
1326
|
+
):
|
|
1327
|
+
"""
|
|
1328
|
+
Get the current background image settings for the specified OB
|
|
1329
|
+
|
|
1330
|
+
returns: data
|
|
1331
|
+
data : None or an <ob_info> json structure, possibly with warning messages.
|
|
1332
|
+
|
|
1333
|
+
usage:
|
|
1334
|
+
::
|
|
1335
|
+
bkg_image_info = api.bkg_image_get_ob_info(obId)
|
|
1336
|
+
"""
|
|
1337
|
+
ob_url=self.apiUrl + '/obsBlocks/%d' % obId
|
|
1338
|
+
return self.p2fc_request('GET', 'bkg_image?url=%s&access_token=%s&mode=%s' %(ob_url, self.access_token, 'get_ob'))
|
|
1339
|
+
|
|
1340
|
+
|
|
1341
|
+
def bkg_image_get_ob_image_list(
|
|
1342
|
+
self,
|
|
1343
|
+
obId,
|
|
1344
|
+
):
|
|
1345
|
+
"""
|
|
1346
|
+
Get the list of currently available background images compatible with the specified OB
|
|
1347
|
+
|
|
1348
|
+
returns: data
|
|
1349
|
+
data : None or an <ob_image_list> json structure, possibly with warning messages.
|
|
1350
|
+
|
|
1351
|
+
usage:
|
|
1352
|
+
::
|
|
1353
|
+
bkg_image_list = api.bkg_image_get_ob_image_list(obId)
|
|
1354
|
+
"""
|
|
1355
|
+
ob_url=self.apiUrl + '/obsBlocks/%d' % obId
|
|
1356
|
+
return self.p2fc_request('GET','bkg_image?url=%s&access_token=%s&mode=%s' %(ob_url, self.access_token, 'get_ob_image_list'))
|
|
1357
|
+
|
|
1358
|
+
|
|
1359
|
+
def bkg_image_set_ob_info(
|
|
1360
|
+
self,
|
|
1361
|
+
obId,
|
|
1362
|
+
info,
|
|
1363
|
+
):
|
|
1364
|
+
"""
|
|
1365
|
+
Set the current background image settings for the specified OB
|
|
1366
|
+
|
|
1367
|
+
The specified info should be one of:
|
|
1368
|
+
- default
|
|
1369
|
+
- gaia
|
|
1370
|
+
- survey|<survey_name>, e.g. 'survey|2MASS-K'
|
|
1371
|
+
- eso_im|<DPid>, e.g. 'eso_im|ADP.2015-05-11T10:19:53.607
|
|
1372
|
+
- user_bkg_image|<fits_hash>, e.g. 'user_bkg_image|00c98a6dde73f309f058378399cad019f8405603c63d74b5f2ba342a8ae0a8f1'
|
|
1373
|
+
|
|
1374
|
+
returns: data
|
|
1375
|
+
data : None or an <ob_info> json structure, possibly with warning messages.
|
|
1376
|
+
|
|
1377
|
+
usage:
|
|
1378
|
+
::
|
|
1379
|
+
bkg_image_info = api.bkg_image_set_ob_info(obId, 'default')
|
|
1380
|
+
bkg_image_info = api.bkg_image_set_ob_info(obId, 'gaia')
|
|
1381
|
+
bkg_image_info = api.bkg_image_set_ob_info(obId, 'survey|2MASS-K')
|
|
1382
|
+
bkg_image_info = api.bkg_image_set_ob_info(obId, 'eso_im|ADP.2015-05-11T10:19:53.607')
|
|
1383
|
+
bkg_image_info = api.bkg_image_set_ob_info(obId, 'user_bkg_image|00c98a6dde73f309f058378399cad019f8405603c63d74b5f2ba342a8ae0a8f1')
|
|
1384
|
+
|
|
1385
|
+
bkg_image_list = api.bkg_image_get_ob_image_list(obId)
|
|
1386
|
+
first_image=list(bkg_image_list.keys())[0]
|
|
1387
|
+
fits_hash=bkg_image_list[first_image]['fits_hash']
|
|
1388
|
+
bkg_image_info = api.bkg_image_set_ob_info(obId, 'user_bkg_image|%s' %(fits_hash))
|
|
1389
|
+
"""
|
|
1390
|
+
data = {
|
|
1391
|
+
'url': self.apiUrl + '/obsBlocks/%d' % obId,
|
|
1392
|
+
'access_token': self.access_token,
|
|
1393
|
+
'mode': "set_ob_info",
|
|
1394
|
+
'value': info,
|
|
1395
|
+
}
|
|
1396
|
+
return self.p2fc_request('POST', data=data)
|
|
1397
|
+
|
|
1398
|
+
|
|
1399
|
+
def bkg_image_upload(
|
|
1400
|
+
self,
|
|
1401
|
+
obId,
|
|
1402
|
+
bkg_image,
|
|
1403
|
+
bkg_lam=None,
|
|
1404
|
+
):
|
|
1405
|
+
"""
|
|
1406
|
+
Upload a background image for future use with the specified OB
|
|
1407
|
+
|
|
1408
|
+
bkg_image: a local FITS file
|
|
1409
|
+
|
|
1410
|
+
bkg_lam: discription of the bandpass of the image, only needed if file
|
|
1411
|
+
Primary header does not include a keyword BANDPASS or FILTER or is not
|
|
1412
|
+
a 'standard' ESO or GEMINI image.
|
|
1413
|
+
|
|
1414
|
+
returns: data
|
|
1415
|
+
data : None or an <ob_info> json structure, possibly with warning messages.
|
|
1416
|
+
|
|
1417
|
+
usage:
|
|
1418
|
+
::
|
|
1419
|
+
bkg_image_info = api.bkg_image_upload(obId 'ADP.2015-05-11T10:19:53.607.fits')
|
|
1420
|
+
bkg_image_info = api.bkg_image_upload(obId 'my_image.fits', bkg_lam='my_special_filter:')
|
|
1421
|
+
"""
|
|
1422
|
+
if os.path.exists(bkg_image) :
|
|
1423
|
+
files={
|
|
1424
|
+
'bkg_image_fits_file': (
|
|
1425
|
+
os.path.basename(bkg_image),
|
|
1426
|
+
open(bkg_image, 'rb'),
|
|
1427
|
+
'image/fits',
|
|
1428
|
+
{
|
|
1429
|
+
'url': self.apiUrl + '/obsBlocks/%d' % obId,
|
|
1430
|
+
'access_token': self.access_token,
|
|
1431
|
+
'mode': "upload_bkg_image",
|
|
1432
|
+
'bkg_lam': bkg_lam,
|
|
1433
|
+
},
|
|
1434
|
+
)
|
|
1435
|
+
}
|
|
1436
|
+
else :
|
|
1437
|
+
raise P2Error(404, 'POST', self.p2fc_url, "Could not find bkg_image %s" %(bkg_image))
|
|
1438
|
+
|
|
1439
|
+
return self.p2fc_request('POST', files=files)
|
|
1440
|
+
|
|
1441
|
+
def bkg_image_delete(
|
|
1442
|
+
self,
|
|
1443
|
+
obId,
|
|
1444
|
+
fits_hash,
|
|
1445
|
+
):
|
|
1446
|
+
"""
|
|
1447
|
+
Delete the specified image for the specified OB
|
|
1448
|
+
|
|
1449
|
+
fits_hash: as provided by e.g. bkg_image_get_info(obId)
|
|
1450
|
+
|
|
1451
|
+
returns: data
|
|
1452
|
+
data : None or an <ob_image_list> json structure, possibly with warning messages.
|
|
1453
|
+
|
|
1454
|
+
usage:
|
|
1455
|
+
::
|
|
1456
|
+
bkg_image_info = api.bkg_image_delete(obId, 'user_bkg_image|00c98a6dde73f309f058378399cad019f8405603c63d74b5f2ba342a8ae0a8f1')
|
|
1457
|
+
|
|
1458
|
+
bkg_image_list = api.bkg_image_get_ob_image_list(obId)
|
|
1459
|
+
first_image=list(bkg_image_list.keys())[0]
|
|
1460
|
+
fits_hash=bkg_image_list[first_image]['fits_hash']
|
|
1461
|
+
bkg_image_list = api.bkg_image_delete(obId, 'user_bkg_image|%s' %(fits_hash))
|
|
1462
|
+
"""
|
|
1463
|
+
ob_url=self.apiUrl + '/obsBlocks/%d' % obId
|
|
1464
|
+
return self.p2fc_request('DELETE', 'bkg_image_delete?url=%s&access_token=%s&fits_hash=%s' %(ob_url, self.access_token, fits_hash))
|
|
1465
|
+
|
|
1466
|
+
|
|
1467
|
+
def p2fc_request(self, method, url='', data=None, files=None) :
|
|
1468
|
+
r = self.session.request(method, "%s/%s" %(self.p2fc_url, url), data=data, files=files)
|
|
1469
|
+
|
|
1470
|
+
content_type = r.headers['Content-Type'].split(';')[0]
|
|
1471
|
+
# handle response
|
|
1472
|
+
if 200 <= r.status_code < 300:
|
|
1473
|
+
if content_type == 'application/json':
|
|
1474
|
+
data = r.json()
|
|
1475
|
+
return data
|
|
1476
|
+
return None
|
|
1477
|
+
elif content_type == 'application/json' and 'error' in r.json():
|
|
1478
|
+
raise P2Error(r.status_code, 'POST', self.p2fc_url, r.json()['error'])
|
|
1479
|
+
else:
|
|
1480
|
+
raise P2Error(r.status_code, 'POST', self.p2fc_url, r.text)
|
|
1481
|
+
|
|
1482
|
+
|
|
1483
|
+
def getFindingChart(self, obId, index, filename):
|
|
1484
|
+
"""Download finding chart save it into the given filename.
|
|
1485
|
+
|
|
1486
|
+
api: GET /obsBlocks/{obId}/findingCharts/{index}
|
|
1487
|
+
|
|
1488
|
+
usage:
|
|
1489
|
+
::
|
|
1490
|
+
|
|
1491
|
+
api.getFindingChart(obId, 1, 'fc1.jpg')
|
|
1492
|
+
"""
|
|
1493
|
+
url = '/obsBlocks/%d/findingCharts/%d' % (obId, index)
|
|
1494
|
+
return self.downloadBinaryFile(url, filename)
|
|
1495
|
+
|
|
1496
|
+
def deleteFindingChart(self, obId, index):
|
|
1497
|
+
"""Delete finding chart.
|
|
1498
|
+
|
|
1499
|
+
The finding chart at the given index is deleted.
|
|
1500
|
+
Finding charts with a higher index 'slide down',
|
|
1501
|
+
i.e. finding charts are always numbered consecutively
|
|
1502
|
+
starting with 1.
|
|
1503
|
+
|
|
1504
|
+
The OB must have the latest ipVersion.
|
|
1505
|
+
|
|
1506
|
+
Service Mode: The OB must have status (-) or (P).
|
|
1507
|
+
|
|
1508
|
+
api: DELETE /obsBlocks/{obId}/findingCharts/{index}
|
|
1509
|
+
|
|
1510
|
+
usage:
|
|
1511
|
+
::
|
|
1512
|
+
|
|
1513
|
+
api.deleteFindingChart(obId, 1)
|
|
1514
|
+
"""
|
|
1515
|
+
return self.delete('/obsBlocks/%d/findingCharts/%d' % (obId, index))
|
|
1516
|
+
|
|
1517
|
+
def duplicateFindingChart(self, obId, index, destinationObId):
|
|
1518
|
+
"""Duplicate this finding chart to another OB.
|
|
1519
|
+
|
|
1520
|
+
Attach this finding chart to a different OB.
|
|
1521
|
+
The finding chart is appended at the end of the target OB's
|
|
1522
|
+
finding chart list.
|
|
1523
|
+
|
|
1524
|
+
The target OB must have the latest ipVersion.
|
|
1525
|
+
|
|
1526
|
+
Visitor Mode: The target OB can have any status.
|
|
1527
|
+
|
|
1528
|
+
Service Mode: The target OB must have status (-) or (P).
|
|
1529
|
+
|
|
1530
|
+
api: POST /obsBlocks/{obId}/findingCharts/{index}/duplicate
|
|
1531
|
+
|
|
1532
|
+
usage:
|
|
1533
|
+
::
|
|
1534
|
+
|
|
1535
|
+
api.duplicateFindingChart(obId, 1, destinationObId)
|
|
1536
|
+
"""
|
|
1537
|
+
return self.post('/obsBlocks/%d/findingCharts/%d/duplicate' % (obId, index), {'obId': destinationObId})
|
|
1538
|
+
|
|
1539
|
+
def getOBSchedulingInfo(self, obId):
|
|
1540
|
+
"""Get OB scheduling information.
|
|
1541
|
+
|
|
1542
|
+
api: GET /obsBlocks/{obId}/schedule
|
|
1543
|
+
|
|
1544
|
+
usage:
|
|
1545
|
+
::
|
|
1546
|
+
|
|
1547
|
+
obInfo, obInfoVersion = api.getOBSchedulingInfo(obId)
|
|
1548
|
+
"""
|
|
1549
|
+
return self.get('/obsBlocks/%d/schedule' % obId)
|
|
1550
|
+
|
|
1551
|
+
def saveOBSchedulingInfo(self, obId, obInfo, version):
|
|
1552
|
+
"""Update OB scheduling information.
|
|
1553
|
+
|
|
1554
|
+
api: PUT /obsBlocks/{obId}/schedule
|
|
1555
|
+
|
|
1556
|
+
usage:
|
|
1557
|
+
::
|
|
1558
|
+
|
|
1559
|
+
obInfo, obInfoVersion = api.getOBSchedulingInfo(obId)
|
|
1560
|
+
# change something
|
|
1561
|
+
obInfo['targetName'] = 'My Best Target'
|
|
1562
|
+
api.saveOBSchedulingInfo(obId, obInfo, obInfoVersion)
|
|
1563
|
+
"""
|
|
1564
|
+
return self.put('/obsBlocks/%d/schedule' % obId, obInfo, etag=version)
|
|
1565
|
+
|
|
1566
|
+
def getContainerSchedulingInfo(self, containerId):
|
|
1567
|
+
"""Get scheduling information of a Concatenation inside a Group or TimeLink
|
|
1568
|
+
|
|
1569
|
+
api: GET /containers/{containerId}/schedule
|
|
1570
|
+
|
|
1571
|
+
usage:
|
|
1572
|
+
::
|
|
1573
|
+
|
|
1574
|
+
containerInfo, containerInfoVersion = api.getContainerSchedulingInfo(containerId)
|
|
1575
|
+
"""
|
|
1576
|
+
return self.get('/containers/%d/schedule' % containerId)
|
|
1577
|
+
|
|
1578
|
+
def saveContainerSchedulingInfo(self, containerId, containerInfo, version):
|
|
1579
|
+
"""Update OB scheduling information.
|
|
1580
|
+
|
|
1581
|
+
api: PUT /containers/%d/schedule
|
|
1582
|
+
|
|
1583
|
+
usage:
|
|
1584
|
+
::
|
|
1585
|
+
|
|
1586
|
+
containerInfo, containerInfoVersion = api.getContainerSchedulingInfo(containerId)
|
|
1587
|
+
# change something
|
|
1588
|
+
containerInfo['groupContribution'] = 50
|
|
1589
|
+
api.saveContainerSchedulingInfo(containerId, containerInfo, containerInfoVersion)
|
|
1590
|
+
"""
|
|
1591
|
+
return self.put('/containers/%d/schedule' % containerId, containerInfo, etag=version)
|
|
1592
|
+
|
|
1593
|
+
def getContainerItemsSchedulingInfo(self, containerId):
|
|
1594
|
+
"""Get scheduling information of all items (OBs or containers) in a container
|
|
1595
|
+
|
|
1596
|
+
api: GET /containers/{containerId}/items/schedule
|
|
1597
|
+
|
|
1598
|
+
usage:
|
|
1599
|
+
::
|
|
1600
|
+
|
|
1601
|
+
containerInfo, containerInfoVersion = api.getContainerItemsSchedulingInfo(containerId)
|
|
1602
|
+
"""
|
|
1603
|
+
return self.get('/containers/%d/items/scheduling' % containerId)
|
|
1604
|
+
|
|
1605
|
+
# ---------- VISITOR EXECUTION SEQUENCE APIs ----------
|
|
1606
|
+
def getExecutionSequence(self, instrument):
|
|
1607
|
+
"""Retrieve the personal execution sequence for an instrument.
|
|
1608
|
+
|
|
1609
|
+
api: GET /executionSequences/{instrument}
|
|
1610
|
+
|
|
1611
|
+
usage:
|
|
1612
|
+
::
|
|
1613
|
+
|
|
1614
|
+
executionSequence, esVersion = api.getExecutionSequence('UVES')
|
|
1615
|
+
"""
|
|
1616
|
+
return self.get('/executionSequences/%s' % instrument)
|
|
1617
|
+
|
|
1618
|
+
def saveExecutionSequence(self, instrument, executionSequence, version):
|
|
1619
|
+
"""Change personal visitor execution sequence for an instrument.
|
|
1620
|
+
|
|
1621
|
+
The list should contain objects with an obId in the desired execution
|
|
1622
|
+
order and may also contain OBs from delegated observing runs. Only
|
|
1623
|
+
visitor mode OBs in status '+', 'A', 'X' or 'S' can be included.
|
|
1624
|
+
|
|
1625
|
+
The obId is the only required field in each list member, but all fields
|
|
1626
|
+
sent by a GET request can be included unchanged.
|
|
1627
|
+
|
|
1628
|
+
api: PUT /executionSequences/{instrument}
|
|
1629
|
+
|
|
1630
|
+
usage:
|
|
1631
|
+
::
|
|
1632
|
+
|
|
1633
|
+
executionSequence, esVersion = api.getExecutionSequence('UVES')
|
|
1634
|
+
executionSequence, esVersion = api.saveExecutionSequence('UVES',
|
|
1635
|
+
[{ 'obId': ob1 }, { 'obId': ob2 }], esVersion)
|
|
1636
|
+
print('OBs in UVES execution sequence', ', '.join(str(e['obId']) for e in executionSequence))
|
|
1637
|
+
"""
|
|
1638
|
+
return self.put('/executionSequences/%s' % instrument, executionSequence, version)
|
|
1639
|
+
|
|
1640
|
+
# ---------- INSTRUMENT PACKAGE APIs ----------
|
|
1641
|
+
def getInstrumentConstraints(self, instrument, ipVersion):
|
|
1642
|
+
"""Get all observing constraints that OBs using this instrument package
|
|
1643
|
+
must define.
|
|
1644
|
+
|
|
1645
|
+
The information returned is useful for building a user interface and
|
|
1646
|
+
also contains allowed values or ranges.
|
|
1647
|
+
|
|
1648
|
+
api: GET /instrumentPackages/{instrument}/{ipVersion}/instrumentConstraints
|
|
1649
|
+
|
|
1650
|
+
usage:
|
|
1651
|
+
::
|
|
1652
|
+
|
|
1653
|
+
insConstraints, _ = api.getInstrumentConstraints('UVES', '98.09')
|
|
1654
|
+
"""
|
|
1655
|
+
return self.get('/instrumentPackages/%s/%s/instrumentConstraints' % (instrument, ipVersion))
|
|
1656
|
+
|
|
1657
|
+
def getTemplateSignatures(self, instrument, ipVersion, phase='phase2' ):
|
|
1658
|
+
"""List all template signatures that can be attached to OBs using this
|
|
1659
|
+
instrument package.
|
|
1660
|
+
|
|
1661
|
+
api: GET /instrumentPackages/{instrument}/{ipVersion}/templateSignatures
|
|
1662
|
+
|
|
1663
|
+
usage:
|
|
1664
|
+
::
|
|
1665
|
+
|
|
1666
|
+
tplSignatures, _ = api.getTemplateSignatures('UVES', '98.09')
|
|
1667
|
+
tplSignatures, _ = api.getTemplateSignatures('UVES', '98.09', 'phase1')
|
|
1668
|
+
tplSignatures, _ = api.getTemplateSignatures('UVES', '98.09', 'phase2')
|
|
1669
|
+
"""
|
|
1670
|
+
assert phase in ['phase1','phase2']
|
|
1671
|
+
return self.get('/instrumentPackages/%s/%s/templateSignatures?for=%s' % (instrument, ipVersion, phase))
|
|
1672
|
+
|
|
1673
|
+
def getTemplateSignature(self, instrument, ipVersion, templateName):
|
|
1674
|
+
"""Get a specific template signature.
|
|
1675
|
+
|
|
1676
|
+
api: GET /instrumentPackages/{instrument}/{ipVersion}/templateSignatures/{templateName}
|
|
1677
|
+
|
|
1678
|
+
usage:
|
|
1679
|
+
::
|
|
1680
|
+
|
|
1681
|
+
tplSignature, _ = api.getTemplateSignature('UVES', '98.08', 'UVES_blue_acq_slit')
|
|
1682
|
+
"""
|
|
1683
|
+
return self.get('/instrumentPackages/%s/%s/templateSignatures/%s' % (instrument, ipVersion, templateName))
|
|
1684
|
+
|
|
1685
|
+
def getInstrumentPackageVersions(self, instrument):
|
|
1686
|
+
"""Get list of instrument package versions.
|
|
1687
|
+
|
|
1688
|
+
api: GET /instrumentPackages/{instrument}
|
|
1689
|
+
|
|
1690
|
+
usage:
|
|
1691
|
+
::
|
|
1692
|
+
|
|
1693
|
+
ipVersions, _ = api.getInstrumentPackageVersions('UVES')
|
|
1694
|
+
"""
|
|
1695
|
+
return self.get('/instrumentPackages/%s/' % instrument)
|
|
1696
|
+
|
|
1697
|
+
# ---------- USER APIs ----------
|
|
1698
|
+
def getUser(self):
|
|
1699
|
+
"""Get information about the logged-in user.
|
|
1700
|
+
|
|
1701
|
+
api: GET /authenticatedUser
|
|
1702
|
+
|
|
1703
|
+
usage:
|
|
1704
|
+
::
|
|
1705
|
+
|
|
1706
|
+
user, _ = api.getUser()
|
|
1707
|
+
"""
|
|
1708
|
+
return self.get('/authenticatedUser')
|
|
1709
|
+
|
|
1710
|
+
# ---------- private methods ----------
|
|
1711
|
+
# Retry Handling for requests
|
|
1712
|
+
# See https://www.peterbe.com/plog/best-practice-with-retries-with-requests
|
|
1713
|
+
# Or https://findwork.dev/blog/advanced-usage-python-requests-timeouts-retries-hooks/
|
|
1714
|
+
def requests_retry_session(self):
|
|
1715
|
+
adapter = requests.adapters.HTTPAdapter(max_retries=3)
|
|
1716
|
+
session = requests.Session()
|
|
1717
|
+
session.mount('https://', adapter)
|
|
1718
|
+
return session
|
|
1719
|
+
|
|
1720
|
+
def request(self, method, url, data=None, etag=None):
|
|
1721
|
+
self.request_count += 1
|
|
1722
|
+
|
|
1723
|
+
# configure request headers
|
|
1724
|
+
assert self.access_token is not None
|
|
1725
|
+
headers = {
|
|
1726
|
+
'Authorization': 'Bearer ' + self.access_token,
|
|
1727
|
+
'Accept': 'application/json'
|
|
1728
|
+
}
|
|
1729
|
+
if data is not None:
|
|
1730
|
+
headers['Content-Type'] = 'application/json'
|
|
1731
|
+
if etag is not None:
|
|
1732
|
+
headers['If-Match'] = etag
|
|
1733
|
+
|
|
1734
|
+
# make request
|
|
1735
|
+
url = self.apiUrl + url
|
|
1736
|
+
if self.debug:
|
|
1737
|
+
print(method, url, data)
|
|
1738
|
+
r = self.session.request(method, url, headers=headers, data=json.dumps(data))
|
|
1739
|
+
content_type = r.headers['Content-Type'].split(';')[0]
|
|
1740
|
+
etag = r.headers.get('ETag', None)
|
|
1741
|
+
|
|
1742
|
+
# handle response
|
|
1743
|
+
if 200 <= r.status_code < 300:
|
|
1744
|
+
if content_type == 'application/json':
|
|
1745
|
+
data = r.json()
|
|
1746
|
+
return data, etag
|
|
1747
|
+
return None, etag
|
|
1748
|
+
elif content_type == 'application/json' and 'error' in r.json():
|
|
1749
|
+
raise P2Error(r.status_code, method, url, r.json()['error'])
|
|
1750
|
+
else:
|
|
1751
|
+
raise P2Error(r.status_code, method, url, 'oops unknown error')
|
|
1752
|
+
|
|
1753
|
+
def uploadFile(self, method, url, filename, contentType, etag=None):
|
|
1754
|
+
basename = os.path.basename(filename)
|
|
1755
|
+
assert self.access_token is not None
|
|
1756
|
+
headers = {
|
|
1757
|
+
'Authorization': 'Bearer ' + self.access_token,
|
|
1758
|
+
'Content-Disposition': 'inline; filename="%s"' % basename,
|
|
1759
|
+
'Content-Type': contentType
|
|
1760
|
+
}
|
|
1761
|
+
if etag is not None:
|
|
1762
|
+
headers['If-Match'] = etag
|
|
1763
|
+
with open(filename, 'rb') as f:
|
|
1764
|
+
r = self.session.request(method, self.apiUrl + url, data=f, headers=headers)
|
|
1765
|
+
if r.status_code == 201 or r.status_code == 204:
|
|
1766
|
+
etag = r.headers.get('ETag', None)
|
|
1767
|
+
return basename, etag
|
|
1768
|
+
else:
|
|
1769
|
+
content_type = r.headers['Content-Type'].split(';')[0]
|
|
1770
|
+
if content_type == 'application/json' and 'error' in r.json():
|
|
1771
|
+
raise P2Error(r.status_code, method, url, r.json()['error'])
|
|
1772
|
+
else:
|
|
1773
|
+
raise P2Error(r.status_code, method, url, 'oops unknown error')
|
|
1774
|
+
|
|
1775
|
+
def downloadTextFile(self, url, filename):
|
|
1776
|
+
assert self.access_token is not None
|
|
1777
|
+
headers = {
|
|
1778
|
+
'Authorization': 'Bearer ' + self.access_token,
|
|
1779
|
+
'Accept': 'text/plain'
|
|
1780
|
+
}
|
|
1781
|
+
r = self.session.request('GET', self.apiUrl + url, stream=True, headers=headers)
|
|
1782
|
+
with open(filename, 'wb') as f:
|
|
1783
|
+
for line in r.iter_lines():
|
|
1784
|
+
f.write(line + b'\n')
|
|
1785
|
+
return None, r.headers.get('ETag', None)
|
|
1786
|
+
|
|
1787
|
+
def downloadBinaryFile(self, url, filename):
|
|
1788
|
+
assert self.access_token is not None
|
|
1789
|
+
headers = {
|
|
1790
|
+
'Authorization': 'Bearer ' + self.access_token
|
|
1791
|
+
}
|
|
1792
|
+
r = self.session.request('GET', self.apiUrl + url, stream=True, headers=headers)
|
|
1793
|
+
with open(filename, 'wb') as f:
|
|
1794
|
+
for chunk in r.iter_content(chunk_size=128):
|
|
1795
|
+
f.write(chunk)
|
|
1796
|
+
return None, None
|
|
1797
|
+
|
|
1798
|
+
def get(self, url):
|
|
1799
|
+
return self.request('GET', url)
|
|
1800
|
+
|
|
1801
|
+
def put(self, url, data, etag=None):
|
|
1802
|
+
return self.request('PUT', url, data, etag)
|
|
1803
|
+
|
|
1804
|
+
def post(self, url, data=None, etag=None):
|
|
1805
|
+
return self.request('POST', url, data, etag)
|
|
1806
|
+
|
|
1807
|
+
def delete(self, url, etag=None):
|
|
1808
|
+
return self.request('DELETE', url, etag=etag)
|