PyDIET 0.9.3__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 (177) hide show
  1. pydiet/__init__.py +12 -0
  2. pydiet/api_client/__init__.py +6 -0
  3. pydiet/api_client/client.py +57 -0
  4. pydiet/cmd/__init__.py +9 -0
  5. pydiet/cmd/start.py +107 -0
  6. pydiet/data/data_config.toml +242 -0
  7. pydiet/data/description.txt +1 -0
  8. pydiet/data/instruments/description.txt +1 -0
  9. pydiet/data/instruments/megacam/default +0 -0
  10. pydiet/data/instruments/megacam/description.txt +1 -0
  11. pydiet/data/instruments/megacam/detector/description.txt +1 -0
  12. pydiet/data/instruments/megacam/detector/qe/MegaCam_QE.average.fits +0 -0
  13. pydiet/data/instruments/megacam/detector/qe/description.txt +2 -0
  14. pydiet/data/instruments/megacam/filters/CaHK.MP9303.fits +0 -0
  15. pydiet/data/instruments/megacam/filters/Ha.MP9603.fits +0 -0
  16. pydiet/data/instruments/megacam/filters/HaOFF.MP9604.fits +0 -0
  17. pydiet/data/instruments/megacam/filters/M4112.MP9403.fits +0 -0
  18. pydiet/data/instruments/megacam/filters/M4376.MP9404.fits +0 -0
  19. pydiet/data/instruments/megacam/filters/OIII.MP9501.fits +0 -0
  20. pydiet/data/instruments/megacam/filters/OIIIOFF.MP9502.fits +0 -0
  21. pydiet/data/instruments/megacam/filters/description.txt +1 -0
  22. pydiet/data/instruments/megacam/filters/g.MP9402.fits +0 -0
  23. pydiet/data/instruments/megacam/filters/gri.MP9605.fits +0 -0
  24. pydiet/data/instruments/megacam/filters/i.MP9703.fits +0 -0
  25. pydiet/data/instruments/megacam/filters/r.MP9602.fits +0 -0
  26. pydiet/data/instruments/megacam/filters/u.MP9302.fits +0 -0
  27. pydiet/data/instruments/megacam/filters/z.MP9901.fits +0 -0
  28. pydiet/data/instruments/megacam/optics/description.txt +2 -0
  29. pydiet/data/instruments/megacam/optics/transmission/MegaPrime_transmission.fits +0 -0
  30. pydiet/data/instruments/megacam/optics/transmission/description.txt +2 -0
  31. pydiet/data/instruments/wircam/description.txt +1 -0
  32. pydiet/data/instruments/wircam/detector/description.txt +1 -0
  33. pydiet/data/instruments/wircam/detector/qe/WIRCam_QE.average.fits +0 -0
  34. pydiet/data/instruments/wircam/detector/qe/description.txt +2 -0
  35. pydiet/data/instruments/wircam/filters/BrG.WC8305.fits +0 -0
  36. pydiet/data/instruments/wircam/filters/CH4Off.WC8204.fits +0 -0
  37. pydiet/data/instruments/wircam/filters/CH4On.WC8203.fits +0 -0
  38. pydiet/data/instruments/wircam/filters/CO.WC8306.fits +0 -0
  39. pydiet/data/instruments/wircam/filters/H.WC8201.fits +0 -0
  40. pydiet/data/instruments/wircam/filters/H.WC8202.fits +0 -0
  41. pydiet/data/instruments/wircam/filters/H2.WC8304.fits +0 -0
  42. pydiet/data/instruments/wircam/filters/J.WC8101.fits +0 -0
  43. pydiet/data/instruments/wircam/filters/J.WC8103.fits +0 -0
  44. pydiet/data/instruments/wircam/filters/Kcont.WC8303.fits +0 -0
  45. pydiet/data/instruments/wircam/filters/Ks.WC8301.fits +0 -0
  46. pydiet/data/instruments/wircam/filters/Ks.WC8302.fits +0 -0
  47. pydiet/data/instruments/wircam/filters/LowOH1.WC8104.fits +0 -0
  48. pydiet/data/instruments/wircam/filters/LowOH2.WC8102.fits +0 -0
  49. pydiet/data/instruments/wircam/filters/W.WC8105.fits +0 -0
  50. pydiet/data/instruments/wircam/filters/Y.WC8002.fits +0 -0
  51. pydiet/data/instruments/wircam/filters/description.txt +1 -0
  52. pydiet/data/instruments/wircam/optics/description.txt +2 -0
  53. pydiet/data/instruments/wircam/optics/transmission/WIRCam_transmission.fits +0 -0
  54. pydiet/data/instruments/wircam/optics/transmission/description.txt +2 -0
  55. pydiet/data/sites/description.txt +1 -0
  56. pydiet/data/sites/mko/default +0 -0
  57. pydiet/data/sites/mko/description.txt +2 -0
  58. pydiet/data/sites/mko/emission/MKO_emission.bright.AM1.0.fits +0 -0
  59. pydiet/data/sites/mko/emission/MKO_emission.bright.AM1.1.fits +0 -0
  60. pydiet/data/sites/mko/emission/MKO_emission.bright.AM1.2.fits +0 -0
  61. pydiet/data/sites/mko/emission/MKO_emission.bright.AM1.3.fits +0 -0
  62. pydiet/data/sites/mko/emission/MKO_emission.bright.AM1.4.fits +0 -0
  63. pydiet/data/sites/mko/emission/MKO_emission.bright.AM1.5.fits +0 -0
  64. pydiet/data/sites/mko/emission/MKO_emission.bright.AM1.6.fits +0 -0
  65. pydiet/data/sites/mko/emission/MKO_emission.bright.AM1.7.fits +0 -0
  66. pydiet/data/sites/mko/emission/MKO_emission.bright.AM1.8.fits +0 -0
  67. pydiet/data/sites/mko/emission/MKO_emission.bright.AM1.9.fits +0 -0
  68. pydiet/data/sites/mko/emission/MKO_emission.bright.AM2.0.fits +0 -0
  69. pydiet/data/sites/mko/emission/MKO_emission.bright.AM2.5.fits +0 -0
  70. pydiet/data/sites/mko/emission/MKO_emission.bright.AM3.0.fits +0 -0
  71. pydiet/data/sites/mko/emission/MKO_emission.dark.AM1.0.fits +0 -0
  72. pydiet/data/sites/mko/emission/MKO_emission.dark.AM1.1.fits +0 -0
  73. pydiet/data/sites/mko/emission/MKO_emission.dark.AM1.2.fits +0 -0
  74. pydiet/data/sites/mko/emission/MKO_emission.dark.AM1.3.fits +0 -0
  75. pydiet/data/sites/mko/emission/MKO_emission.dark.AM1.4.fits +0 -0
  76. pydiet/data/sites/mko/emission/MKO_emission.dark.AM1.5.fits +0 -0
  77. pydiet/data/sites/mko/emission/MKO_emission.dark.AM1.6.fits +0 -0
  78. pydiet/data/sites/mko/emission/MKO_emission.dark.AM1.7.fits +0 -0
  79. pydiet/data/sites/mko/emission/MKO_emission.dark.AM1.8.fits +0 -0
  80. pydiet/data/sites/mko/emission/MKO_emission.dark.AM1.9.fits +0 -0
  81. pydiet/data/sites/mko/emission/MKO_emission.dark.AM2.0.fits +0 -0
  82. pydiet/data/sites/mko/emission/MKO_emission.dark.AM2.5.fits +0 -0
  83. pydiet/data/sites/mko/emission/MKO_emission.dark.AM3.0.fits +0 -0
  84. pydiet/data/sites/mko/emission/MKO_emission.grey.AM1.0.fits +0 -0
  85. pydiet/data/sites/mko/emission/MKO_emission.grey.AM1.1.fits +0 -0
  86. pydiet/data/sites/mko/emission/MKO_emission.grey.AM1.2.fits +0 -0
  87. pydiet/data/sites/mko/emission/MKO_emission.grey.AM1.3.fits +0 -0
  88. pydiet/data/sites/mko/emission/MKO_emission.grey.AM1.4.fits +0 -0
  89. pydiet/data/sites/mko/emission/MKO_emission.grey.AM1.5.fits +0 -0
  90. pydiet/data/sites/mko/emission/MKO_emission.grey.AM1.6.fits +0 -0
  91. pydiet/data/sites/mko/emission/MKO_emission.grey.AM1.7.fits +0 -0
  92. pydiet/data/sites/mko/emission/MKO_emission.grey.AM1.8.fits +0 -0
  93. pydiet/data/sites/mko/emission/MKO_emission.grey.AM1.9.fits +0 -0
  94. pydiet/data/sites/mko/emission/MKO_emission.grey.AM2.0.fits +0 -0
  95. pydiet/data/sites/mko/emission/MKO_emission.grey.AM2.5.fits +0 -0
  96. pydiet/data/sites/mko/emission/MKO_emission.grey.AM3.0.fits +0 -0
  97. pydiet/data/sites/mko/emission/description.txt +5 -0
  98. pydiet/data/sites/mko/transmission/MKO_transmission.AM1.0.fits +0 -0
  99. pydiet/data/sites/mko/transmission/MKO_transmission.AM1.1.fits +0 -0
  100. pydiet/data/sites/mko/transmission/MKO_transmission.AM1.2.fits +0 -0
  101. pydiet/data/sites/mko/transmission/MKO_transmission.AM1.3.fits +0 -0
  102. pydiet/data/sites/mko/transmission/MKO_transmission.AM1.4.fits +0 -0
  103. pydiet/data/sites/mko/transmission/MKO_transmission.AM1.5.fits +0 -0
  104. pydiet/data/sites/mko/transmission/MKO_transmission.AM1.6.fits +0 -0
  105. pydiet/data/sites/mko/transmission/MKO_transmission.AM1.7.fits +0 -0
  106. pydiet/data/sites/mko/transmission/MKO_transmission.AM1.8.fits +0 -0
  107. pydiet/data/sites/mko/transmission/MKO_transmission.AM1.9.fits +0 -0
  108. pydiet/data/sites/mko/transmission/MKO_transmission.AM2.0.fits +0 -0
  109. pydiet/data/sites/mko/transmission/MKO_transmission.AM2.5.fits +0 -0
  110. pydiet/data/sites/mko/transmission/MKO_transmission.AM3.0.fits +0 -0
  111. pydiet/data/sites/mko/transmission/MKO_transmission.AM3.5.fits +0 -0
  112. pydiet/data/sites/mko/transmission/MKO_transmission.AM4.0.fits +0 -0
  113. pydiet/data/sites/mko/transmission/MKO_transmission.AM4.5.fits +0 -0
  114. pydiet/data/sites/mko/transmission/MKO_transmission.AM5.0.fits +0 -0
  115. pydiet/data/sites/mko/transmission/description.txt +5 -0
  116. pydiet/data/telescopes/cfht/default +0 -0
  117. pydiet/data/telescopes/cfht/description.txt +1 -0
  118. pydiet/data/telescopes/cfht/emission/description.txt +2 -0
  119. pydiet/data/telescopes/cfht/transmission/CFHT_M1_transmission.fits +0 -0
  120. pydiet/data/telescopes/cfht/transmission/description.txt +1 -0
  121. pydiet/data/telescopes/description.txt +1 -0
  122. pydiet/package.py +55 -0
  123. pydiet/py.typed +0 -0
  124. pydiet/server/__init__.py +9 -0
  125. pydiet/server/app.py +369 -0
  126. pydiet/server/config/__init__.py +51 -0
  127. pydiet/server/config/config.py +330 -0
  128. pydiet/server/config/fields.py +49 -0
  129. pydiet/server/config/settings.py +166 -0
  130. pydiet/server/data.py +31 -0
  131. pydiet/server/datafiles.py +367 -0
  132. pydiet/server/image.py +342 -0
  133. pydiet/server/models/__init__.py +34 -0
  134. pydiet/server/models/dataconfig.py +195 -0
  135. pydiet/server/models/default.py +9 -0
  136. pydiet/server/models/exceptions.py +9 -0
  137. pydiet/server/models/instrument.py +314 -0
  138. pydiet/server/models/query.py +172 -0
  139. pydiet/server/models/response.py +97 -0
  140. pydiet/server/models/types.py +35 -0
  141. pydiet/server/photsys.py +71 -0
  142. pydiet/server/response.py +237 -0
  143. pydiet/server/types/__init__.py +8 -0
  144. pydiet/server/types/quantity.py +532 -0
  145. pydiet/server/types/string.py +318 -0
  146. pydiet/templates/common/base.html +80 -0
  147. pydiet/templates/common/plot_filter.html +17 -0
  148. pydiet/templates/common/privacy.html +132 -0
  149. pydiet/templates/common/settings.html +23 -0
  150. pydiet/templates/common/terms.html +101 -0
  151. pydiet/templates/megacam/etc_form.html +319 -0
  152. pydiet/templates/megacam/etc_results.html +190 -0
  153. pydiet/templates/wircam/etc_form.html +319 -0
  154. pydiet/templates/wircam/etc_results.html +190 -0
  155. pydiet/web_client/css/style.css +221 -0
  156. pydiet/web_client/dist/pydiet.js +31 -0
  157. pydiet/web_client/images/logo.svg +6 -0
  158. pydiet/web_client/images/megacam/background.jpg +0 -0
  159. pydiet/web_client/images/megacam/logo.png +0 -0
  160. pydiet/web_client/images/wircam/background.jpg +0 -0
  161. pydiet/web_client/images/wircam/logo.png +0 -0
  162. pydiet/web_client/js/dom.js +51 -0
  163. pydiet/web_client/js/etc.js +63 -0
  164. pydiet/web_client/js/fetch.js +49 -0
  165. pydiet/web_client/js/instrument.js +62 -0
  166. pydiet/web_client/js/main.js +15 -0
  167. pydiet/web_client/js/plot.js +88 -0
  168. pydiet/web_client/js/settings.js +57 -0
  169. pydiet/web_client/js/theme.js +43 -0
  170. pydiet/web_client/js/url.js +12 -0
  171. pydiet/web_client/jsdoc.json +20 -0
  172. pydiet/web_client/package.json +83 -0
  173. pydiet-0.9.3.dist-info/METADATA +118 -0
  174. pydiet-0.9.3.dist-info/RECORD +177 -0
  175. pydiet-0.9.3.dist-info/WHEEL +4 -0
  176. pydiet-0.9.3.dist-info/entry_points.txt +5 -0
  177. pydiet-0.9.3.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,532 @@
