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.
Files changed (47) hide show
  1. a2p2/__main__.py +40 -1
  2. a2p2/chara/facility.py +54 -4
  3. a2p2/chara/gui.py +31 -5
  4. a2p2/client.py +158 -31
  5. a2p2/facility.py +14 -1
  6. a2p2/gui.py +46 -12
  7. a2p2/instrument.py +3 -0
  8. a2p2/jmmc/__init__.py +7 -0
  9. a2p2/jmmc/catalogs.py +129 -0
  10. a2p2/jmmc/generated_models.py +191 -0
  11. a2p2/jmmc/models.py +104 -0
  12. a2p2/jmmc/services.py +16 -0
  13. a2p2/jmmc/utils.py +130 -0
  14. a2p2/jmmc/webservices.py +48 -0
  15. a2p2/ob.py +98 -9
  16. a2p2/samp.py +20 -0
  17. a2p2/version.py +210 -131
  18. a2p2/vlti/conf/GRAVITY_ditTable.json +21 -19
  19. a2p2/vlti/conf/GRAVITY_rangeTable.json +200 -28
  20. a2p2/vlti/conf/MATISSE_rangeTable.json +58 -22
  21. a2p2/vlti/conf/PIONIER_ditTable.json +1 -1
  22. a2p2/vlti/conf/PIONIER_rangeTable.json +16 -18
  23. a2p2/vlti/facility.py +160 -43
  24. a2p2/vlti/gravity.py +243 -311
  25. a2p2/vlti/gui.py +165 -39
  26. a2p2/vlti/instrument.py +266 -49
  27. a2p2/vlti/matisse.py +61 -147
  28. a2p2/vlti/pionier.py +34 -157
  29. {a2p2-0.2.14.dist-info → a2p2-0.7.4.dist-info}/METADATA +34 -20
  30. a2p2-0.7.4.dist-info/RECORD +39 -0
  31. {a2p2-0.2.14.dist-info → a2p2-0.7.4.dist-info}/WHEEL +1 -1
  32. {a2p2-0.2.14.dist-info → a2p2-0.7.4.dist-info}/entry_points.txt +0 -1
  33. a2p2/vlti/confP104/GRAVITY_ditTable.json +0 -122
  34. a2p2/vlti/confP104/GRAVITY_rangeTable.json +0 -202
  35. a2p2/vlti/confP104/MATISSE_ditTable.json +0 -2
  36. a2p2/vlti/confP104/MATISSE_rangeTable.json +0 -202
  37. a2p2/vlti/confP104/PIONIER_ditTable.json +0 -77
  38. a2p2/vlti/confP104/PIONIER_rangeTable.json +0 -118
  39. a2p2/vlti/confP105/GRAVITY_ditTable.json +0 -37
  40. a2p2/vlti/confP105/GRAVITY_rangeTable.json +0 -42
  41. a2p2/vlti/confP105/MATISSE_ditTable.json +0 -2
  42. a2p2/vlti/confP105/MATISSE_rangeTable.json +0 -44
  43. a2p2/vlti/confP105/PIONIER_ditTable.json +0 -25
  44. a2p2/vlti/confP105/PIONIER_rangeTable.json +0 -38
  45. a2p2-0.2.14.dist-info/RECORD +0 -44
  46. {a2p2-0.2.14.dist-info → a2p2-0.7.4.dist-info}/LICENSE +0 -0
  47. {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)
@@ -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
- d[t.tag].update(('' + k, v)
23
- for k, v in t.attrib.items()) # was '@' but can't be serialized by namedtuple
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
- # store attributes
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
- # observationConfiguration may be uniq but force it to be a list
62
- if "observationConfiguration" in e and not isinstance(o, list):
63
- setattr(self, e, [o])
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
+