a2p2 0.2.15__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 (46) hide show
  1. a2p2/__main__.py +39 -0
  2. a2p2/chara/facility.py +51 -3
  3. a2p2/chara/gui.py +29 -5
  4. a2p2/client.py +144 -34
  5. a2p2/facility.py +11 -1
  6. a2p2/gui.py +32 -1
  7. a2p2/jmmc/__init__.py +7 -0
  8. a2p2/jmmc/catalogs.py +129 -0
  9. a2p2/jmmc/generated_models.py +191 -0
  10. a2p2/jmmc/models.py +104 -0
  11. a2p2/jmmc/services.py +16 -0
  12. a2p2/jmmc/utils.py +130 -0
  13. a2p2/jmmc/webservices.py +48 -0
  14. a2p2/ob.py +95 -9
  15. a2p2/samp.py +17 -0
  16. a2p2/version.py +205 -137
  17. a2p2/vlti/conf/GRAVITY_ditTable.json +21 -19
  18. a2p2/vlti/conf/GRAVITY_rangeTable.json +200 -28
  19. a2p2/vlti/conf/MATISSE_rangeTable.json +58 -22
  20. a2p2/vlti/conf/PIONIER_ditTable.json +1 -1
  21. a2p2/vlti/conf/PIONIER_rangeTable.json +16 -18
  22. a2p2/vlti/facility.py +156 -118
  23. a2p2/vlti/gravity.py +243 -311
  24. a2p2/vlti/gui.py +162 -40
  25. a2p2/vlti/instrument.py +264 -49
  26. a2p2/vlti/matisse.py +61 -147
  27. a2p2/vlti/pionier.py +34 -157
  28. {a2p2-0.2.15.dist-info → a2p2-0.7.4.dist-info}/METADATA +34 -20
  29. a2p2-0.7.4.dist-info/RECORD +39 -0
  30. {a2p2-0.2.15.dist-info → a2p2-0.7.4.dist-info}/WHEEL +1 -1
  31. {a2p2-0.2.15.dist-info → a2p2-0.7.4.dist-info}/entry_points.txt +0 -1
  32. a2p2/vlti/confP104/GRAVITY_ditTable.json +0 -122
  33. a2p2/vlti/confP104/GRAVITY_rangeTable.json +0 -202
  34. a2p2/vlti/confP104/MATISSE_ditTable.json +0 -2
  35. a2p2/vlti/confP104/MATISSE_rangeTable.json +0 -202
  36. a2p2/vlti/confP104/PIONIER_ditTable.json +0 -77
  37. a2p2/vlti/confP104/PIONIER_rangeTable.json +0 -118
  38. a2p2/vlti/confP105/GRAVITY_ditTable.json +0 -37
  39. a2p2/vlti/confP105/GRAVITY_rangeTable.json +0 -42
  40. a2p2/vlti/confP105/MATISSE_ditTable.json +0 -2
  41. a2p2/vlti/confP105/MATISSE_rangeTable.json +0 -44
  42. a2p2/vlti/confP105/PIONIER_ditTable.json +0 -25
  43. a2p2/vlti/confP105/PIONIER_rangeTable.json +0 -38
  44. a2p2-0.2.15.dist-info/RECORD +0 -44
  45. {a2p2-0.2.15.dist-info → a2p2-0.7.4.dist-info}/LICENSE +0 -0
  46. {a2p2-0.2.15.dist-info → a2p2-0.7.4.dist-info}/top_level.txt +0 -0
