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,227 @@
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
+ this module will be included in the api
16
+ """
17
+
18
+ def get_data_info(path=None, logger=None, type=None):
19
+ """
20
+ Get the summary information on the 3 types of data managed by casaconfig.
21
+
22
+ The return value is a dictionary by type of data : 'casarundata', 'measures',
23
+ and 'release'. Each type is itself a dictionary as described below.
24
+
25
+ The path is the location to use to search for the installed release information.
26
+ The path argument defaults to config.measurespath when not set.
27
+
28
+ The return value can be limited to just a specific type using the type
29
+ argument ('casarundata', 'measures', 'release'). In that case the returned value
30
+ is the dictionary for just the requested type. When type is None (the default)
31
+ the full set of dictionaries is returned.
32
+
33
+ The 'casarundata' and 'measures' dictionaries contain 'version'
34
+ and 'date' where version is the version string and date is the date when it
35
+ was installed. These values are taken from the readme.txt file for each type.
36
+ For 'casarundata' an additional field of 'manifest' is present which is
37
+ the list of files that have been installed for that specific version (this will
38
+ be empty for an unknown or invalid version).
39
+
40
+ The 'release' dictionary comes from the release_data_readme.txt file which is copied
41
+ into place when a modular CASA is built. It consists of 'casarundata' and 'measures'
42
+ dictionaries where each dictionary contains the 'version' string and 'date' that that
43
+ version was created for the described release. This allows casaconfig to install that
44
+ casarundata version for testing purposes. The release information does not depend
45
+ on the path argument since it is found in the casaconfig module.
46
+
47
+ If path is empty or does not exist then the return value for the 'casarundata' and
48
+ 'measures' types is None.
49
+
50
+ If no readme.txt file can be found at path but the contents of path have the directories
51
+ expected for casarundata then the version returned for 'casarundata' is 'unknown' and
52
+ the date is an empty string. In that case the path may contain casarundata from a legacy
53
+ installation of CASA data. CASA will be able to use the files at this location but they
54
+ can not be maintained by casaconfig. If the path is not empty (except for a possible lock
55
+ file while it's in use but does not appear to contain legacy casarundata or the readme.txt
56
+ file found there can not be read as expected then the version is 'invalid'.
57
+
58
+ If no readme.txt file can be found for the measures at path/geodetic but both the geodetic
59
+ and ephemeris directories are present in path then the version returned for 'measures' is
60
+ 'unknown' and the date is an empty string. The path location may contain measures data from
61
+ a legacy installation of CASA data. CASA will be able to use any measures tables at this
62
+ location by they can not be maintained by casaconfig. If the path is not empty but does not
63
+ appear to contain legacy measures data or the readme.txt file found in path/geodetic can
64
+ not be read as expected then the version is 'invalid'.
65
+
66
+ If no casadata release information is found or the contents are unexpected the returned
67
+ value for 'release' is None and the "--reference-testing" option will not do anything
68
+ for this installation of casaconfig. This will be the case for a modular installation.
69
+
70
+ If path has not been set (has a value of None) then the returned value will be None. This
71
+ likely means that a casasiteconfig.py exists but has not yet been edited to set measurespath.
72
+
73
+ Parameters
74
+ - path (str) - Folder path to find the casarundata and measures data information. If not set then config.measurespath is used.
75
+ - logger (casatools.logsink=None) - Instance of the casalogger to use for writing messages. Default None writes messages to the terminal.
76
+ - type (str) - the specific type of data info to return (None, 'casarundata', 'measures', 'release'; None returns a dictionary of all types)
77
+
78
+ Returns
79
+ - a dictionary by type, 'casarundata', 'measures', 'release' where each type is a dictionary containing 'version' and 'date'. A return value of None indicates path is unset. A value of None for that type means no information could be found about that type. If a specific type is requested then only the dictionary for that type is returned (or None if that type can not be found).
80
+
81
+ Raises
82
+ - casaconfig.UnsetMeasurespath - path is None and has not been set in config
83
+ - ValueError - raised when type has an invalid value
84
+
85
+ """
86
+
87
+ result = None
88
+
89
+ import os
90
+ import time
91
+ import importlib.resources
92
+ from .print_log_messages import print_log_messages
93
+ from .read_readme import read_readme
94
+
95
+ from casaconfig import UnsetMeasurespath
96
+
97
+
98
+ currentTime = time.time()
99
+ secondsPerDay = 24. * 60. * 60.
100
+
101
+ if path is None:
102
+ from .. import config as _config
103
+ path = _config.measurespath
104
+
105
+ if path is None:
106
+ raise UnsetMeasurespath('get_data_info: path is None and has not been set in config.measurespath. Provide a valid path and retry.')
107
+
108
+ path = os.path.abspath(os.path.expanduser(path))
109
+
110
+ result = {'casarundata':None, 'measures':None, 'release':None}
111
+
112
+ if type is not None and type not in result:
113
+ raise ValueError("invalid type %s; must be one of None, 'casarundata', 'measures', 'release'" % type)
114
+
115
+ # casarundata and measures
116
+
117
+ if os.path.isdir(path) and (len(os.listdir(path))>0):
118
+ # if the only thing at path is the lock file then proceed as if path is empty - skip this section
119
+ pathfiles = os.listdir(path)
120
+ if len(pathfiles) == 1 and pathfiles[0] == "data_update.lock":
121
+ pass
122
+ else:
123
+ # there's something at path, look for the casarundata readme
124
+ if type is None or type=='casarundata':
125
+ datareadme_path = os.path.join(path,'readme.txt')
126
+ if os.path.exists(datareadme_path):
127
+ # the readme exists, get the info
128
+ result['casarundata'] = {'version':'error', 'date':'', 'manifest':[], 'age':None}
129
+ readmeContents = read_readme(datareadme_path)
130
+ if readmeContents is not None:
131
+ currentAge = (currentTime - os.path.getmtime(datareadme_path)) / secondsPerDay
132
+ currentVersion = readmeContents['version']
133
+ currentDate = readmeContents['date']
134
+ # the manifest ('extra') must exist with at least 1 entry, otherwise this is no a valid readme file and the version should be 'error'
135
+ if len(readmeContents['extra']) > 0:
136
+ result['casarundata'] = {'version':currentVersion, 'date':currentDate, 'manifest':readmeContents['extra'], 'age':currentAge}
137
+ else:
138
+ # does it look like it's probably casarundata?
139
+ expected_dirs = ['alma','catalogs','demo','ephemerides','geodetic','gui','nrao']
140
+ ok = True
141
+ for d in expected_dirs:
142
+ if not os.path.isdir(os.path.join(path,d)): ok = False
143
+ if ok:
144
+ # probably casarundata
145
+ result['casarundata'] = {'version':'unknown', 'date':'', 'manifest': None,'age':None}
146
+ else:
147
+ # probably not casarundata
148
+ # this is invalid, unexpected things are happening there
149
+ result['casarundata'] = {'version':'invalid', 'date':'', 'manifest': None, 'age':None}
150
+
151
+ if type is None or type=='measures':
152
+ # look for the measures readme
153
+ measuresreadme_path = os.path.join(path,'geodetic/readme.txt')
154
+ if os.path.exists(measuresreadme_path):
155
+ # the readme exists, get the info
156
+ result['measures'] = {'version':'error', 'date':'', 'age':None}
157
+ readmeContents = read_readme(measuresreadme_path)
158
+ if readmeContents is not None:
159
+ currentVersion = readmeContents['version']
160
+ currentDate = readmeContents['date']
161
+ currentAge = (currentTime - os.path.getmtime(measuresreadme_path)) / secondsPerDay
162
+ result['measures'] = {'version':currentVersion,'date':currentDate,'age':currentAge}
163
+ else:
164
+ # does it look like it's probably measuresdata?
165
+ # path should have ephemerides and geodetic directories
166
+ if os.path.isdir(os.path.join(path,'ephemerides')) and os.path.isdir(os.path.join(path,'geodetic')):
167
+ result['measures'] = {'version':'unknown', 'date':'', 'age':None}
168
+ else:
169
+ # probably not measuresdata
170
+ result['measures'] = {'version':'invalid', 'date':'', 'age':None}
171
+
172
+ if type is None or type=='release':
173
+ # release data versions
174
+ if importlib.resources.is_resource('casaconfig','release_data_readme.txt'):
175
+ try:
176
+ casarundataVersion = None
177
+ measuresVersion = None
178
+ ok = True
179
+ reason = None
180
+ readme_lines = importlib.resources.read_text('casaconfig','release_data_readme.txt').split('\n')
181
+ for readmeLine in readme_lines:
182
+ # lines must contain something and not start with #
183
+ if len(readmeLine) > 0 and readmeLine[0] != '#':
184
+ splitLine = readmeLine.split(':')
185
+ if len(splitLine) == 2:
186
+ lineType = splitLine[0].strip()
187
+ lineVers = splitLine[1].strip()
188
+ if lineType == 'casarundata':
189
+ if casarundataVersion is not None:
190
+ ok = False
191
+ reason = "duplicate casarundata lines"
192
+ break
193
+ casarundataVersion = lineVers
194
+ elif lineType == 'measures':
195
+ if measuresVersion is not None:
196
+ ok = False
197
+ reason = "duplicate measures lins"
198
+ break
199
+ measuresVersion = lineVers
200
+ else:
201
+ ok = False
202
+ reason = "Unexpected type : %s" % lineType
203
+ break
204
+ else:
205
+ ok = False
206
+ reason = "Missing or too many ':' separators"
207
+ if (casarundataVersion is None or measuresVersion is None) and ok:
208
+ ok = False
209
+ reason = "missing one or more version strings for expected casarundata and measures types"
210
+
211
+ if not ok:
212
+ print_log_messages("Incorrectly formatted release_data_readme.txt. %s" % reason, logger, True)
213
+ # leave 'release' as None
214
+ else:
215
+ result['release'] = {'casarundata':casarundataVersion, 'measures':measuresVersion}
216
+ except:
217
+ print("Unexpected error reading release_data_readme.txt")
218
+ # leave 'release' as None
219
+ else:
220
+ # no release information available, probably a modular install only
221
+ # leave 'release' as None
222
+ pass
223
+
224
+ if type is not None:
225
+ result = result[type]
226
+
227
+ return result
@@ -0,0 +1,86 @@
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 get_data_lock(path, fn_name):
16
+ """
17
+ Get and initialize and set the lock on 'data_update.log' in path.
18
+
19
+ If path does not already exist or the lock file is not empty then a lock
20
+ is not set then a BadLock exception is raised.
21
+
22
+ When a lock is set the lock file will contain the user, hostname, pid,
23
+ date, and time.
24
+
25
+ The opened file descriptor holding the lock is returned.
26
+
27
+ The caller is responsible for closing the returned file descriptor to release the
28
+ lock. The lock file should be truncated if everything went as expected.
29
+
30
+ This function is intended for internal casaconfig use.
31
+
32
+ Parameters
33
+ - path (str) - The location where 'data_update.log' is to be found.
34
+ - fn_name (str) - A string giving the name of the calling function to be recorded in the lock file.
35
+
36
+ Returns:
37
+ - the open file descriptor holding the lock. Close this file descriptor to release the lock.
38
+
39
+ Raises:
40
+ - casaconfig.BadLock - raised when the path to the lock file does not exist or the lock file is not empty as found
41
+ - Exception - an unexpected exception was seen while writing the lock information to the file
42
+
43
+ """
44
+
45
+ import fcntl
46
+ import os
47
+ import getpass
48
+ from datetime import datetime
49
+
50
+ from casaconfig import BadLock
51
+
52
+ if not os.path.exists(path):
53
+ raise BadLock("path to contain lock file does not exist : %s" % path)
54
+
55
+ lock_path = os.path.join(path, 'data_update.lock')
56
+ lock_exists = os.path.exists(lock_path)
57
+
58
+ # open and lock the lock file - don't truncate the lock file here if it already exists, wait until it's locked
59
+ mode = 'r+' if os.path.exists(lock_path) else 'w'
60
+ lock_fd = open(lock_path, mode)
61
+ fcntl.lockf(lock_fd, fcntl.LOCK_EX)
62
+
63
+ # see if the lock file is empty
64
+ if (lock_exists):
65
+ lockLines = lock_fd.readlines()
66
+ if (len(lockLines) > 0) and (len(lockLines[0]) > 0):
67
+ # not empty
68
+ lock_fd.close()
69
+ raise BadLock("lock file is not empty : %s" % lock_path)
70
+
71
+ # write the lock information, the seek and truncate are probably not necessary
72
+ try:
73
+ lock_fd.seek(0)
74
+ lock_fd.truncate(0)
75
+ lock_fd.write("locked using %s by %s on %s : pid = %s at %s" % (fn_name, getpass.getuser(), os.uname().nodename, os.getpid(), datetime.today().strftime('%Y-%m-%d:%H:%M:%S')))
76
+ lock_fd.flush()
77
+ except Exception as exc:
78
+ print("ERROR! Unexpected failure in writing lock information to lock file %s" % lock_path)
79
+ print("ERROR! Called by function : %s" % fn_name)
80
+ print("ERROR! : %s" % exc)
81
+ lock_fd.close()
82
+ # reraise the exception - this shouldn't happen
83
+ raise exc
84
+
85
+ return lock_fd
86
+
@@ -0,0 +1,74 @@
1
+ import os
2
+ import sys
3
+ from contextlib import contextmanager
4
+
5
+ def _fileno(file_or_fd):
6
+ fd = getattr(file_or_fd, 'fileno', lambda: file_or_fd)()
7
+ if not isinstance(fd, int):
8
+ raise ValueError("Expected a file (`.fileno()`) or a file descriptor")
9
+ return fd
10
+
11
+ @contextmanager
12
+ def stdout_redirected(to=os.devnull, stdout=None):
13
+ if stdout is None:
14
+ stdout = sys.stdout
15
+
16
+ stdout_fd = _fileno(stdout)
17
+ # copy stdout_fd before it is overwritten
18
+ #NOTE: `copied` is inheritable on Windows when duplicating a standard stream
19
+ with os.fdopen(os.dup(stdout_fd), 'wb') as copied:
20
+ stdout.flush() # flush library buffers that dup2 knows nothing about
21
+ try:
22
+ os.dup2(_fileno(to), stdout_fd) # $ exec >&to
23
+ except ValueError: # filename
24
+ with open(to, 'wb') as to_file:
25
+ os.dup2(to_file.fileno(), stdout_fd) # $ exec > to
26
+ try:
27
+ yield stdout # allow code to be run with the redirected stdout
28
+ finally:
29
+ # restore stdout to its previous value
30
+ #NOTE: dup2 makes stdout_fd inheritable unconditionally
31
+ stdout.flush()
32
+ os.dup2(copied.fileno(), stdout_fd) # $ exec >&copied
33
+
34
+ @contextmanager
35
+ def all_redirected(to=os.devnull):
36
+ stdout = sys.stdout
37
+ stderr = sys.stderr
38
+
39
+ stdout_fd = _fileno(stdout)
40
+ stderr_fd = _fileno(stderr)
41
+ # copy stdout_fd before it is overwritten
42
+ #NOTE: `copied` is inheritable on Windows when duplicating a standard stream
43
+ with os.fdopen(os.dup(stdout_fd), 'wb') as out_copied, os.fdopen(os.dup(stderr_fd), 'wb') as err_copied:
44
+ stdout.flush() # flush library buffers that dup2 knows nothing about
45
+ try:
46
+ os.dup2(_fileno(to), stdout_fd) # $ exec >&to
47
+ os.dup2(_fileno(to), stderr_fd) # $ exec >&to
48
+ except ValueError: # filename
49
+ with open(to, 'wb') as to_file:
50
+ os.dup2(to_file.fileno(), stdout_fd) # $ exec > to
51
+ os.dup2(to_file.fileno(), stderr_fd) # $ exec > to
52
+ try:
53
+ yield stdout # allow code to be run with the redirected stdout
54
+ finally:
55
+ # restore stdout to its previous value
56
+ #NOTE: dup2 makes stdout_fd inheritable unconditionally
57
+ stdout.flush()
58
+ os.dup2(out_copied.fileno(), stdout_fd) # $ exec >&copied
59
+ os.dup2(err_copied.fileno(), stderr_fd) # $ exec >&copied
60
+
61
+ @contextmanager
62
+ def redirect_stdout(new_target):
63
+ old_target, sys.stdout = sys.stdout, new_target # replace sys.stdout
64
+ try:
65
+ yield new_target # run some code with the replaced stdout
66
+ finally:
67
+ sys.stdout = old_target # restore to the previous value
68
+
69
+ def merged_stderr_stdout(): # $ exec 2>&1
70
+ return stdout_redirected(to=sys.stdout, stdout=sys.stderr)
71
+
72
+ @contextmanager
73
+ def no_redirect( ):
74
+ yield None
@@ -0,0 +1,59 @@
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
+
19
+ def measures_available():
20
+ """
21
+ List available measures versions on the ASTRON server
22
+
23
+ This returns a list of the measures files available on the ASTRON
24
+ server. The version parameter of measures_update must be one
25
+ of the values in that list if set (otherwise the most recent version
26
+ in this list is used).
27
+
28
+ Parameters
29
+ None
30
+
31
+ Returns
32
+ version names returned as list of strings
33
+
34
+ Raises:
35
+ - casaconfig.RemoteError - raised when when a socket.gaierror is seen, likely due to no network connection
36
+ - Exception: raised when any unexpected exception happens
37
+
38
+ """
39
+ from ftplib import FTP
40
+ import socket
41
+
42
+ from casaconfig import RemoteError
43
+
44
+ files = []
45
+ try:
46
+ ftp = FTP('ftp.astron.nl')
47
+ rc = ftp.login()
48
+ rc = ftp.cwd('outgoing/Measures')
49
+ files = ftp.nlst()
50
+ ftp.quit()
51
+ #files = [ff.replace('WSRT_Measures','').replace('.ztar','').replace('_','') for ff in files]
52
+ files = [ff for ff in files if (len(ff) > 0) and (not ff.endswith('.dat'))]
53
+ except socket.gaierror as gaierr:
54
+ raise RemoteError("Unable to retrieve list of available measures versions : " + str(gaierr)) from None
55
+ except Exception as exc:
56
+ msg = "Unexpected exception while getting list of available measures versions : " + str(exc)
57
+ raise Exception(msg)
58
+
59
+ return files