libinsitu 1.7.1__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 (61) hide show
  1. libinsitu/__init__.py +6 -0
  2. libinsitu/_version.py +10 -0
  3. libinsitu/catalog.py +155 -0
  4. libinsitu/cdl.py +336 -0
  5. libinsitu/cli/__init__.py +0 -0
  6. libinsitu/cli/cat.py +360 -0
  7. libinsitu/cli/check.py +36 -0
  8. libinsitu/cli/diff.py +152 -0
  9. libinsitu/cli/enrich_csv.py +440 -0
  10. libinsitu/cli/index.py +234 -0
  11. libinsitu/cli/info.py +58 -0
  12. libinsitu/cli/ls.py +135 -0
  13. libinsitu/cli/qc.py +92 -0
  14. libinsitu/cli/sync.py +470 -0
  15. libinsitu/cli/transform.py +495 -0
  16. libinsitu/cli/update_meta.py +102 -0
  17. libinsitu/common.py +943 -0
  18. libinsitu/handlers/ABOM.py +38 -0
  19. libinsitu/handlers/BSRN.py +43 -0
  20. libinsitu/handlers/ESMAP.py +98 -0
  21. libinsitu/handlers/GenericCSVHandler.py +249 -0
  22. libinsitu/handlers/IEA_PVPS.py +26 -0
  23. libinsitu/handlers/ISE_PVLive.py +17 -0
  24. libinsitu/handlers/METEO_FRANCE.py +20 -0
  25. libinsitu/handlers/NREL_MIDC.py +130 -0
  26. libinsitu/handlers/SAURAN.py +41 -0
  27. libinsitu/handlers/SKYNET.py +70 -0
  28. libinsitu/handlers/SOLRAD.py +25 -0
  29. libinsitu/handlers/SURFRAD.py +31 -0
  30. libinsitu/handlers/UniOregon_readdata.py +117 -0
  31. libinsitu/handlers/__init__.py +34 -0
  32. libinsitu/handlers/base_handler.py +231 -0
  33. libinsitu/handlers/enerMENA.py +113 -0
  34. libinsitu/log.py +176 -0
  35. libinsitu/qc/__init__.py +0 -0
  36. libinsitu/qc/graphs/__init__.py +1 -0
  37. libinsitu/qc/graphs/base.py +1184 -0
  38. libinsitu/qc/graphs/main_layout.py +296 -0
  39. libinsitu/qc/qc_utils.py +725 -0
  40. libinsitu/res/__init__.py +0 -0
  41. libinsitu/res/base.cdl +282 -0
  42. libinsitu/res/index.cdl +51 -0
  43. libinsitu/res/networks.csv +13 -0
  44. libinsitu/res/qc-tests.csv +13 -0
  45. libinsitu/res/station-info/ABOM.csv +22 -0
  46. libinsitu/res/station-info/BSRN.csv +79 -0
  47. libinsitu/res/station-info/ESMAP.csv +17 -0
  48. libinsitu/res/station-info/IEA_PVPS.csv +162 -0
  49. libinsitu/res/station-info/NREL_MIDC.csv +34 -0
  50. libinsitu/res/station-info/SAURAN.csv +22 -0
  51. libinsitu/res/station-info/SKYNET.csv +8 -0
  52. libinsitu/res/station-info/SOLRAD.csv +10 -0
  53. libinsitu/res/station-info/SURFRAD.csv +8 -0
  54. libinsitu/res/station-info/__init__.py +0 -0
  55. libinsitu/res/station-info/enerMENA.csv +13 -0
  56. libinsitu-1.7.1.dist-info/METADATA +81 -0
  57. libinsitu-1.7.1.dist-info/RECORD +61 -0
  58. libinsitu-1.7.1.dist-info/WHEEL +5 -0
  59. libinsitu-1.7.1.dist-info/entry_points.txt +12 -0
  60. libinsitu-1.7.1.dist-info/licenses/LICENSE +25 -0
  61. libinsitu-1.7.1.dist-info/top_level.txt +1 -0