1
+ """
2
+ Provide Pydantic-compatible annotations and fields for validating and
3
+ serializing AstroPy unit-aware "quantities"
4
+ """
5
+ # Copyright UParisSaclay/CEA/CFHT/CNRS
6
+ # Licensed under the MIT licence
7
+
8
+ from __future__ import annotations
9
+
10
+ from numbers import Number
11
+ from re import findall
12
+ from typing import TYPE_CHECKING, Annotated, Any, Iterable, Literal, Tuple
13
+
14
+ from astropy import units as u #type: ignore[import-untyped]
15
+ import numpy as np
16
+ if TYPE_CHECKING:
17
+ from pydantic import GetCoreSchemaHandler
18
+ from pydantic import Field
19
+ from pydantic_core import core_schema
20
+
21
+ # Enable imperial units such as inches
22
+ u.imperial.enable()
23
+
24
+
25
+
26
+ class QuantityAnnotation:
27
+ """
28
+ Pydantic compatible annotation for validating and serializing AstroPy
29
+ Quantity fields.
30
+ Loosely adapted from pint-quantity by Tyler Hughes <tylerxh111+git@proton.me>
31
+
32
+ Examples
33
+ --------
34
+ >>> from typing import Annotated
35
+ >>> from astropy import units as u
36
+ >>> from pydantic import BaseModel
37
+ >>> from .quantity import QuantityAnnotation
38
+
39
+ >>> class Coordinates(BaseModel):
40
+ ... latitude: Annotated[
41
+ ... u.Quantity, QuantityAnnotation("deg", ge=-90.*u.deg, le=90.*u.deg)
42
+ ... ]
43
+ ... longitude: Annotated[u.Quantity, QuantityAnnotation("deg")]
44
+ ... altitude: Annotated[u.Quantity, QuantityAnnotation("km")]
45
+
46
+ >>> # The following instantiation validates
47
+ >>> coord = Coordinates(
48
+ ... latitude="39.905705 deg",
49
+ ... longitude="-75.166519 deg",
50
+ ... altitude="12 m"
51
+ ... )
52
+
53
+ >>> coord
54
+ Coordinates(latitude=<Quantity 39.905705 deg>,
55
+ longitude=<Quantity -75.166519 deg>, altitude=<Quantity 0.012 km>)
56
+
57
+ >>> f"{coord!r}"
58
+ 'Coordinates(latitude=<Quantity 39.905705 deg>,
59
+ longitude=<Quantity -75.166519 deg>, altitude=<Quantity 0.012 km>)'
60
+
61
+ >>> coord.model_dump()
62
+ {'latitude': <Quantity 39.905705 deg>, 'longitude': <Quantity -75.166519 deg>,
63
+ 'altitude': <Quantity 0.012 km>}
64
+
65
+ >>> coord.model_dump(mode="json")
66
+ {'latitude': '39.905705 deg', 'longitude': '-75.166519 deg',
67
+ 'altitude': '0.012 km'}
68
+
69
+ >>> coord.model_dump_json()
70
+ '{"latitude":"39.905705 deg","longitude":"-75.166519 deg","altitude":"0.012 km"}'
71
+
72
+ >>> # The following instantiation does not validate
73
+ >>> coord = Coordinates(
74
+ ... latitude="99.905705 deg",
75
+ ... longitude="-75.166519 deg",
76
+ ... altitude="12 m"
77
+ ... )
78
+ Traceback (most recent call last):
79
+ ...
80
+ pydantic_core._pydantic_core.ValidationError: 1 validation error for Coordinates
81
+ latitude
82
+ Value error, greater than 90.0 deg
83
+ [type=value_error, input_value='99.905705 deg', input_type=str]
84
+ For further information visit https://errors.pydantic.dev/2.7/v/value_error
85
+
86
+ Parameters
87
+ ----------
88
+ unit: str
89
+ The unit type of the Pydantic field (e.g., `"m"`, or `"deg"`).
90
+ All input units must be convertible to this unit.
91
+ description: str, optional
92
+ Description string.
93
+ decimals: int, optional
94
+ Maximum number of decimals for the serialization of quantities.
95
+ min_shape: tuple[int], optional
96
+ Minimum number of vector components on each axis.
97
+ max_shape: tuple[int], optional
98
+ Maximum number of vector components on each axis.
99
+ ge: ~astropy.units.Quantity or str, optional
100
+ Lower limit (inclusive).
101
+ gt: ~astropy.units.Quantity or str, optional
102
+ Lower limit (strict).
103
+ le: ~astropy.units.Quantity or str, optional
104
+ Lower limit (inclusive).
105
+ lt: ~astropy.units.Quantity or str, optional
106
+ Lower limit (strict).
107
+ ser_mode: Literal["str", "dict"], optional
108
+ The mode for serializing the field; either `"str"` or `"dict"`.
109
+ By default, in Pydantic's `"python"` serialization mode, fields are
110
+ serialized to a `Quantity`;
111
+ in Pydantic's `"json"` serialization mode, fields are serialized to a `str`.
112
+ strict: bool, optional
113
+ Forces users to specify units; on by default.
114
+ If disabled, a value without units - provided by the user - will be
115
+ treated as the base units of the `QuantityUnit`.
116
+ """
117
+ description: str = ""
118
+ decimals: int | None = None
119
+ min_shape: np.ndarray | None = None
120
+ max_shape: np.ndarray | None = None
121
+ ge: u.Quantity | str | None = None
122
+ gt: u.Quantity | str | None = None
123
+ le: u.Quantity | str | None = None
124
+ lt: u.Quantity | str | None = None
125
+ def __init__(
126
+ self,
127
+ unit: str,
128
+ *,
129
+ description: str = "",
130
+ decimals: int | None = None,
131
+ min_shape: Tuple[int, ...] | None = None,
132
+ max_shape: Tuple[int, ...] | None = None,
133
+ ge: u.Quantity | str | None = None,
134
+ gt: u.Quantity | str | None = None,
135
+ le: u.Quantity | str | None = None,
136
+ lt: u.Quantity | str | None = None,
137
+ ser_mode: Literal["str", "dict"] | None = None,
138
+ strict: bool = True):
139
+
140
+ self.ser_mode = ser_mode.lower() if ser_mode else None
141
+ self.strict = strict
142
+
143
+ self.unit = unit
144
+ self.description = description
145
+ self.decimals = decimals
146
+
147
+ self.min_shape = np.array(1 if min_shape is None else min_shape, dtype=np.int32)
148
+ self.max_shape = np.array(1 if max_shape is None else max_shape, dtype=np.int32)
149
+ # Make sure all shapes match
150
+ self.min_shape += 0 * self.max_shape
151
+
152
+ self.ge = u.Quantity(ge) if ge is not None else None
153
+ self.gt = u.Quantity(gt) if gt is not None else None
154
+ self.le = u.Quantity(le) if le is not None else None
155
+ self.lt = u.Quantity(lt) if lt is not None else None
156
+
157
+ def validate(
158
+ self,
159
+ v: dict | str | Number | u.Quantity,
160
+ info: core_schema.ValidationInfo | None = None) -> u.Quantity:
161
+
162
+ """
163
+ Validate `Quantity`.
164
+
165
+ Parameters
166
+ ----------
167
+ v: dict | str | numbers.Number ~astropy.units.Quantity
168
+ Quantity that should be validated.
169
+ info: ~pydantic.core_schema.ValidationInfo, optional
170
+ The validation info provided by the Pydantic schema.
171
+
172
+ Returns
173
+ -------
174
+ v: ~astropy.units.Quantity
175
+ Validated `Quantity` with the correct units.
176
+
177
+ Raises
178
+ ------
179
+ ValueError: exception
180
+ An error occurred validating the specified value.
181
+ It is raised if any of the following occur:
182
+ - A `dict` is received and the keys `"value"` and `"units"` do not exist.
183
+ - There are no units provided.
184
+ - Provided units cannot be converted to base units.
185
+ - An unknown unit was provided.
186
+ - An unknown type for value was provided.
187
+ TypeError: exception
188
+ An error occurred from unit registry or unit registry context.
189
+ It is not propagated as a `pydantic.ValidationError` because
190
+ it does not stem from a user error.
191
+ """
192
+ try:
193
+ if isinstance(v, dict):
194
+ v = f"{v['value']} {v.get('unit', '')}"
195
+ except KeyError as e:
196
+ raise ValueError("no `value` or `unit` keys found") from e
197
+
198
+ try:
199
+ if isinstance(v, str):
200
+ # relies on units to return a number if no units are present
201
+ # if value is a quantity, then units are present and check on the units being convertible
202
+ # if value is a number, then check on strict mode will happen next
203
+ v = str_to_quantity_array(v)
204
+ except ValueError as e:
205
+ raise ValueError(e) from e
206
+
207
+ try:
208
+ if isinstance(v, Number) and not self.strict:
209
+ # check must happen after conversion from string because string might not have any units
210
+ # only applicable if dealing with no units and if in strict mode
211
+ v = u.Quantity(v, self.unit)
212
+ if isinstance(v, u.Quantity):
213
+ v = v.to(self.unit)
214
+
215
+ # Check array shape if any.
216
+ shape = np.array(np.array(v.value).shape)
217
+ if (len(shape) == 0 and self.min_shape is not None and \
218
+ np.any(self.min_shape > 1)) or np.any(shape < self.min_shape):
219
+ raise ValueError(
220
+ f"missing components (found {shape}, "
221
+ f"{self.min_shape} expected)")
222
+ if np.any(shape > self.max_shape):
223
+ raise ValueError(
224
+ f"too many components (found {shape}, "
225
+ f"{self.max_shape} expected)")
226
+
227
+ # Check limits if any
228
+ if self.ge is not None and np.any(v < self.ge):
229
+ raise ValueError(f"less than {self.ge}")
230
+ if self.gt is not None and np.any(v <= self.gt):
231
+ raise ValueError(f"equal to or less than {self.gt}")
232
+ if self.le is not None and np.any(v > self.le):
233
+ raise ValueError(f"greater than {self.le}")
234
+ if self.lt is not None and np.any(v >= self.lt):
235
+ raise ValueError(f"equal to or greater than {self.lt}")
236
+ return v
237
+
238
+ except AttributeError as e:
239
+ # raises attribute error if value is a number
240
+ # this case only happen when parsing from a string, the units are not present, and not in strict mode
241
+ # see comments above related to ureg returning a number
242
+ raise ValueError("no units found") from e
243
+ except u.UnitConversionError as e:
244
+ raise ValueError(e) from e
245
+ raise ValueError(f"unknown type {type(v)}")
246
+
247
+
248
+ def serialize(
249
+ self,
250
+ v: u.Quantity,
251
+ info: core_schema.SerializationInfo | None = None,
252
+ *,
253
+ to_json: bool = False) -> dict | str | u.Quantity:
254
+ """
255
+ Serialize `Quantity`.
256
+
257
+ Parameters
258
+ ----------
259
+ v: ~astropy.units.Quantity
260
+ Quantity that should be serialized.
261
+ info: pydantic.core_schema.SerializationInfo, optional
262
+ Serialization info provided by the Pydantic schema.
263
+ to_json: bool, optional
264
+ Whether or not to serialize to a json convertible object.
265
+ Useful if using `QuantityUnit` as a utility outside of Pydantic models.
266
+
267
+ Returns
268
+ -------
269
+ quantity: str
270
+ The serialized `Quantity`.
271
+ """
272
+ to_json = to_json or (info is not None and info.mode_is_json())
273
+
274
+ if self.ser_mode == "dict":
275
+ return {
276
+ "value": v.value.round(self.decimals) if self.decimals is not None \
277
+ else v.value,
278
+ "unit": v.unit if not to_json else f"{v.unit}",
279
+ }
280
+
281
+ if to_json:
282
+ return {
283
+ "value": v.value.round(self.decimals).tolist() \
284
+ if self.decimals is not None \
285
+ else v.value.tolist(),
286
+ "unit": f"{v.unit}"
287
+ }
288
+
289
+ if self.ser_mode == "str":
290
+ return f"{v.round(self.decimals)}" if self.decimals is not None \
291
+ else f"{v}"
292
+
293
+ return v
294
+
295
+ def __get_pydantic_core_schema__(
296
+ self,
297
+ source_type: Any,
298
+ handler: GetCoreSchemaHandler) -> core_schema.CoreSchema:
299
+ """
300
+ Get the Pydantic core schema.
301
+
302
+ Parameters
303
+ ----------
304
+ source_type:
305
+ The source type.
306
+ handler: ~pydantic.GetCoreSchemaHandler
307
+ The `GetCoreSchemaHandler` instance.
308
+
309
+ Returns
310
+ -------
311
+ core_schema: ~pydantic.core_schema.CoreSchema
312
+ The Pydantic core schema.
313
+ """
314
+ _from_typedict_schema = {
315
+ "value": core_schema.typed_dict_field(
316
+ core_schema.str_schema(coerce_numbers_to_str=True)
317
+ ),
318
+ "unit": core_schema.typed_dict_field(core_schema.str_schema()),
319
+ }
320
+
321
+ validate_schema = core_schema.chain_schema([
322
+ core_schema.union_schema([
323
+ core_schema.is_instance_schema(u.Quantity),
324
+ core_schema.str_schema(coerce_numbers_to_str=True),
325
+ core_schema.typed_dict_schema(_from_typedict_schema),
326
+ ]),
327
+ core_schema.with_info_plain_validator_function(self.validate)
328
+ ])
329
+
330
+ """
331
+ validate_json_schema = core_schema.chain_schema([
332
+ core_schema.union_schema([
333
+ core_schema.str_schema(coerce_numbers_to_str=True),
334
+ core_schema.typed_dict_schema(_from_typedict_schema),
335
+ ]),
336
+ core_schema.no_info_plain_validator_function(self.validate)
337
+ ])
338
+ """
339
+ validate_json_schema = core_schema.chain_schema([
340
+ core_schema.str_schema(coerce_numbers_to_str=True),
341
+ core_schema.no_info_plain_validator_function(self.validate)
342
+ ])
343
+
344
+ serialize_schema = core_schema.plain_serializer_function_ser_schema(
345
+ self.serialize,
346
+ info_arg=True,
347
+ )
348
+
349
+ return core_schema.json_or_python_schema(
350
+ json_schema=validate_json_schema,
351
+ python_schema=validate_schema,
352
+ serialization=serialize_schema,
353
+ )
354
+
355
+
356
+
357
+ def AnnotatedQuantity(
358
+ default: u.Quantity | str | None = None,
359
+ unit: str | None = None,
360
+ short: str | None = None,
361
+ description: str = "",
362
+ decimals: int | None = None,
363
+ min_shape: Tuple[int, ...] | None = None,
364
+ max_shape: Tuple[int, ...] | None = None,
365
+ ge: u.Quantity | str | None = None,
366
+ gt: u.Quantity | str | None = None,
367
+ le: u.Quantity | str | None = None,
368
+ lt: u.Quantity | str | None = None) -> Any:
369
+ """
370
+ Pydantic pseudo-field for validating and serializing AstroPy Quantities.
371
+
372
+ Examples
373
+ --------
374
+ >>> from pydantic_settings import BaseSettings
375
+ >>> from .quantity import AnnotatedQuantity
376
+
377
+ >>> class Settings(BaseSettings):
378
+ ... size: AnnotatedQuantity(
379
+ ... short='S',
380
+ ... description="an arbitrary length",
381
+ ... default=10. * u.m,
382
+ ... ge=1. * u.micron,
383
+ ... lt=1. * u.km
384
+ ... )
385
+
386
+ >>> # The following instantiation validates
387
+ >>> s = Settings(size="3. cm")
388
+
389
+ >>> s
390
+ Settings(size=<Quantity 0.03 m>)
391
+
392
+ >>> f"{s!r}"
393
+ 'Settings(size=<Quantity 0.03 m>)'
394
+
395
+ >>> s.model_dump()
396
+ {'size': <Quantity 0.03 m>}
397
+
398
+ >>> s.model_dump(mode="json")
399
+ {'size': '0.03 m'}
400
+
401
+ >>> s.model_json_schema()
402
+ {'additionalProperties': False, 'properties': {'size': {'default':
403
+ '10.0 m', 'description': 'an arbitrary length', 'exclusiveMaximum':
404
+ '1.0 km', 'minimum': '1.0 micron', 'physType': 'length', 'short': 'S',
405
+ 'title': 'Size', 'type': 'string'}}, 'title': 'Settings', 'type':
406
+ 'object'}
407
+
408
+ >>> # The following instantiation does not validate
409
+ >>> s = Settings(size="4 deg")
410
+ Traceback (most recent call last):
411
+ ...
412
+ pydantic_core._pydantic_core.ValidationError: 1 validation error for Settings
413
+ size
414
+ Value error, 'deg' (angle) and 'm' (length) are not convertible
415
+ [type=value_error, input_value='4 deg', input_type=str]
416
+ For further information visit https://errors.pydantic.dev/2.8/v/value_error
417
+
418
+ Parameters
419
+ ----------
420
+ default: ~astropy.units.Quantity or str, optional
421
+ Default value.
422
+ unit: str, optional
423
+ Unit for quantity (overrided by default unit if provided).
424
+ short: str, optional
425
+ shortcut for keyword.
426
+ description: str, optional
427
+ Description string.
428
+ decimals: int, optional
429
+ Maximum number of decimals for the serialization of quantities.
430
+ min_shape: tuple[int], optional
431
+ Minimum number of vector components on each axis.
432
+ max_shape: tuple[int], optional
433
+ Maximum number of vector components on each axis.
434
+ ge: ~astropy.units.Quantity or str, optional
435
+ Lower limit (inclusive).
436
+ gt: ~astropy.units.Quantity or str, optional
437
+ Lower limit (strict).
438
+ le: ~astropy.units.Quantity or str, optional
439
+ Lower limit (inclusive).
440
+ lt: ~astropy.units.Quantity or str, optional
441
+ Lower limit (strict).
442
+ """
443
+ if default is not None:
444
+ default = u.Quantity(default)
445
+ unit = default.unit #type: ignore[union-attr]
446
+ elif unit is None:
447
+ raise ValueError
448
+ physType = u.get_physical_type(u.Quantity("1 " + str(unit)))
449
+ json_extra: dict = {}
450
+ if default is not None:
451
+ json_extra['default'] = default.to_string() #type: ignore[union-attr]
452
+ if min_shape is not None:
453
+ json_extra['minShape'] = str(min_shape)
454
+ if max_shape is not None:
455
+ json_extra['maxShape'] = str(max_shape)
456
+ if lt is not None:
457
+ lt = u.Quantity(lt)
458
+ json_extra['exclusiveMaximum'] = lt.to_string() #type: ignore[union-attr]
459
+ if gt is not None:
460
+ gt = u.Quantity(gt)
461
+ json_extra['exclusiveMinimum'] = gt.to_string() #type: ignore[union-attr]
462
+ if le is not None:
463
+ le = u.Quantity(le)
464
+ json_extra['maximum'] = le.to_string() #type: ignore[union-attr]
465
+ if ge is not None:
466
+ ge = u.Quantity(ge)
467
+ json_extra['minimum'] = ge.to_string() #type: ignore[union-attr]
468
+ if physType is not None:
469
+ json_extra['physType'] = str(physType)
470
+ if short:
471
+ json_extra['short'] = short
472
+ return Annotated[
473
+ u.Quantity,
474
+ QuantityAnnotation(
475
+ unit=unit,
476
+ decimals=decimals,
477
+ min_shape=min_shape,
478
+ max_shape=max_shape,
479
+ ge=ge,
480
+ gt=gt,
481
+ le=le,
482
+ lt=lt
483
+ ),
484
+ Field(
485
+ json_schema_extra=json_extra
486
+ )
487
+ ]
488
+
489
+
490
+
491
+ def str_to_quantity_array(s: str) -> u.Quantity | None:
492
+ """
493
+ Convert string to Astropy "units" Quantity array
494
+
495
+ Notes
496
+ -----
497
+ Currently limited to "well-formed", 1D arrays.
498
+
499
+ Examples
500
+ --------
501
+ >>> from .quantity import str_to_quantity_array
502
+
503
+ >>> str_to_quantity_array("[3.14, 1e+06] m")
504
+ <Quantity [3.14e+00, 1.00e+06] m>
505
+
506
+ Parameters
507
+ ----------
508
+ s: str
509
+ Input string.
510
+
511
+ Returns
512
+ -------
513
+ v: ~astropy.units.Quantity
514
+ Astropy units Quantity object.
515
+ """
516
+ found = findall(
517
+ r"^\s*[\(\[]?((?:[\(\[\)\]\,\;\s]|(?:[\+\-]*\d\.?\d*(?:[eE][\+\-]?\d+)?))+)"
518
+ r"[\)\]]?\s*(\w+[\w\d\s\/\^\*\-\.]*)*$",
519
+ s
520
+ )
521
+ if found is None:
522
+ return None
523
+ # Return result with largest number of components
524
+ value = max(
525
+ [np.fromstring(found[0][0], sep=sep) for sep in [' ', ',', ';']],
526
+ key=lambda x: len(x)
527
+ )
528
+ return u.Quantity(
529
+ value=value if len(value) > 1 else float(value),
530
+ unit=found[0][1]
531
+ )
532
+