casaconfig 1.0.2__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.
@@ -0,0 +1,387 @@
1
+ # Copyright 2020 AUI, Inc. Washington DC, USA
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """
15
+ this module will be included in the api
16
+ """
17
+
18
+ def measures_update(path=None, version=None, force=False, logger=None, auto_update_rules=False, use_astron_obs_table=False, verbose=None):
19
+ """
20
+ Update or install the IERS data used for measures calculations from the ASTRON server at path.
21
+
22
+ Original data source used by ASTRON is here: https://www.iers.org/IERS/EN/DataProducts/data.html
23
+
24
+ If no update is necessary then this function will silently return.
25
+
26
+ The verbose argument controls the level of information provided when this function when the data
27
+ are unchanged for expected reasons. A level of 0 prints and logs nothing. A
28
+ value of 1 logs the information and a value of 2 logs and prints the information.
29
+
30
+ CASA maintains a separate Observatories table which is available in the casarundata
31
+ collection through pull_data and data_update. The Observatories table found at ASTRON
32
+ is not installed by measures_update and any Observatories file at path will not be changed
33
+ by using this function.
34
+
35
+ A text file (readme.txt in the geodetic directory in path) records the measures version string
36
+ and the date when that version was installed in path.
37
+
38
+ If path is None then config.measurespath is used.
39
+
40
+ If the version requested matches the one in that text file then this function does
41
+ nothing unless force is True.
42
+
43
+ If a specific version is not requested (the default) and the modification time of that text
44
+ file is less than 24 hrs before now then this function does nothing unless force is True. When this
45
+ function checks for a more recent version and finds that the installed version is the most recent
46
+ then modification time of that text file is checked to the current time even though nothing has
47
+ changed in path. This limits the number of attempts to update the measures data (including checking f\
48
+ or more recent data) to once per day. When the force argument is True and a specific version is
49
+ not requested then this function always checks for the latest version.
50
+
51
+ When auto_update_rules is True then path must exist and contain the expected readme.txt file.
52
+ Path must be owned by the user, force must be False, and the version must be None. This
53
+ option is used during casatools initialization when measures_auto_update is True. Automatic
54
+ updating happens during casatools initialization so that the updated measures are in place
55
+ before any tool needs to use them.
56
+
57
+ Using measures_update after casatools has started should always be followed by exiting
58
+ and restarting casa (or the casatools module if modular casa components are being used).
59
+
60
+ A file lock is used to prevent more that one data update (pull_data, measures_update,
61
+ or data_update) from updating any files in path at the same time. When locked, the
62
+ lock file (data_update.lock in path) contains information about the process that
63
+ has the lock. When measures_update gets the lock it checks the readme.txt file in path
64
+ to make sure that an update is still necessary (if force is True then an update
65
+ always happens). If the lock file is not empty then a previous update of path (pull_data,
66
+ data_update, or measures_update) did not exit as expected and the contents of path are
67
+ suspect. In that case, an error will be reported and nothing will be updated. The lock
68
+ file can be checked to see the details of when that file was locked. The lock file can be
69
+ removed and measures_update can be tried again. It may be safest in that case to remove path
70
+ completely or use a different path and use pull_data to install a fresh copy of the
71
+ desired version.
72
+
73
+ Care should be used when using measures_update outside of the normal automatic
74
+ update that other casa sessions are not using the same measures at the same time,
75
+ especially if they may also be starting at that time. If a specific version is
76
+ requested or force is True there is a risk that the measures may be updated while
77
+ one of those other sessions are trying to load the same measures data, leading to
78
+ unpredictable results. The lock file will prevent simulateous updates from
79
+ happening but if each simultanous update eventually updates the same measures
80
+ location (because force is True or the updates are requesting different versions)
81
+ then the measures that any of those simultanous casatools modules sees is
82
+ unpredictable. Avoid multiple, simultanous updates outside of the automatic
83
+ update process.
84
+
85
+ **Note:** during auto updates, measures_update requires that the expected
86
+ readme.txt file already exists in the geodetic directory at path. If that file does
87
+ not exist or can not be interpreted as expected then measures_update will
88
+ return without updating any data.
89
+
90
+ **Note:** if auto_update_rules is True the user must own path (in addition to having
91
+ read and write permissions there). The version must then also be None and the force option
92
+ must be False.
93
+
94
+ **Note::** During use outside of auto updates, if path does not exist it will be created
95
+ by this function.
96
+
97
+ **Notes::** During use outside of auto updates, if the readme.txt file exists but can not
98
+ be read as expected **OR** that file does not exist but the contents of path appear to
99
+ contain measures data (table names in the expected locations) then this function will
100
+ print messages describing that and exit without changing anything at path. Using
101
+ a force value of True will disable this check and install measures at path even if path
102
+ is not empty or the readme.txt file can not be read. This use of force should be used
103
+ with caution.
104
+
105
+ Parameters
106
+ - path (str=None) - Folder path to place updated measures data. Must contain a valid geodetic/readme.txt. If not set then config.measurespath is used.
107
+ - version (str=None) - Version of measures data to retrieve (usually in the form of yyyymmdd-160001.ztar, see measures_available()). Default None retrieves the latest.
108
+ - force (bool=False) - If True, always re-download the measures data. Default False will not download measures data if updated within the past day unless the version parameter is specified and different from what was last downloaded.
109
+ - logger (casatools.logsink=None) - Instance of the casalogger to use for writing messages. Default None writes messages to the terminal
110
+ - auto_update_rules (bool=False) - If True then the user must be the owner of path, version must be None, and force must be False.
111
+ - verbose (int=None) - Level of output, 0 is none, 1 is to logger, 2 is to logger and terminal, defaults to casaconfig_verbose in config dictionary.
112
+
113
+ Returns
114
+ None
115
+
116
+ Raises
117
+ casaconfig.AutoUpdatesNotAllowed - raised when path does not exists as a directory or is not owned by the user when auto_update_rules is True
118
+ casaconfig.BadLock - raised when the lock file was not empty when found
119
+ casaconfig.BadReadme - raised when something unexpected is found in the readme or the readme changed after an update is in progress
120
+ casaconfig.NoReadme - raised when the readme.txt file is not found at path (path also may not exist)
121
+ casaconfig.NotWritable - raised when the user does not have permission to write to path
122
+ casaconfig.RemoteError - raised by measures_available when the remote list of measures could not be fetched
123
+ casaconfig.UnsetMeasurespath - raised when path is None and has not been set in config
124
+ Exception - raised when something unexpected happened while updating measures
125
+
126
+ """
127
+ import os
128
+ import pkg_resources
129
+ from datetime import datetime
130
+ import sys
131
+
132
+ from ftplib import FTP
133
+ import tarfile
134
+ import re
135
+ import ssl
136
+ import urllib.request
137
+ import certifi
138
+ import fcntl
139
+
140
+ from casaconfig import measures_available
141
+ from casaconfig import AutoUpdatesNotAllowed, UnsetMeasurespath, RemoteError, NotWritable, BadReadme, BadLock, NoReadme
142
+
143
+ from .print_log_messages import print_log_messages
144
+ from .get_data_lock import get_data_lock
145
+ from .get_data_info import get_data_info
146
+ from .measures_available import measures_available
147
+
148
+ if path is None:
149
+ from .. import config as _config
150
+ path = _config.measurespath
151
+
152
+ if path is None:
153
+ raise UnsetMeasurespath('measures_update: path is None and has not been set in config.measurespath. Provide a valid path and retry.')
154
+
155
+ if verbose is None:
156
+ from .. import config as _config
157
+ verbose = _config.casaconfig_verbose
158
+
159
+ path = os.path.expanduser(path)
160
+
161
+ if auto_update_rules:
162
+ if version is not None:
163
+ print_log_messages('auto_update_rules requires that version be None', logger, True)
164
+ return
165
+ if force:
166
+ print_log_messages('force must be False when auto_update_rules is True', logger, True)
167
+ return
168
+ if (not os.path.isdir(path)) or (os.stat(path).st_uid != os.getuid()):
169
+ raise AutoUpdatesNotAllowed("measures_update: path must exist as a directory and it must be owned by the user, path = %s" % path)
170
+
171
+ if not os.path.exists(path):
172
+ # make dirs all the way down, if possible
173
+ os.makedirs(path)
174
+
175
+ current = None
176
+ ageRecent = False
177
+
178
+ # first, does this look like it needs to be updated
179
+
180
+ # get any existing measures readme information
181
+ readmeInfo = get_data_info(path, logger, type='measures')
182
+ if readmeInfo is not None:
183
+ current = readmeInfo['version']
184
+ if readmeInfo['age'] is not None:
185
+ ageRecent = readmeInfo['age'] < 1.0
186
+
187
+ if not force:
188
+ # don't check for new version if the age is less than 1 day
189
+ if version is None and ageRecent:
190
+ if verbose > 0:
191
+ print_log_messages('measures_update: version installed or checked less than 1 day ago, nothing updated or checked', logger, verbose=verbose)
192
+ return
193
+
194
+ # don't overwrite something that looks bad unless forced to do so
195
+ if current == 'invalid':
196
+ raise NoReadme('measures_update: no measures readme.txt file found at %s. Nothing updated or checked.' % path)
197
+
198
+ if current == 'error':
199
+ raise BadReadme('measures_update: the measures readme.txt file at %s could not be read as expected, an update can not proceed unless force is True' % path)
200
+
201
+ # don't overwrite something that looks like valid measures data unless forced to do so
202
+ if current == 'unknown':
203
+ print_log_messages('measures_update: the measures data at %s is not maintained by casaconfig and so it can not be updated unless force is True' % path, logger)
204
+ return
205
+
206
+ checkVersion = version
207
+ if checkVersion is None:
208
+ # get the current most recent version
209
+ try:
210
+ checkVersion = measures_available()[-1]
211
+ except RemoteError as exc:
212
+ # no network, no point in continuing, just reraise
213
+ raise exc
214
+ except:
215
+ # unsure what happened, leave it at none, which will trigger an update attempt, which might work
216
+ pass
217
+
218
+ # don't re-download the same data
219
+ if (checkVersion is not None) and (checkVersion == current):
220
+ if verbose > 0:
221
+ print_log_messages('measures_update: requested version already installed in %s' % path, logger, verbose=verbose)
222
+ # update the age of the readme to now
223
+ readme_path = os.path.join(path,'geodetic/readme.txt')
224
+ # readme_path should already exist if it's here
225
+ os.utime(readme_path)
226
+
227
+ return
228
+
229
+ # don't do anything unless the Observatories table is already installed as expected
230
+ obsTabPath = os.path.join(path,'geodetic/Observatories')
231
+ if not os.path.isdir(obsTabPath):
232
+ msgs = []
233
+ msgs.append("Error: the Observatories table was not found as expected in %s" % path)
234
+ msgs.append("Either install casarundata first or set use_astron_obs_table and force to be True when using measures_update.")
235
+ msgs.append("Note that the Observatories table provided in the Astron measures tarfile is not the same as that maintained by CASA")
236
+ print_log_messages(msgs, logger, True)
237
+ return
238
+
239
+ # path must be writable with execute bit set
240
+ if (not os.access(path, os.W_OK | os.X_OK)) :
241
+ raise NotWritable('measures_update: No permission to write to path, cannot update : %s' % path)
242
+
243
+ # an update needs to happen
244
+
245
+ # lock the measures_update.lock file
246
+ lock_fd = None
247
+ clean_lock = True # set to false if the contents are actively being u pdate and the lock file should not be cleaned one exception
248
+ try:
249
+ print_log_messages('measures_update ... acquiring the lock ... ', logger)
250
+
251
+ # the BadLock exception that may happen here is caught below
252
+ lock_fd = get_data_lock(path, 'measures_update')
253
+
254
+ do_update = force
255
+
256
+ if not do_update:
257
+ # recheck the readme file, another update may have already happened before the lock was obtained
258
+ current = None
259
+ ageRecent = False
260
+
261
+ readmeInfo = get_data_info(path, logger, type='measures')
262
+ if readmeInfo is not None:
263
+ current = readmeInfo['version']
264
+ if readmeInfo['age'] is not None:
265
+ ageRecent = readmeInfo['age'] < 1.0
266
+
267
+ if (version is not None) and (version == current):
268
+ # no update will be done, version is as requested - always verbose here because the lock is in use
269
+ print_log_messages('The requested measures version is already installed in %s, using version %s' % (path, current), logger)
270
+ elif (version is None) and ageRecent:
271
+ # no update will be done, it's already been checked or updated recently - always verbose here because the lock is in use
272
+ print_log_messages('The latest measures version was checked recently in %s, using version %s' % (path, current), logger)
273
+ else:
274
+ # final check for problems before updating
275
+ if not force and readmeInfo is not None and (version=='invalid' or version=='unknown'):
276
+ # at this point, this indicates something is unexpectedly wrong, do not continue
277
+ # this exception is caught below
278
+ # do not clean up the lock file
279
+ clean_lock = False
280
+ raise BadReadme('measures_update: something unexpected has changed in the path location, can not continue')
281
+ else:
282
+ # an update is needed
283
+ do_update = True
284
+
285
+ if do_update:
286
+ if force:
287
+ print_log_messages('A measures update has been requested by the force argument', logger)
288
+
289
+ print_log_messages(' ... connecting to ftp.astron.nl ...', logger)
290
+
291
+ clean_lock = False
292
+ ftp = FTP('ftp.astron.nl')
293
+ rc = ftp.login()
294
+ rc = ftp.cwd('outgoing/Measures')
295
+ files = sorted([ff for ff in ftp.nlst() if (len(ff) > 0) and (not ff.endswith('.dat')) and (ftp.size(ff) > 0)])
296
+
297
+ # target filename to download
298
+ # for the non-force unspecified version case this can only get here if the age is > 1 day so there should be a newer version
299
+ # but that isn't checked - this could install a version that's already installed
300
+ target = files[-1] if version is None else version
301
+ if target not in files:
302
+ print_log_messages("measures_update can't find specified version %s" % target, logger, True)
303
+
304
+ else:
305
+ # there are files to extract, make sure there's no past measures.ztar from a failed previous install
306
+ ztarPath = os.path.join(path,'measures.ztar')
307
+ if os.path.exists(ztarPath):
308
+ os.remove(ztarPath)
309
+
310
+ with open(ztarPath, 'wb') as fid:
311
+ print_log_messages(' ... downloading %s from ASTRON server to %s ...' % (target, path), logger)
312
+ ftp.retrbinary('RETR ' + target, fid.write)
313
+
314
+ ftp.close()
315
+
316
+ # remove any existing measures readme.txt now in case something goes wrong during extraction
317
+ readme_path = os.path.join(path,'geodetic/readme.txt')
318
+ if os.path.exists(readme_path):
319
+ os.remove(readme_path)
320
+
321
+ # extract from the fetched tarfile
322
+ with tarfile.open(ztarPath, mode='r:gz') as ztar:
323
+ # the list of members to extract
324
+ x_list = []
325
+ for m in ztar.getmembers() :
326
+ if force and use_astron_obs_table:
327
+ # exclude the *.old names in geodetic
328
+ if not(re.search('geodetic',m.name) and re.search('.old',m.name)):
329
+ x_list.append(m)
330
+ else:
331
+ # exclude the Observatories table and *.old names in geodetic
332
+ if not((re.search('geodetic',m.name) and re.search('.old',m.name)) or re.search('Observatories',m.name)):
333
+ x_list.append(m)
334
+
335
+ # use the 'data' filter if available, revert to previous 'fully_trusted' behavior of not available
336
+ ztar.extraction_filter = getattr(tarfile, 'data_filter', (lambda member, path: member))
337
+ ztar.extractall(path=path,members=x_list)
338
+ ztar.close()
339
+
340
+ os.remove(ztarPath)
341
+
342
+ # create a new readme.txt file
343
+ with open(readme_path,'w') as fid:
344
+ fid.write("# measures data populated by casaconfig\nversion : %s\ndate : %s" % (target, datetime.today().strftime('%Y-%m-%d')))
345
+
346
+ print_log_messages(' ... measures data updated at %s' % path, logger)
347
+
348
+ clean_lock = True
349
+
350
+ # closing out the do_update
351
+
352
+ # closing out the try block
353
+
354
+ except BadLock as exc:
355
+ # the path is known to exist so this means that the lock file was not empty and it's not locked
356
+ msgs = [str(exc)]
357
+ msgs.append('The lock file at %s is not empty.' % path)
358
+ msgs.append('A previous attempt to update path may have failed or exited prematurely.')
359
+ msgs.append('Remove the lock file and set force to True with the desired version (default to most recent).')
360
+ msgs.append('It may be best to completely repopulate path using pull_data and measures_update.')
361
+ print_log_messages(msgs, logger, True)
362
+ # reraise this
363
+ raise
364
+
365
+ except BadReadme as exc:
366
+ # something is wrong in the readme after an update was triggered, this shouldn't happen, print more context and reraise this
367
+ msgs = [str(exc)]
368
+ msgs.append('This should not happen unless multiple sessions are trying to update data at the same time and one experienced problems or was done out of sequence')
369
+ msgs.append('Check for other updates in progress or choose a different path or clear out this path and reinstall the casarundata as well as the measures data')
370
+ print_log_messages(msgs, logger, True)
371
+ raise
372
+
373
+ except Exception as exc:
374
+ msgs = []
375
+ msgs.append("ERROR! : Unexpected exception while updating measures at %s" % path)
376
+ msgs.append("ERROR! : %s" % exc)
377
+ print_log_messages(msgs, logger, True)
378
+ raise
379
+
380
+ finally:
381
+ # make sure the lock file is closed and also clean the lock file if safe to do so, this is always executed
382
+ if lock_fd is not None and not lock_fd.closed:
383
+ if clean_lock:
384
+ lock_fd.truncate(0)
385
+ lock_fd.close()
386
+
387
+ return
@@ -0,0 +1,57 @@
1
+ # Copyright 2023 AUI, Inc. Washington DC, USA
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ def print_log_messages(msg, logger, is_err=False, verbose=2):
16
+ """
17
+ Print msg and optionally write it to an instance of the casalogger.
18
+
19
+ If msg is a list then the elements are each printed first followed by logging each element.
20
+
21
+ Messages are normally printed to sys.stdout and logged as INFO to the casalogger.
22
+
23
+ When is_err is True the message is printed sys.stderr and logged as SEVERE
24
+
25
+ When verbose is < 2 then the message is not printed unless there is no logger
26
+
27
+ This function is intended for internal casaconfig use.
28
+
29
+ Parameters
30
+ - msg (str) - The message to print and optionally log.
31
+ - logger (casatools.logsink) - Instance of the casalogger to use. Not used if None.
32
+ - is_err (bool=False) - When False, output goes to sys.stdout and logged as INFO level. When True, output goes to sys.stderr and logged as SEVERE
33
+ - verbose (int=2) - When < 2 then msg is only printed if there is no logger, otherwise it's just logged
34
+
35
+ Returns
36
+ None
37
+
38
+ """
39
+
40
+ import sys
41
+ fileout = sys.stdout
42
+ loglevel = 'INFO'
43
+ if (is_err):
44
+ fileout = sys.stderr
45
+ loglevel = 'SEVERE'
46
+
47
+ # this is rarely called and this should be a fast operation, it makes the code simpler
48
+ if type(msg) is not list:
49
+ msg = [msg]
50
+
51
+ # always print if there is no logger, if there is a logger and verbose is < 2 then do not print
52
+ if (logger is None) or (not verbose < 2):
53
+ for m_msg in msg:
54
+ print(m_msg,file=fileout)
55
+
56
+ for m_msg in msg:
57
+ if logger is not None: logger.post(m_msg, loglevel)