libinsitu/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ from .common import *
2
+ from .catalog import *
3
+ from .qc.qc_utils import *
4
+ from .qc.graphs import *
5
+ from .cli.transform import dataframe_to_netcdf
6
+ from ._version import __version__
libinsitu/_version.py ADDED
@@ -0,0 +1,10 @@
1
+ from importlib.metadata import version, PackageNotFoundError
2
+ import sys
3
+
4
+ if "pytest" in sys.modules:
5
+ __version__ = "TEST"
6
+ else:
7
+ try:
8
+ __version__ = version("libinsitu")
9
+ except PackageNotFoundError:
10
+ __version__ = "DEV"
libinsitu/catalog.py ADDED
@@ -0,0 +1,155 @@
1
+ # Handling fetch and parsing of Thredds server
2
+
3
+ import xml.etree.ElementTree as ET
4
+
5
+ from typing import Dict
6
+ from urllib.parse import urljoin, urlsplit
7
+ import pprint
8
+
9
+ from requests import HTTPError, Session
10
+
11
+ from libinsitu import parallel_map
12
+ from libinsitu.log import info
13
+
14
+ NS={
15
+ "thredds" : "http://www.unidata.ucar.edu/namespaces/thredds/InvCatalog/v1.0",
16
+ "xlink" : "http://www.w3.org/1999/xlink"}
17
+
18
+ XLINK = "{http://www.w3.org/1999/xlink}"
19
+
20
+
21
+ class AutoRepr :
22
+ def __repr__(self):
23
+ return pprint.pformat(self.__dict__)
24
+
25
+ class Dataset(AutoRepr) :
26
+ def __init__(self, id, name, size) :
27
+ self.id = id
28
+ self.name = name
29
+ self.size = size
30
+ self.services = dict() # Dict of service type => URL
31
+
32
+ class Catalog(AutoRepr) :
33
+ def __init__(self, id, name, description, url, link) :
34
+ self.id = id
35
+ self.name = name
36
+ self.description = description
37
+ self.url = url
38
+ self.link = link
39
+ self.authorized = None
40
+ self.datasets : Dict[str, Dataset]= dict()
41
+ self.catalogs : Dict[str, Catalog] = dict() # Dict of sub catalogs
42
+ self.services = dict() # Dict of service type => URL
43
+
44
+ def parse_services(catalog, base):
45
+
46
+ res = dict()
47
+ for service in catalog.findall('.//thredds:service', NS):
48
+ name = service.get("name")
49
+ url = service.get("base")
50
+ if url != "":
51
+ res[name] = base + url
52
+ return res
53
+
54
+ def parse_catalog(el, url, id=None, name=None) :
55
+ """Parse catalog from 'Dataset' element of root catalog or datasetREf/metadata element """
56
+
57
+ if id is None :
58
+ name = el.attrib['name']
59
+ id = el.attrib['ID']
60
+
61
+ # find doc
62
+ metadata = el.find('thredds:metadata', NS) # Optionnal nested node "metadata"
63
+ if metadata is None:
64
+ metadata = el
65
+ docs = metadata.findall('thredds:documentation', NS)
66
+ description = None
67
+ link = None
68
+ for doc in docs:
69
+ if doc.get("type") == "summary":
70
+ description = doc.text
71
+ else:
72
+ link_ = doc.get('{%s}href' % NS["xlink"])
73
+ if link_ is not None:
74
+ link = link_
75
+ return Catalog(id, name, description, url, link)
76
+
77
+ def extract_sub(url, subCatEl) :
78
+ id = subCatEl.attrib[XLINK + "title"]
79
+ href = subCatEl.attrib[XLINK + "href"]
80
+ sub_url = urljoin(url, href)
81
+ return (id, sub_url)
82
+
83
+ def fetch_catalog(url, session=None, recursive=True, parallel=True) :
84
+
85
+ info("Fetching : %s" % url)
86
+
87
+ if session is None :
88
+ session = Session()
89
+
90
+ base = base_url(url)
91
+ xml = http_get(url, session)
92
+
93
+ catalogEl = ET.fromstring(xml)
94
+ datasetEl = catalogEl.find('thredds:dataset', NS)
95
+
96
+ catalog = parse_catalog(datasetEl, url)
97
+ catalog.services = parse_services(catalogEl, base)
98
+
99
+ # Parse or fetch sub catalogs
100
+
101
+ sub_elements = list(datasetEl.findall("thredds:catalogRef", NS))
102
+
103
+ def fetch_rec(subCatEl) :
104
+ id, sub_url = extract_sub(url, subCatEl)
105
+
106
+ try:
107
+ sub_catalog = fetch_catalog(sub_url, session, parallel=parallel)
108
+ return (id, sub_catalog)
109
+ except HTTPError as e:
110
+ if e.response.status_code == 401:
111
+ print("URL %s not authorized." % e.request.url)
112
+ else:
113
+ raise e
114
+
115
+ if recursive :
116
+ # Common loop for sequential of parallel fetch
117
+ for id, sub_catalog in parallel_map(fetch_rec, sub_elements, parallel):
118
+ catalog.catalogs[id] = sub_catalog
119
+ else :
120
+ for subCatEl in sub_elements :
121
+ id, sub_url = extract_sub(url, subCatEl)
122
+ catalog.catalogs[id] = parse_catalog(subCatEl, sub_url, id, id)
123
+
124
+ # Datasets
125
+ datasets = datasetEl.findall("thredds:dataset", NS)
126
+
127
+ for dataset_el in datasets :
128
+
129
+ uri = dataset_el.attrib["urlPath"]
130
+ id = dataset_el.attrib["ID"]
131
+ name = dataset_el.attrib["name"]
132
+ size = float(dataset_el.find("thredds:dataSize", NS).text)
133
+
134
+ dataset = Dataset(id, name, size)
135
+ dataset.services = {key: base + uri for key, base in catalog.services.items()}
136
+
137
+ catalog.datasets[dataset.name] = dataset
138
+
139
+ return catalog
140
+
141
+ def base_url(url) :
142
+ parts = urlsplit(url)
143
+ return "%s://%s" % (parts.scheme, parts.netloc)
144
+
145
+
146
+
147
+ def http_get(url, session) :
148
+
149
+ #print("fetching : %s" % url)
150
+
151
+ res = session.get(url)
152
+ res.raise_for_status()
153
+
154
+ res.encoding = res.apparent_encoding
155
+ return res.text
libinsitu/cdl.py ADDED
@@ -0,0 +1,336 @@
1
+ import re
2
+ from copy import deepcopy
3
+ from typing import Dict, List, Any
4
+ from libinsitu import STATION_NAME_VAR, DefaultDict, read_str, readSingleVar
5
+ from libinsitu.common import parse_value, DATA_VARS, read_res, CDL_PATH, LONGITUDE_VAR, LATITUDE_VAR, ELEVATION_VAR, \
6
+ fill_str, TIME_VAR
7
+ from libinsitu.log import info, warning
8
+ import numpy as np
9
+ from enum import Enum
10
+
11
+ VALUE_ATTR = "_value"
12
+ FILL_VALUE_ATTR = "_FillValue"
13
+ SYSTEM_ATTRIBUTES = [FILL_VALUE_ATTR, VALUE_ATTR]
14
+
15
+ class Variable :
16
+ def __init__(self, name:str, type:str, dimensions:List[str]):
17
+ self.type = type
18
+ self.dimensions = dimensions
19
+ self.name = name
20
+ self.attributes = {}
21
+
22
+ class CDL :
23
+ def __init__(self):
24
+ self.dimensions : Dict[str, int] = {}
25
+ self.variables : Dict[str, Variable] = {}
26
+ self.global_attributes = {}
27
+
28
+ class CDLType:
29
+ CHAR = "char"
30
+ STRING = "string"
31
+ FLOAT = "float"
32
+ DOUBLE = "double"
33
+ INT = "int"
34
+ SHORT = "short"
35
+ UINT = "uint"
36
+
37
+ class NetCDFType :
38
+ CHAR = "c"
39
+ STRING = str
40
+ FLOAT = "f4"
41
+ DOUBLE = "f8"
42
+ INT = "i4"
43
+ SHORT = "i2"
44
+ UINT = "u4"
45
+
46
+ # CDL to NetCDF types
47
+ CDL_TYPES_MAP = {
48
+ CDLType.CHAR : NetCDFType.CHAR,
49
+ CDLType.STRING: NetCDFType.STRING,
50
+ CDLType.FLOAT: NetCDFType.FLOAT,
51
+ CDLType.DOUBLE: NetCDFType.DOUBLE,
52
+ CDLType.INT: NetCDFType.INT,
53
+ CDLType.SHORT: NetCDFType.SHORT,
54
+ CDLType.UINT: NetCDFType.UINT}
55
+
56
+
57
+
58
+ # Cache to CDL
59
+ _CDL:CDL = None
60
+
61
+ def init_cdl(properties=DefaultDict(lambda : "-"), custom_cdl = None) :
62
+ global _CDL
63
+ # Read CDL from resource or custom file
64
+ if custom_cdl is None:
65
+ cdl_file = read_res(CDL_PATH)
66
+ else:
67
+ info("Using custom CDL file %s" % custom_cdl)
68
+ cdl_file = open(custom_cdl, "r")
69
+ _CDL = parse_cdl(cdl_file, properties)
70
+
71
+ def get_cdl(init=False) :
72
+ if _CDL is None:
73
+ if not init :
74
+ raise Exception("No CDL set yet")
75
+ else:
76
+ init_cdl(DefaultDict(lambda : "-"))
77
+ return _CDL
78
+
79
+
80
+
81
+ def replace_placeholders(strval, attributes) :
82
+
83
+ def repl(m) :
84
+ key = m.group().strip("{").strip("}")
85
+
86
+ mandatory = False
87
+
88
+ if "!" in key :
89
+ mandatory = True
90
+ key = key.replace("!", "")
91
+
92
+ if not key in attributes :
93
+ if mandatory:
94
+ raise Exception(f"Missing mandatory attribute '{key}'")
95
+ else:
96
+ warning("Key : '%s' not found in attributes, using empty string instead" % key)
97
+ return ""
98
+
99
+ res = attributes[key]
100
+ return "" if res is None else str(res)
101
+
102
+ return re.sub(r'{\!?\w+}', repl, strval)
103
+
104
+ def parse_cdl(lines, attributes=dict()) :
105
+ """ Parse CDL file """
106
+
107
+ res = CDL()
108
+
109
+ section = None
110
+ curr_var = None
111
+
112
+ for line in lines :
113
+ line = line.strip()
114
+
115
+ # Skip comments
116
+ if line.startswith("#") or len(line) == 0:
117
+ continue
118
+
119
+ if "#" in line :
120
+ line = line.split("#")[0]
121
+ line = line.strip()
122
+
123
+ # key = value
124
+ if "=" in line :
125
+ line = line.strip(";")
126
+ key, val = line.split("=", 1)
127
+ key = key.strip()
128
+ val = parse_value(val.strip(), split=True)
129
+
130
+ if isinstance(val, str):
131
+ val = replace_placeholders(val.strip(), attributes)
132
+
133
+ # Defining dimension
134
+ if section == "dimensions" :
135
+
136
+ dim = 0 if val == "UNLIMITED" else int(val)
137
+ res.dimensions[key] = dim
138
+
139
+ elif section == "variables" :
140
+ varname, attrname = key.split(":")
141
+
142
+ if varname == "*" :
143
+ varname = curr_var
144
+
145
+ if varname == "" :
146
+ res.global_attributes[attrname] = val
147
+ else :
148
+ res.variables[varname].attributes[attrname] = val
149
+ else :
150
+ raise Exception("Assignement outside any section : %s" % line)
151
+
152
+
153
+ # Skip start or end
154
+ elif "{" in line or "}" in line :
155
+ continue
156
+
157
+ # Change section
158
+ elif ":" in line :
159
+ section = line.strip(":").strip()
160
+ continue
161
+
162
+ elif ";" in line :
163
+ # New var
164
+ line = line.strip(";").strip()
165
+ type, var = line.split()
166
+
167
+ # Transform CDL type to NetCDF type
168
+ type = CDL_TYPES_MAP[type]
169
+
170
+ dims=[]
171
+ if "(" in var :
172
+ var, dims = var.split("(")
173
+ dims = dims.strip(")").strip().split(",")
174
+ res.variables[var] = Variable(var, type, dims)
175
+ curr_var = var
176
+ else :
177
+ raise Exception("Bad line : %s" %line)
178
+
179
+ return res
180
+
181
+ def update_attributes(dest_var, attrs:Dict[str, Any], dry_run=False, delete=False) :
182
+
183
+ dry_prefix = "would " if dry_run else ""
184
+
185
+ existing_attrs = set(dest_var.ncattrs())
186
+ new_attrs = set(attrs.keys())
187
+
188
+ extra_attrs = existing_attrs - new_attrs
189
+
190
+ if delete :
191
+ for attrname in extra_attrs :
192
+ info(dry_prefix + "delete attribute %s#%s" % (dest_var.name, attrname))
193
+ if not dry_run :
194
+ dest_var.delncattr(attrname)
195
+
196
+ for key, val in attrs.items() :
197
+ oldval = None if not key in existing_attrs else dest_var.getncattr(key)
198
+
199
+ if key in SYSTEM_ATTRIBUTES :
200
+ # Do not update system attributes
201
+ continue
202
+
203
+ if oldval != val :
204
+
205
+ if (val is None or val == "") and not delete :
206
+ continue
207
+
208
+ info(dry_prefix + "update attribute %s#%s %s -> %s" % (dest_var.name, key, oldval, val))
209
+
210
+ if not dry_run:
211
+ dest_var.setncattr(key, val)
212
+
213
+ def update_value(dest_var, vardef:Variable, dry_run=False):
214
+ """"Update single value from _value attribute"""
215
+
216
+ # No _value attribute ?
217
+ if not VALUE_ATTR in vardef.attributes :
218
+ return
219
+
220
+ value = vardef.attributes[VALUE_ATTR]
221
+
222
+ if len(vardef.dimensions) > 0:
223
+ raise Exception(f"Variable {vardef.name} : _value attribute only supported on non zero dimention variables")
224
+
225
+ if vardef.type == NetCDFType.STRING :
226
+ old_val = read_str(dest_var)
227
+ else:
228
+ old_val = readSingleVar(dest_var)
229
+
230
+ prefix = "would " if dry_run else ""
231
+ info(f'{prefix}replace value {old_val} => {value} in {vardef.name}')
232
+
233
+ if dry_run:
234
+ return
235
+
236
+ # Skip empty value
237
+ if value is None or value == "":
238
+ return
239
+
240
+ dest_var[0] = value
241
+
242
+
243
+
244
+ def cmp_var(var, vardef:Variable) :
245
+ return var.dtype == vardef.type and var.dimensions == tuple(vardef.dimensions)
246
+
247
+ def create_or_replace_var(ncfile, vardef:Variable, dry_run=False) :
248
+
249
+ if vardef.name in ncfile.variables:
250
+
251
+ var = ncfile.variables[vardef.name]
252
+
253
+ if cmp_var(var, vardef):
254
+ # Same var -> skipping
255
+ return
256
+
257
+ # Different vars
258
+ if dry_run:
259
+ info("Would replace var : %s" % vardef.name)
260
+ else:
261
+ info("Replacing var : %s" % vardef.name)
262
+ del ncfile.variables[vardef.name]
263
+
264
+ if dry_run :
265
+ info("Would add variable : %s" % vardef.name)
266
+ return
267
+
268
+ least_significant_digit = vardef.attributes.get("least_significant_digit", None)
269
+ fill_value = vardef.attributes.get(FILL_VALUE_ATTR, None)
270
+
271
+ info("Adding variable '%s'. Precision:%s" % (vardef.name, least_significant_digit))
272
+
273
+ ncfile.createVariable(
274
+ vardef.name, vardef.type, vardef.dimensions,
275
+ zlib=vardef.type is not str,
276
+ complevel=9,
277
+ least_significant_digit=least_significant_digit,
278
+ fill_value=fill_value)
279
+
280
+ def initVar(ncfile, vardef:Variable, dry_run=False, delete_attrs=False) :
281
+
282
+ create_or_replace_var(ncfile, vardef, dry_run)
283
+
284
+ var = ncfile.variables[vardef.name]
285
+
286
+ # Update attributes
287
+ update_attributes(var, vardef.attributes, dry_run, delete_attrs)
288
+
289
+ # Update single value if any
290
+ update_value(var, vardef, dry_run=dry_run)
291
+
292
+ def cdl2netcdf(ncfile, cdl: CDL, dry_run=False, delete_attrs=False) :
293
+ """Init NetCDF file from a CDL"""
294
+
295
+ for dimname, dim in cdl.dimensions.items() :
296
+
297
+ # Already there, skipping
298
+ if not dimname in ncfile.dimensions and not dry_run :
299
+ info("Adding dimension '%s'", dimname)
300
+ ncfile.createDimension(dimname, dim)
301
+
302
+ for varname, vardef in cdl.variables.items() :
303
+ initVar(ncfile, vardef, dry_run, delete_attrs)
304
+
305
+ # Update global attributes
306
+ update_attributes(ncfile, cdl.global_attributes, dry_run, delete_attrs)
307
+
308
+
309
+ def init_nc(netcdf, properties, data_vars=DATA_VARS, dry_run=False, delete_attrs=False, custom_cdl=None) :
310
+
311
+ init_cdl(properties, custom_cdl)
312
+
313
+ cdl = get_cdl()
314
+
315
+ # Ensures all requested data vars are defined
316
+ missing_vars = set(data_var for data_var in data_vars if data_var not in cdl.variables)
317
+ if len(missing_vars) > 0 :
318
+ raise Exception("Unknown data vars : %s" % missing_vars)
319
+
320
+ filtered_cdl = deepcopy(cdl)
321
+
322
+ # Filter data vars (variables with "time" dimension)
323
+ # Also adds the "Time" variable
324
+ filtered_cdl.variables = dict()
325
+ for key, var in cdl.variables.items() :
326
+ if "time" in var.dimensions and not key in data_vars + [TIME_VAR] :
327
+ continue
328
+ filtered_cdl.variables[key] = var
329
+
330
+ cdl2netcdf(netcdf, filtered_cdl, dry_run, delete_attrs)
331
+
332
+ return cdl
333
+
334
+
335
+
336
+
File without changes