a2p2 0.2.14__py3-none-any.whl → 0.7.4__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.
- a2p2/__main__.py +40 -1
- a2p2/chara/facility.py +54 -4
- a2p2/chara/gui.py +31 -5
- a2p2/client.py +158 -31
- a2p2/facility.py +14 -1
- a2p2/gui.py +46 -12
- a2p2/instrument.py +3 -0
- a2p2/jmmc/__init__.py +7 -0
- a2p2/jmmc/catalogs.py +129 -0
- a2p2/jmmc/generated_models.py +191 -0
- a2p2/jmmc/models.py +104 -0
- a2p2/jmmc/services.py +16 -0
- a2p2/jmmc/utils.py +130 -0
- a2p2/jmmc/webservices.py +48 -0
- a2p2/ob.py +98 -9
- a2p2/samp.py +20 -0
- a2p2/version.py +210 -131
- a2p2/vlti/conf/GRAVITY_ditTable.json +21 -19
- a2p2/vlti/conf/GRAVITY_rangeTable.json +200 -28
- a2p2/vlti/conf/MATISSE_rangeTable.json +58 -22
- a2p2/vlti/conf/PIONIER_ditTable.json +1 -1
- a2p2/vlti/conf/PIONIER_rangeTable.json +16 -18
- a2p2/vlti/facility.py +160 -43
- a2p2/vlti/gravity.py +243 -311
- a2p2/vlti/gui.py +165 -39
- a2p2/vlti/instrument.py +266 -49
- a2p2/vlti/matisse.py +61 -147
- a2p2/vlti/pionier.py +34 -157
- {a2p2-0.2.14.dist-info → a2p2-0.7.4.dist-info}/METADATA +34 -20
- a2p2-0.7.4.dist-info/RECORD +39 -0
- {a2p2-0.2.14.dist-info → a2p2-0.7.4.dist-info}/WHEEL +1 -1
- {a2p2-0.2.14.dist-info → a2p2-0.7.4.dist-info}/entry_points.txt +0 -1
- a2p2/vlti/confP104/GRAVITY_ditTable.json +0 -122
- a2p2/vlti/confP104/GRAVITY_rangeTable.json +0 -202
- a2p2/vlti/confP104/MATISSE_ditTable.json +0 -2
- a2p2/vlti/confP104/MATISSE_rangeTable.json +0 -202
- a2p2/vlti/confP104/PIONIER_ditTable.json +0 -77
- a2p2/vlti/confP104/PIONIER_rangeTable.json +0 -118
- a2p2/vlti/confP105/GRAVITY_ditTable.json +0 -37
- a2p2/vlti/confP105/GRAVITY_rangeTable.json +0 -42
- a2p2/vlti/confP105/MATISSE_ditTable.json +0 -2
- a2p2/vlti/confP105/MATISSE_rangeTable.json +0 -44
- a2p2/vlti/confP105/PIONIER_ditTable.json +0 -25
- a2p2/vlti/confP105/PIONIER_rangeTable.json +0 -38
- a2p2-0.2.14.dist-info/RECORD +0 -44
- {a2p2-0.2.14.dist-info → a2p2-0.7.4.dist-info}/LICENSE +0 -0
- {a2p2-0.2.14.dist-info → a2p2-0.7.4.dist-info}/top_level.txt +0 -0
a2p2/jmmc/utils.py
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
|
3
|
+
from urllib3.util.retry import Retry
|
4
|
+
from requests.adapters import HTTPAdapter
|
5
|
+
from requests.auth import HTTPBasicAuth
|
6
|
+
import requests
|
7
|
+
|
8
|
+
from a2p2.client import A2P2ClientPreferences
|
9
|
+
__all__ = []
|
10
|
+
|
11
|
+
import logging
|
12
|
+
|
13
|
+
logger = logging.getLogger(__name__)
|
14
|
+
|
15
|
+
# Do not use next code directly - this is dev related code.
|
16
|
+
# Please have a look on the other jmmc modules for client code.
|
17
|
+
|
18
|
+
|
19
|
+
# Retry Handling for requests
|
20
|
+
# See https://www.peterbe.com/plog/best-practice-with-retries-with-requests
|
21
|
+
# Or https://findwork.dev/blog/advanced-usage-python-requests-timeouts-retries-hooks/
|
22
|
+
|
23
|
+
|
24
|
+
# TODO check that we return more infiormation for 500 error case because some server side code part do return 500
|
25
|
+
# and here we may
|
26
|
+
|
27
|
+
def requests_retry_session(
|
28
|
+
retries=3,
|
29
|
+
backoff_factor=1.0,
|
30
|
+
status_forcelist=(500, 502, 504)):
|
31
|
+
|
32
|
+
adapter = HTTPAdapter()
|
33
|
+
adapter.max_retries = Retry(
|
34
|
+
total=retries,
|
35
|
+
read=retries,
|
36
|
+
connect=retries,
|
37
|
+
backoff_factor=backoff_factor,
|
38
|
+
status_forcelist=status_forcelist,
|
39
|
+
respect_retry_after_header=False
|
40
|
+
)
|
41
|
+
session = requests.Session()
|
42
|
+
session.mount('http://', adapter)
|
43
|
+
session.mount('https://', adapter)
|
44
|
+
return session
|
45
|
+
|
46
|
+
# r = requests.request(method, url, headers=headers, data=json.dumps(data))
|
47
|
+
# -> r = requests_session.request(method, url, headers=headers, data=json.dumps(data))
|
48
|
+
|
49
|
+
|
50
|
+
class JmmcAPI():
|
51
|
+
def __init__(self, rootURL, username=None, password=None):
|
52
|
+
self.rootURL = rootURL
|
53
|
+
|
54
|
+
# credentials can be given :
|
55
|
+
# by parameters
|
56
|
+
# if username or password is None, try to get it from the a2p2 preferences ( run: a2p2 -c )
|
57
|
+
# if username or password still is None keep auth to None.
|
58
|
+
# a None auth will search for ~/.netrc files trying to fix 401 accesses
|
59
|
+
|
60
|
+
prefs = A2P2ClientPreferences()
|
61
|
+
if not username :
|
62
|
+
username = prefs.getJmmcLogin()
|
63
|
+
if not password :
|
64
|
+
password = prefs.getJmmcPassword()
|
65
|
+
|
66
|
+
# if we got complete credential
|
67
|
+
if username and password:
|
68
|
+
self.auth = HTTPBasicAuth(username, password)
|
69
|
+
else:
|
70
|
+
self.auth = None
|
71
|
+
|
72
|
+
self.requests_session = requests_retry_session()
|
73
|
+
|
74
|
+
logger.debug("Instanciating JmmcAPI on '%s'" % self.rootURL)
|
75
|
+
|
76
|
+
def _get(self, url):
|
77
|
+
return self._request('GET', url)
|
78
|
+
|
79
|
+
def _put(self, url, json):
|
80
|
+
return self._request('PUT', url, json)
|
81
|
+
|
82
|
+
def _post(self, url, json):
|
83
|
+
return self._request('POST', url, json)
|
84
|
+
|
85
|
+
def _post(self, url, **kwargs):
|
86
|
+
return self._request2('POST', url, **kwargs)
|
87
|
+
|
88
|
+
def _request2(self, method, url, **kwargs):
|
89
|
+
logger.info("performing %s request on %s" % (method, self.rootURL+url))
|
90
|
+
r = self.requests_session.request(
|
91
|
+
method, self.rootURL+url, **kwargs)
|
92
|
+
# handle response if any or throw an exception
|
93
|
+
if (r.status_code == 204): # No Content : everything is fine
|
94
|
+
return
|
95
|
+
elif 200 <= r.status_code < 300:
|
96
|
+
if 'Content-Type' in r.headers.keys() and 'application/json' in r.headers['Content-Type']:
|
97
|
+
return r.json()
|
98
|
+
else:
|
99
|
+
return r.content
|
100
|
+
# TODO enhance error handling ? Throw an exception ....
|
101
|
+
error = []
|
102
|
+
error.append("status_code is %s"%r.status_code)
|
103
|
+
if r.reason :
|
104
|
+
error.append(r.reason)
|
105
|
+
if "X-Http-Error-Description" in r.headers.keys():
|
106
|
+
error.append(r.headers["X-Http-Error-Description"])
|
107
|
+
|
108
|
+
raise Exception(error)
|
109
|
+
|
110
|
+
def _request(self, method, url, json=None, data=None, files=None):
|
111
|
+
logger.info("performing %s request on %s" % (method, self.rootURL+url))
|
112
|
+
r = self.requests_session.request(
|
113
|
+
method, self.rootURL+url, auth=self.auth, json=json, data=data, files=files)
|
114
|
+
# handle response if any or throw an exception
|
115
|
+
if (r.status_code == 204): # No Content : everything is fine
|
116
|
+
return
|
117
|
+
elif 200 <= r.status_code < 300:
|
118
|
+
if 'Content-Type' in r.headers.keys() and 'application/json' in r.headers['Content-Type']:
|
119
|
+
return r.json()
|
120
|
+
else:
|
121
|
+
return r.content
|
122
|
+
# TODO enhance error handling ? Throw an exception ....
|
123
|
+
error = []
|
124
|
+
error.append("status_code is %s"%r.status_code)
|
125
|
+
if r.reason :
|
126
|
+
error.append(r.reason)
|
127
|
+
if "X-Http-Error-Description" in r.headers.keys():
|
128
|
+
error.append(r.headers["X-Http-Error-Description"])
|
129
|
+
|
130
|
+
raise Exception(error)
|
a2p2/jmmc/webservices.py
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
|
3
|
+
__all__ = []
|
4
|
+
|
5
|
+
import logging
|
6
|
+
import json
|
7
|
+
|
8
|
+
from .utils import JmmcAPI
|
9
|
+
from . import PRODLABEL
|
10
|
+
|
11
|
+
logger = logging.getLogger(__name__)
|
12
|
+
|
13
|
+
class CallIper():
|
14
|
+
""" Call IPER using its 'API' through simple methods.
|
15
|
+
|
16
|
+
from a2p2.jmmc import CallIper
|
17
|
+
calliper = CallIper()
|
18
|
+
diskresult = caliper.runDiskFit(pathname)
|
19
|
+
ud = diskresult["diameter"]
|
20
|
+
|
21
|
+
"""
|
22
|
+
def __init__(self, prod=False, apiUrl=None):
|
23
|
+
self.prod = prod
|
24
|
+
self.serviceName = "callIper"
|
25
|
+
|
26
|
+
# Manage prod & preprod or user provided access points
|
27
|
+
if apiUrl:
|
28
|
+
self.apiUrl = apiUrl # trust given url as callIperAPI if value is provided
|
29
|
+
elif self.prod:
|
30
|
+
self.apiUrl = "" # no api in production yet
|
31
|
+
raise Warning("sorry, no api for production yet")
|
32
|
+
else:
|
33
|
+
self.apiUrl = "http://apps.jmmc.fr/~mellag/LITproWebService/run.php"
|
34
|
+
|
35
|
+
self.api = JmmcAPI(self.apiUrl)
|
36
|
+
|
37
|
+
logger.info("Create calliper client to access '%s' (%s API at %s)" %
|
38
|
+
(self.serviceName, PRODLABEL[self.prod], self.api.rootURL))
|
39
|
+
|
40
|
+
# API will evolve in the future with parameter to select fitter, model and data to fit...
|
41
|
+
def runDiskFit(self, oifitsFilename, jsonOutput=True):
|
42
|
+
""" Ask CallIper to fit given data for a simple disk """
|
43
|
+
files = {'userfile': (oifitsFilename, open(oifitsFilename, 'rb'))}
|
44
|
+
data = {'method': 'runJsonFit'}
|
45
|
+
res=self.api._post("", data=data, files=files)
|
46
|
+
if jsonOutput:
|
47
|
+
return json.loads(res)
|
48
|
+
return res
|
a2p2/ob.py
CHANGED
@@ -2,9 +2,16 @@
|
|
2
2
|
|
3
3
|
__all__ = []
|
4
4
|
|
5
|
+
import logging
|
5
6
|
import json
|
6
7
|
import xml.etree.ElementTree as ET
|
7
8
|
from collections import defaultdict, namedtuple, OrderedDict
|
9
|
+
import re
|
10
|
+
|
11
|
+
import copy
|
12
|
+
|
13
|
+
|
14
|
+
logger = logging.getLogger(__name__)
|
8
15
|
|
9
16
|
# https://stackoverflow.com/questions/2148119/how-to-convert-an-xml-string-to-a-dictionary-in-python
|
10
17
|
# see comment below for our custom mods on attributes naming
|
@@ -19,8 +26,11 @@ def etree_to_dict(t):
|
|
19
26
|
d = {t.tag: {k: v[0] if len(v) == 1 else v for k, v in dd.items()}}
|
20
27
|
# print("add d=%s"%str(d))
|
21
28
|
if t.attrib:
|
22
|
-
|
23
|
-
|
29
|
+
attrs = {}
|
30
|
+
for k, v in t.attrib.items():
|
31
|
+
if not "{" in k[0]: # ignore elementName with prefix
|
32
|
+
attrs[k]=v
|
33
|
+
d[t.tag].update(attrs)
|
24
34
|
if t.text:
|
25
35
|
text = t.text.strip()
|
26
36
|
if children or t.attrib:
|
@@ -30,6 +40,66 @@ def etree_to_dict(t):
|
|
30
40
|
d[t.tag] = text
|
31
41
|
return d
|
32
42
|
|
43
|
+
def normalizeObj(obj):
|
44
|
+
"""
|
45
|
+
Modify in place the structure if required.
|
46
|
+
Note:
|
47
|
+
dict key must be valid identifier (only tested for EXTRA_INFORMATIONS created fields)
|
48
|
+
"""
|
49
|
+
otype=type(obj)
|
50
|
+
if otype is dict:
|
51
|
+
for k,v in obj.items():
|
52
|
+
|
53
|
+
if k in ['observationConfiguration', 'HAinterval', 'LSTinterval'] and not isinstance(v, list):
|
54
|
+
# always defines observationConfiguration as a list
|
55
|
+
normalizeObj(v)
|
56
|
+
obj[k]=[v]
|
57
|
+
elif k=='EXTRA_INFORMATIONS':
|
58
|
+
#print(f"Updating dict {k} was : {obj[k]}")
|
59
|
+
# normalize input that may have various form on list of dict
|
60
|
+
try:
|
61
|
+
fields=[]
|
62
|
+
funits={}
|
63
|
+
for fk in ["parameter","field"]:
|
64
|
+
if fk in obj[k]:
|
65
|
+
l=obj[k][fk].copy()
|
66
|
+
#print(f"'{fk}' => {l}\n({type(l)})\n")
|
67
|
+
if type(l) is dict:
|
68
|
+
fields.append(l)
|
69
|
+
else:
|
70
|
+
fields+=l
|
71
|
+
|
72
|
+
#print(f"Fields to update: {fields}")
|
73
|
+
#store new fields
|
74
|
+
for vn in fields:
|
75
|
+
fname=vn["name"]
|
76
|
+
# test if we have to normalize field name that will become a python field
|
77
|
+
if not fname.isidentifier():
|
78
|
+
# replace unvalid chars by _ and append F to avoid starting by _
|
79
|
+
fname=re.sub('^_','F_', re.sub('\W|^(?=\d)','_', fname))
|
80
|
+
obj[k][fname]=vn["value"]
|
81
|
+
if "unit" in vn:
|
82
|
+
funits[fname]=vn["unit"]
|
83
|
+
#store units
|
84
|
+
obj[k]["field_units"]=funits
|
85
|
+
# remove old arrays or dicts
|
86
|
+
for fk in ["parameter","field"]:
|
87
|
+
if fk in obj[k]:
|
88
|
+
del obj[k][fk]
|
89
|
+
pass
|
90
|
+
except:
|
91
|
+
import traceback
|
92
|
+
print (traceback.format_exc())
|
93
|
+
print(f"\nCan't read {v}")
|
94
|
+
pass
|
95
|
+
else:
|
96
|
+
normalizeObj(v)
|
97
|
+
return obj
|
98
|
+
elif otype is list:
|
99
|
+
for e in obj:
|
100
|
+
normalizeObj(e)
|
101
|
+
else:
|
102
|
+
pass
|
33
103
|
|
34
104
|
class OB():
|
35
105
|
"""
|
@@ -42,29 +112,40 @@ class OB():
|
|
42
112
|
|
43
113
|
every values are string (must be converted for numeric values).
|
44
114
|
|
115
|
+
Following fields are converted to list even if the list get a single element. The OB code can then handle such multiples values.
|
116
|
+
['observationConfiguration', 'HAinterval', 'LSTinterval']
|
117
|
+
|
45
118
|
"""
|
46
119
|
|
47
120
|
def __init__(self, url):
|
48
121
|
# extract XML in elementTree
|
49
|
-
|
50
122
|
e = ET.parse(url)
|
123
|
+
# convert data to dict structure
|
51
124
|
d = etree_to_dict(e.getroot())
|
52
125
|
# keep only content of subelement to avoid schema version change
|
53
126
|
# '{http://www.jmmc.fr/aspro-ob/0.1}observingBlockDefinition'
|
54
127
|
ds = d[list(d)[0]] # -> version and name are lost
|
55
|
-
|
128
|
+
|
129
|
+
#self.srcDict = copy.deepcopy(ds)
|
130
|
+
normalizeObj(ds)
|
131
|
+
#self.normDict = ds
|
132
|
+
#self.srcObj = toObject(self.srcDict,inputName="OB")
|
133
|
+
#self.normObj = toObject(self.normDict,inputName="OB")
|
134
|
+
|
135
|
+
# store subelements as attributes
|
56
136
|
for e in ds.keys():
|
57
137
|
# parse JSON into an object with attributes corresponding to dict keys.
|
58
138
|
# We should probably avoid json use...
|
59
139
|
o = json.loads(
|
60
140
|
json.dumps(ds[e]), object_hook=lambda d: namedtuple(e, d.keys())(*d.values()))
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
else:
|
65
|
-
setattr(self, e, o)
|
141
|
+
setattr(self, e, o)
|
142
|
+
|
143
|
+
# store normalized source for str repr
|
66
144
|
self.ds = ds
|
67
145
|
|
146
|
+
def as_dict(self):
|
147
|
+
return self.ds
|
148
|
+
|
68
149
|
def getFluxes(self, target):
|
69
150
|
"""
|
70
151
|
Return a flux mesurements as dict (ordered dict by BVRIJHK ).
|
@@ -78,6 +159,14 @@ class OB():
|
|
78
159
|
|
79
160
|
return OrderedDict(sorted(fluxes.items(), key=lambda t: order.find(t[0])))
|
80
161
|
|
162
|
+
def getPeriod(self):
|
163
|
+
"""
|
164
|
+
Return period given by Aspro2 as integer.
|
165
|
+
"""
|
166
|
+
# use re to remove alpha characters from "Period 109" and make.
|
167
|
+
return int(re.sub(r"\D", "", self.interferometerConfiguration.version))
|
168
|
+
|
169
|
+
|
81
170
|
def get(self, obj, fieldname, defaultvalue=None):
|
82
171
|
if fieldname in obj._fields:
|
83
172
|
return getattr(obj, fieldname)
|
a2p2/samp.py
CHANGED
@@ -4,6 +4,9 @@ __all__ = []
|
|
4
4
|
|
5
5
|
from astropy.samp import SAMPIntegratedClient
|
6
6
|
from os import sep
|
7
|
+
import logging
|
8
|
+
|
9
|
+
logger = logging.getLogger(__name__)
|
7
10
|
|
8
11
|
class Receiver(object):
|
9
12
|
|
@@ -12,12 +15,14 @@ class Receiver(object):
|
|
12
15
|
self.received = False
|
13
16
|
|
14
17
|
def receive_call(self, private_key, sender_id, msg_id, mtype, params, extra):
|
18
|
+
self.mtype=mtype
|
15
19
|
self.params = params
|
16
20
|
self.received = True
|
17
21
|
self.client.reply(
|
18
22
|
msg_id, {"samp.status": "samp.ok", "samp.result": {}})
|
19
23
|
|
20
24
|
def receive_notification(self, private_key, sender_id, mtype, params, extra):
|
25
|
+
self.mtype=mtype
|
21
26
|
self.params = params
|
22
27
|
self.received = True
|
23
28
|
|
@@ -44,6 +49,7 @@ class A2p2SampClient():
|
|
44
49
|
# an error is thrown here if no hub is present
|
45
50
|
|
46
51
|
# TODO get samp client name and display it in the UI
|
52
|
+
# TODO !! declare mtypes as enum
|
47
53
|
|
48
54
|
# Instantiate the receiver
|
49
55
|
self.r = Receiver(self.sampClient)
|
@@ -51,6 +57,10 @@ class A2p2SampClient():
|
|
51
57
|
self.sampClient.bind_receive_call("ob.load.data", self.r.receive_call)
|
52
58
|
self.sampClient.bind_receive_notification(
|
53
59
|
"ob.load.data", self.r.receive_notification)
|
60
|
+
# Listen for Model related instructions
|
61
|
+
self.sampClient.bind_receive_call("fr.jmmc.litpro.start.setting",self.r.receive_call)
|
62
|
+
self.sampClient.bind_receive_notification(
|
63
|
+
"fr.jmmc.litpro.start.setting",self.r.receive_notification)
|
54
64
|
|
55
65
|
def disconnect(self):
|
56
66
|
self.sampClient.disconnect()
|
@@ -79,6 +89,9 @@ class A2p2SampClient():
|
|
79
89
|
def clear_message(self):
|
80
90
|
return self.r.clear()
|
81
91
|
|
92
|
+
def has_ob_message(self):
|
93
|
+
return "ob.load.data" in self.r.mtype
|
94
|
+
|
82
95
|
def get_ob_url(self):
|
83
96
|
url = self.r.params['url']
|
84
97
|
if url.startswith("file:///"):
|
@@ -89,3 +102,10 @@ class A2p2SampClient():
|
|
89
102
|
elif url.startswith("file:/"): # work arround bugged file urls on *nix
|
90
103
|
return url[5:]
|
91
104
|
return url
|
105
|
+
|
106
|
+
def has_model_message(self):
|
107
|
+
return "fr.jmmc.litpro.start.setting" in self.r.mtype
|
108
|
+
|
109
|
+
def get_model(self):
|
110
|
+
return self.r.params['model']
|
111
|
+
|