a2p2/jmmc/catalogs.py ADDED
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env python
2
+
3
+ __all__ = []
4
+
5
+ import logging
6
+
7
+ from ..client import A2P2ClientPreferences
8
+
9
+ from .utils import JmmcAPI
10
+ from . import PRODLABEL
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class Catalog():
16
+ """ Get remote access to read and update catalogs exposed through JMMC's API.
17
+ Credential can be explicitly given for method that require an authentication, else:
18
+ - a2p2 preferences login will be used if present (see a2p2 -c)
19
+ - or uses .netrc file
20
+ """
21
+
22
+ def __init__(self, catalogName, username=None, password=None, prod=False, apiUrl=None):
23
+ self.catalogName = catalogName
24
+ self.prod = prod
25
+
26
+ # Manage prod & preprod or user provided access points
27
+ if apiUrl:
28
+ self.apiUrl = apiUrl # trust given url as catalogAPI if value is provided
29
+ elif self.prod:
30
+ self.apiUrl = "https://oidb.jmmc.fr/restxq/catalogs"
31
+ else:
32
+ self.apiUrl = "https://oidb-beta.jmmc.fr/restxq/catalogs"
33
+
34
+ self.api = JmmcAPI(self.apiUrl, username, password)
35
+
36
+ logger.info(f"Create catalog wrapper to access '{catalogName}' ({PRODLABEL[self.prod]} API at {self.api.rootURL})")
37
+
38
+ def list(self):
39
+ """ Get list of exposed catalogs on API associated to this catalog. """
40
+ return self.api._get("")
41
+
42
+ def metadata(self):
43
+ """ Get catalog metadata """
44
+ return self.api._get(f"/meta/{self.catalogName}")
45
+
46
+ def pis(self):
47
+ """ Get PIs from catalog and check for associated JMMC login in OiDB datapi table."""
48
+ return self.api._get(f"/accounts/{self.catalogName}")["pi"]
49
+
50
+ def piname(self, jmmcLogin=None):
51
+ """ Get the piname associated to the given jmmcLogin.
52
+ If jmmcLogin parameter is not provided, try to get jmmc.login preferences.
53
+ Only pi names returned by pis() can be retrieved.
54
+
55
+ usage: piname()
56
+ """
57
+ if not jmmcLogin:
58
+ prefs = A2P2ClientPreferences()
59
+ jmmcLogin = prefs.getJmmcLogin()
60
+ if not jmmcLogin:
61
+ raise Exception(
62
+ "missing login parameter or jmmc.login preference.")
63
+ pis = self.pis()
64
+ for pi in pis:
65
+ if "login" in pi and pi["login"] == jmmcLogin:
66
+ return pi["name"]
67
+
68
+ def getRow(self, id):
69
+ """ Get a single catalog record for the given id.
70
+
71
+ usage: cat.getRow(42)
72
+ """
73
+ return self.api._get(f"/{self.catalogName}/{id}")
74
+
75
+ def updateRow(self, id, values):
76
+ """ Update record identified by given id and associated values.
77
+
78
+ usage: cat.updateRows(42, {"col_a":"a", "col_b":"b" })
79
+ """
80
+ return self.api._put(f"/{self.catalogName}/{id}", values)
81
+
82
+ def updateRows(self, values):
83
+ """ Update multiple rows.
84
+ Values must contain a list of dictionnary and each entry must contains id key among other columns.
85
+
86
+ usage: updateRows([ { "id":42, "col_a":"a" }, { "id":24, "col_b":"b" } ])
87
+ """
88
+
89
+ # We may check befere sending payload that we always provide an id for every record
90
+ return self.api._put(f"/{self.catalogName}", values)
91
+
92
+ def addRows(self, values):
93
+ """ Add multiple rows.
94
+ Values is an array of row to add. id column values will be ignored.
95
+
96
+ usage: addCatalogRows([ { "id":42, "col_a":"a" }, { "id":24, "col_b":"b" } ])
97
+ """
98
+ return self.api._post(f"/{self.catalogName}", json=values)
99
+
100
+ def getDelegations(self, pi=None):
101
+ """ Get -all- delegations.
102
+ A specific pi parameter may be given but requires admin priviledges. Else use current piname will be used by remote service.
103
+
104
+ usage: getDelegations()
105
+ """
106
+ res = self.api._get(f"/delegations?pi={pi}")
107
+ if "delegations" in res:
108
+ return res["delegations"] # login and pi values not returned
109
+
110
+ return []
111
+
112
+ def updateDelegations(self, delegations):
113
+ """ Update delegations according given array of delegations.
114
+ A specific "pi" key may be given in delegations dict but requires admin privileges. Else current piname will be used by remote service.
115
+ "action" may be provided with "add" or "remove" value to set proper delegations (default add given delegation).
116
+
117
+ usage: addDelegations([{"pi":"mypiname", "catalogs":"mycats", "coi":["alice","bob"]}])
118
+
119
+ """
120
+ return self.api._put("/delegations", json=delegations)
121
+
122
+ def removeDelegations(self, delegations):
123
+ """ Remove delegations according given array of delegations.
124
+ A specific pi key may be given but requires admin privileges. Else current piname will be used by remote service.
125
+ usage: removeDelegations([{}, {}])
126
+ """
127
+ return "TODO"
128
+
129
+
@@ -0,0 +1,191 @@
1
+ from .models import _model
2
+
3
+ def punct( name, flux_weight=1.0, x=0.0, y=0.0, output_mode=None):
4
+ """ Returns the Fourier transform of a punctual object (Dirac function) at coordinates (X,Y)
5
+ (milliarcsecond).
6
+
7
+ FLUX_WEIGHT is the intensity coefficient. FLUX_WEIGHT=1 means total energy is 1. """
8
+
9
+ return _model({"flux_weight" : flux_weight, "x" : x, "y" : y, "name":name, "type":"punct"}, output_mode)
10
+
11
+ def disk( name, flux_weight=1.0, x=0.0, y=0.0, diameter=0.0, output_mode=None):
12
+ """ Returns the Fourier transform of a normalized uniform disk of diameter DIAMETER
13
+ (milliarcsecond) and centered at coordinates (X,Y) (milliarcsecond).
14
+
15
+ FLUX_WEIGHT is the intensity coefficient. FLUX_WEIGHT=1 means total energy is 1.
16
+
17
+ The function returns an error if DIAMETER is negative. """
18
+
19
+ return _model({"flux_weight" : flux_weight, "x" : x, "y" : y, "diameter" : diameter, "name":name, "type":"disk"}, output_mode)
20
+
21
+ def elong_disk( name, flux_weight=1.0, x=0.0, y=0.0, minor_axis_diameter=0.0, elong_ratio=1.0, major_axis_pos_angle=0.0, output_mode=None):
22
+ """ Returns the Fourier transform of a normalized ellipse centered at coordinates (X,Y)
23
+ (milliarcsecond) with a ratio ELONG_RATIO between the major diameter and the minor one
24
+ MINOR_AXIS_DIAMETER, turned from the positive vertical semi-axis (i.e. North direction)
25
+ with angle MAJOR_AXIS_POS_ANGLE, in degrees, towards to the positive horizontal semi-axis
26
+ (i.e. East direction). (the elongation is along the major_axis)
27
+
28
+ For avoiding degenerescence, the domain of variation of MAJOR_AXIS_POS_ANGLE is 180
29
+ degrees, for ex. from 0 to 180 degrees.
30
+
31
+ ELONG_RATIO = major_axis / minor_axis
32
+ FLUX_WEIGHT is the intensity coefficient. FLUX_WEIGHT=1 means total energy is 1.
33
+
34
+ The function returns an error if MINOR_AXIS_DIAMETER is negative or if ELONG_RATIO is
35
+ smaller than 1. """
36
+
37
+ return _model({"flux_weight" : flux_weight, "x" : x, "y" : y, "minor_axis_diameter" : minor_axis_diameter, "elong_ratio" : elong_ratio, "major_axis_pos_angle" : major_axis_pos_angle, "name":name, "type":"elong_disk"}, output_mode)
38
+
39
+ def flatten_disk( name, flux_weight=1.0, x=0.0, y=0.0, major_axis_diameter=0.0, flatten_ratio=1.0, minor_axis_pos_angle=0.0, output_mode=None):
40
+ """ Returns the Fourier transform of a normalized ellipse centered at coordinates (X,Y)
41
+ (milliarcsecond) with a ratio FLATTEN_RATIO between the major diameter
42
+ MAJOR_AXIS_DIAMETER and the minor one, turned from the positive vertical semi-axis
43
+ (i.e. North direction) with angle MINOR_AXIS_POS_ANGLE, in degrees, towards to the
44
+ positive horizontal semi-axis (i.e. East direction). (the flattening is along the minor_axis)
45
+
46
+ For avoiding degenerescence, the domain of variation of MINOR_AXIS_POS_ANGLE is 180
47
+ degrees, for ex. from 0 to 180 degrees.
48
+
49
+ FLATTEN_RATIO = major_axis / minor_axis
50
+ FLUX_WEIGHT is the intensity coefficient. FLUX_WEIGHT=1 means total energy is 1.
51
+
52
+ The function returns an error if MAJOR_AXIS_DIAMETER is negative or if FLATTEN_RATIO
53
+ is smaller than 1. """
54
+
55
+ return _model({"flux_weight" : flux_weight, "x" : x, "y" : y, "major_axis_diameter" : major_axis_diameter, "flatten_ratio" : flatten_ratio, "minor_axis_pos_angle" : minor_axis_pos_angle, "name":name, "type":"flatten_disk"}, output_mode)
56
+
57
+ def circle( name, flux_weight=1.0, x=0.0, y=0.0, diameter=0.0, output_mode=None):
58
+ """ Returns the Fourier transform of a normalized uniform circle of diameter DIAMETER
59
+ (milliarcsecond) and centered at coordinates (X,Y) (milliarcsecond).
60
+
61
+ FLUX_WEIGHT is the intensity coefficient. FLUX_WEIGHT=1 means total energy is 1.
62
+
63
+ The function returns an error if DIAMETER is negative. """
64
+
65
+ return _model({"flux_weight" : flux_weight, "x" : x, "y" : y, "diameter" : diameter, "name":name, "type":"circle"}, output_mode)
66
+
67
+ def ring( name, flux_weight=1.0, x=0.0, y=0.0, diameter=0.0, width=0.0, output_mode=None):
68
+ """ Returns the Fourier transform of a normalized uniform ring with internal diameter
69
+ DIAMETER (milliarcsecond) and external diameter DIAMETER + WIDTH centered at coordinates
70
+ (X,Y) (milliarcsecond).
71
+
72
+ FLUX_WEIGHT is the intensity coefficient. FLUX_WEIGHT=1 means total energy is 1.
73
+
74
+ The function returns an error if DIAMETER or WIDTH are negative. """
75
+
76
+ return _model({"flux_weight" : flux_weight, "x" : x, "y" : y, "diameter" : diameter, "width" : width, "name":name, "type":"ring"}, output_mode)
77
+
78
+ def elong_ring( name, flux_weight=1.0, x=0.0, y=0.0, minor_internal_diameter=0.0, elong_ratio=1.0, width=0.0, major_axis_pos_angle=0.0, output_mode=None):
79
+ """ Returns the Fourier transform of a normalized uniform elongated ring centered at
80
+ coordinates (X,Y) (milliarcsecond). The sizes of the function in two orthogonal directions
81
+ are given by the narrowest internal diameter (MINOR_INTERNAL_DIAMETER) and by the ratio
82
+ ELONG_RATIO between the widest internal diameter and MINOR_INTERNAL_DIAMETER,
83
+ in the same way as for an ellipse (the elongation is along the major_axis) :
84
+
85
+ ELONG_RATIO = MAJOR_INTERNAL_DIAMETER / MINOR_INTERNAL_DIAMETER.
86
+ In the direction of MINOR_INTERNAL_DIAMETER, the external diameter is
87
+ MINOR_INTERNAL_DIAMETER + WIDTH. In the direction of the widest internal diameter,
88
+ the width is magnified by the ratio ELONG_RATIO, so that the external diameter is
89
+ the elongated MAJOR_INTERNAL_DIAMETER + WIDTH * ELONG_RATIO.
90
+ MAJOR_AXIS_POS_ANGLE is measured in degrees, from the positive vertical semi-axis
91
+ (i.e. North direction) towards to the positive horizontal semi-axis (i.e. East direction).
92
+ For avoiding degenerescence, the domain of variation of MAJOR_AXIS_POS_ANGLE is 180
93
+ degrees, for ex. from 0 to 180 degrees.
94
+
95
+ FLUX_WEIGHT is the intensity coefficient. FLUX_WEIGHT=1 means total energy is 1.
96
+
97
+ The function returns an error if MINOR_INTERNAL_DIAMETER is negative or if ELONG_RATIO
98
+ is smaller than 1. """
99
+
100
+ return _model({"flux_weight" : flux_weight, "x" : x, "y" : y, "minor_internal_diameter" : minor_internal_diameter, "elong_ratio" : elong_ratio, "width" : width, "major_axis_pos_angle" : major_axis_pos_angle, "name":name, "type":"elong_ring"}, output_mode)
101
+
102
+ def flatten_ring( name, flux_weight=1.0, x=0.0, y=0.0, major_internal_diameter=0.0, flatten_ratio=1.0, width=0.0, minor_axis_pos_angle=0.0, output_mode=None):
103
+ """ Returns the Fourier transform of a normalized uniform flattened ring centered at
104
+ coordinates (X,Y) (milliarcsecond). The sizes of the function in two orthogonal directions
105
+ are given by the widest internal diameter (MAJOR_INTERNAL_DIAMETER) and by the ratio
106
+ FLATTEN_RATIO between MAJOR_INTERNAL_DIAMETER and the narrowest internal diameter,
107
+ in the same way as for an ellipse (the flattening is along the minor axis) :
108
+
109
+ FLATTEN_RATIO = MAJOR_INTERNAL_DIAMETER / MINOR_INTERNAL_DIAMETER.
110
+ In the direction of MAJOR_INTERNAL_DIAMETER, the external diameter is
111
+ MAJOR_INTERNAL_DIAMETER + WIDTH. In the direction of the narrowest internal diameter,
112
+ the width is decreased by the ratio FLATTEN_RATIO, so that the external diameter is
113
+ the flattened MINOR_INTERNAL_DIAMETER + WIDTH / FLATTEN_RATIO.
114
+ MINOR_AXIS_POS_ANGLE is measured in degrees, from the positive vertical semi-axis
115
+ (i.e. North direction) towards to the positive horizontal semi-axis (i.e. East direction).
116
+ For avoiding degenerescence, the domain of variation of MINOR_AXIS_POS_ANGLE is 180
117
+ degrees, for ex. from 0 to 180 degrees.
118
+
119
+ FLUX_WEIGHT is the intensity coefficient. FLUX_WEIGHT=1 means total energy is 1.
120
+
121
+ The function returns an error if MAJOR_INTERNAL_DIAMETER is negative or if FLATTEN_RATIO
122
+ is smaller than 1. """
123
+
124
+ return _model({"flux_weight" : flux_weight, "x" : x, "y" : y, "major_internal_diameter" : major_internal_diameter, "flatten_ratio" : flatten_ratio, "width" : width, "minor_axis_pos_angle" : minor_axis_pos_angle, "name":name, "type":"flatten_ring"}, output_mode)
125
+
126
+ def gaussian( name, flux_weight=1.0, x=0.0, y=0.0, fwhm=0.0, output_mode=None):
127
+ """ Returns the Fourier transform of a normalized gaussian with given FWHM (milliarcsecond)
128
+ centered at coordinates (X,Y) (milliarcsecond).
129
+
130
+ FLUX_WEIGHT is the intensity coefficient. FLUX_WEIGHT=1 means total energy is 1.
131
+
132
+ The function returns an error if FWHM is negative. """
133
+
134
+ return _model({"flux_weight" : flux_weight, "x" : x, "y" : y, "fwhm" : fwhm, "name":name, "type":"gaussian"}, output_mode)
135
+
136
+ def elong_gaussian( name, flux_weight=1.0, x=0.0, y=0.0, minor_axis_fwhm=0.0, elong_ratio=1.0, major_axis_pos_angle=0.0, output_mode=None):
137
+ """ Returns the Fourier transform of a normalized elongated gaussian centered at coordinates
138
+ (X,Y) (milliarcsecond). The sizes of the function in two orthogonal directions are given by
139
+ the narrowest FWHM (MINOR_AXIS_FWHM) and by the ratio ELONG_RATIO between the largest
140
+ FWHM (MAJOR_AXIS_FWHM) and the MINOR_AXIS_FWHM, in the same way as for an ellipse
141
+ (the elongation is along the major_axis) :
142
+
143
+ ELONG_RATIO = MAJOR_AXIS_FWHM / MINOR_AXIS_FWHM.
144
+ MAJOR_AXIS_POS_ANGLE is measured in degrees, from the positive vertical semi-axis
145
+ (i.e. North direction) towards to the positive horizontal semi-axis (i.e. East direction).
146
+ For avoiding degenerescence, the domain of variation of MAJOR_AXIS_POS_ANGLE is 180
147
+ degrees, for ex. from 0 to 180 degrees.
148
+
149
+ FLUX_WEIGHT is the intensity coefficient. FLUX_WEIGHT=1 means total energy is 1.
150
+
151
+ The function returns an error if MINOR_AXIS_FWHM is negative or if ELONG_RATIO
152
+ is smaller than 1. """
153
+
154
+ return _model({"flux_weight" : flux_weight, "x" : x, "y" : y, "minor_axis_fwhm" : minor_axis_fwhm, "elong_ratio" : elong_ratio, "major_axis_pos_angle" : major_axis_pos_angle, "name":name, "type":"elong_gaussian"}, output_mode)
155
+
156
+ def flatten_gaussian( name, flux_weight=1.0, x=0.0, y=0.0, major_axis_fwhm=0.0, flatten_ratio=1.0, minor_axis_pos_angle=0.0, output_mode=None):
157
+ """ Returns the Fourier transform of a normalized flattened gaussian centered at coordinates
158
+ (X,Y) (milliarcsecond). The sizes of the function in two orthogonal directions are given by
159
+ the largest FWHM (MAJOR_AXIS_FWHM) and by the ratio FLATTEN_RATIO between the largest
160
+ FWHM (MAJOR_AXIS_FWHM) and the MINOR_AXIS_FWHM, in the same way as for an ellipse
161
+ (the flattening is along the minor_axis) :
162
+
163
+ FLATTEN_RATIO = MAJOR_AXIS_FWHM / MINOR_AXIS_FWHM.
164
+ MINOR_AXIS_POS_ANGLE is measured in degrees, from the positive vertical semi-axis
165
+ (i.e. North direction) towards to the positive horizontal semi-axis (i.e. East direction).
166
+ For avoiding degenerescence, the domain of variation of MINOR_AXIS_POS_ANGLE is 180
167
+ degrees, for ex. from 0 to 180 degrees.
168
+
169
+ FLUX_WEIGHT is the intensity coefficient. FLUX_WEIGHT=1 means total energy is 1.
170
+
171
+ The function returns an error if MAJOR_AXIS_FWHM is negative or if FLATTEN_RATIO
172
+ is smaller than 1. """
173
+
174
+ return _model({"flux_weight" : flux_weight, "x" : x, "y" : y, "major_axis_fwhm" : major_axis_fwhm, "flatten_ratio" : flatten_ratio, "minor_axis_pos_angle" : minor_axis_pos_angle, "name":name, "type":"flatten_gaussian"}, output_mode)
175
+
176
+ def limb_quadratic( name, flux_weight=1.0, x=0.0, y=0.0, diameter=0.0, a1_coeff=0.0, a2_coeff=0.0, output_mode=None):
177
+ """ Returns the Fourier transform of a center-to-limb darkened disk of diameter DIAMETER
178
+ (milliarcsecond) centered at coordinates (X,Y) (milliarcsecond).
179
+
180
+ The brightness distribution o, if expressed versus mu, the cosine of the azimuth of
181
+ a surface element of the star, follows a quadratic law of coefficients
182
+ A1_COEFF, A2_COEFF ([-1,1]), and is normalized for mu = 1 (center of the star).
183
+ o(mu) = 1 -A1_COEFF(1-mu) - A2_COEFF(1-mu)^2.
184
+
185
+ FLUX_WEIGHT is the intensity coefficient. FLUX_WEIGHT=1 means total energy is 1.
186
+
187
+ The function returns an error if DIAMETER is negative or if A1_COEFF or A2_coeff is
188
+ outside bounds [-1,1] """
189
+
190
+ return _model({"flux_weight" : flux_weight, "x" : x, "y" : y, "diameter" : diameter, "a1_coeff" : a1_coeff, "a2_coeff" : a2_coeff, "name":name, "type":"limb_quadratic"}, output_mode)
191
+
a2p2/jmmc/models.py ADDED
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env python
2
+
3
+ __all__ = []
4
+
5
+ import json
6
+ import logging
7
+ import xml.etree.ElementTree as ET
8
+ import json
9
+
10
+ from .utils import JmmcAPI
11
+ from a2p2.ob import etree_to_dict
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ #
17
+ # first release done for SAMP interoperability with Aspro2
18
+ #
19
+
20
+ def modelsFromXml(xmlmodel):
21
+ """ Returns a python object representing JSON LIKE model(s) from given XML."""
22
+ # read xml models from Aspro2 and convert them to a JSON Model
23
+ e = ET.fromstring(xmlmodel)
24
+ _remove_namespace(e, "http://www.jmmc.fr/jmcs/models/0.1")
25
+ container = etree_to_dict(e)
26
+ modellist=container['model']['model']
27
+ if not isinstance(modellist, list):
28
+ modellist=[modellist]
29
+
30
+ models=[]
31
+ for m in modellist:
32
+ model={}
33
+ for p in m['parameter']:
34
+ model[p['type']]=p['value']
35
+ model['name']=m['name']
36
+ model['type']=m['type']
37
+ models.append(model)
38
+
39
+ return json.dumps(models)
40
+
41
+
42
+ def _remove_namespace(doc, namespace):
43
+ """Remove namespace in the passed document in place.
44
+ from https://homework.nwsnet.de/releases/45be/"""
45
+ ns = u'{%s}' % namespace
46
+ nsl = len(ns)
47
+ for elem in doc.getiterator():
48
+ if elem.tag.startswith(ns):
49
+ elem.tag = elem.tag[nsl:]
50
+
51
+
52
+ def _model(models, output_mode=None):
53
+ """ Returns a serialisation of given model(s) to XML by default or another format given to output_mode value (only jsonlike at present time)."""
54
+
55
+ # deserialize if models is a string
56
+ if isinstance(models, str):
57
+ models=json.loads(models)
58
+
59
+ if output_mode:
60
+ return models
61
+ else :
62
+ return _xml_model(models)
63
+
64
+ def _xml_model(models, position=1):
65
+ """ Rough conversion of dict(s) to xml (Aspro2 namespaces)
66
+ if name key is not provided, position value will be used to compute suffix(es)"""
67
+
68
+ if isinstance(models, list):
69
+ modellist=[]
70
+ pos=1
71
+ for m in models:
72
+ modellist.append(_xml_model(m, pos))
73
+ pos+=1 # do not use models.index(m) since same str components would get the same index
74
+ return "".join(modellist)
75
+
76
+ model = models # models is now a single element
77
+ modeltype=model["type"]
78
+ if "name" in model.keys():
79
+ modelname=model["name"]
80
+ else:
81
+ modelname=modeltype+str(position)
82
+
83
+
84
+ params=""
85
+ paramNames=[ k for k in model.keys() if not k in ("type", "name")]
86
+ # at present time we must use Aspro2's approach with the same xml namespaces
87
+ # see
88
+ for p in paramNames:
89
+ params+=f""" <tm:parameter name="{modelname}_{p}" type="{p}"><value>{model[p]}</value></tm:parameter>\n"""
90
+
91
+ return f"""<tm:model name="{modelname}" type="{modeltype}">\n{params}</tm:model>\n"""
92
+
93
+
94
+ class Models():
95
+ """ Get analytical model's representations (in sync with Aspro2's ones).
96
+ """
97
+ SAMP_UCD_MODEL="meta.code.class;meta.modelled" # use it for colums that would be filled by models below
98
+
99
+ # generated code below from an Asprox file that contains a single star with list of all supported models
100
+ # cd a2p2/jmmc
101
+ # xml sel -o "from .models import _model" -n -n -b -t -m "//tm:model" -o "def " -v "@type" -o "( name, " -m "tm:parameter" -v "@type" -o "=" -v "value" -o ", " -b -o " output_mode=None):" -n -o ' """ ' -v "desc" -o ' """ ' -n -n -o ' return _model({' -m "tm:parameter" -o '"' -v "@type" -o '" : ' -v "@type" -o ", " -b -o '"name":name, "type":"' -v "@type" -o '"}, output_mode)' -n -n models_template.asprox > generated_models.py
102
+ # xml sel -t -o " from .generated_models import " -m "//tm:model" -v "@type" -o ", " -b -n models_template.asprox
103
+ # cd -
104
+ from .generated_models import punct, disk, elong_disk, flatten_disk, circle, ring, elong_ring, flatten_ring, gaussian, elong_gaussian, flatten_gaussian, limb_quadratic
a2p2/jmmc/services.py ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env python
2
+
3
+ __all__ = []
4
+
5
+ import logging
6
+
7
+ from .catalogs import Catalog
8
+ from .webservices import CallIper
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ # TODO list and give access to whole services and their beta/alpha / pre-prod instances if existing
13
+
14
+ oidb_catalog = Catalog("oidb")
15
+
16
+ calliper = CallIper()
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