pthelma 1.0.0__cp310-cp310-win_amd64.whl
Sign up to get free protection for your applications and to get access to all the features.
- enhydris_api_client/__init__.py +252 -0
- enhydris_cache/__init__.py +5 -0
- enhydris_cache/cli.py +150 -0
- enhydris_cache/enhydris_cache.py +69 -0
- evaporation/__init__.py +4 -0
- evaporation/cli.py +729 -0
- evaporation/evaporation.py +437 -0
- haggregate/__init__.py +5 -0
- haggregate/cli.py +91 -0
- haggregate/haggregate.py +155 -0
- haggregate/regularize.cp310-win_amd64.pyd +0 -0
- haggregate/regularize.pyx +193 -0
- hspatial/__init__.py +4 -0
- hspatial/cli.py +310 -0
- hspatial/hspatial.py +425 -0
- hspatial/test.py +27 -0
- htimeseries/__init__.py +2 -0
- htimeseries/htimeseries.py +574 -0
- htimeseries/timezone_utils.py +44 -0
- pthelma/__init__.py +0 -0
- pthelma/_version.py +16 -0
- pthelma-1.0.0.dist-info/LICENSE.rst +34 -0
- pthelma-1.0.0.dist-info/METADATA +55 -0
- pthelma-1.0.0.dist-info/RECORD +27 -0
- pthelma-1.0.0.dist-info/WHEEL +5 -0
- pthelma-1.0.0.dist-info/entry_points.txt +5 -0
- pthelma-1.0.0.dist-info/top_level.txt +7 -0
@@ -0,0 +1,574 @@
|
|
1
|
+
import csv
|
2
|
+
import datetime as dt
|
3
|
+
from configparser import ParsingError
|
4
|
+
from io import StringIO
|
5
|
+
|
6
|
+
import numpy as np
|
7
|
+
import pandas as pd
|
8
|
+
from pandas.tseries.frequencies import to_offset
|
9
|
+
from textbisect import text_bisect_left
|
10
|
+
|
11
|
+
from .timezone_utils import TzinfoFromString
|
12
|
+
|
13
|
+
|
14
|
+
class _BacktrackableFile(object):
|
15
|
+
def __init__(self, fp):
|
16
|
+
self.fp = fp
|
17
|
+
self.line_number = 0
|
18
|
+
self.next_line = None
|
19
|
+
|
20
|
+
def readline(self):
|
21
|
+
if self.next_line is None:
|
22
|
+
self.line_number += 1
|
23
|
+
result = self.fp.readline()
|
24
|
+
else:
|
25
|
+
result = self.next_line
|
26
|
+
self.next_line = None
|
27
|
+
return result
|
28
|
+
|
29
|
+
def backtrack(self, line):
|
30
|
+
self.next_line = line
|
31
|
+
|
32
|
+
def read(self, size=None):
|
33
|
+
return self.fp.read() if size is None else self.fp.read(size)
|
34
|
+
|
35
|
+
def __getattr__(self, name):
|
36
|
+
return getattr(self.fp, name)
|
37
|
+
|
38
|
+
|
39
|
+
class _FilePart(object):
|
40
|
+
"""A wrapper that views only a subset of the wrapped csv filelike object.
|
41
|
+
|
42
|
+
When it is created, three mandatory parameters are passed: a filelike object,
|
43
|
+
start_date and end_date, the latter as stirngs. This wrapper then acts as a filelike
|
44
|
+
object, which views the part of the wrapped object between start_date and end_date.
|
45
|
+
"""
|
46
|
+
|
47
|
+
def __init__(self, stream, start_date, end_date):
|
48
|
+
self.stream = stream
|
49
|
+
self.start_date = start_date
|
50
|
+
self.end_date = end_date
|
51
|
+
|
52
|
+
lo = stream.tell()
|
53
|
+
key = lambda x: x.split(",")[0] # NOQA
|
54
|
+
self.startpos = text_bisect_left(stream, start_date, lo=lo, key=key)
|
55
|
+
if self.stream.tell() < self.startpos:
|
56
|
+
self.stream.seek(self.startpos)
|
57
|
+
|
58
|
+
def readline(self, size=-1):
|
59
|
+
max_available_size = self.endpos + 1 - self.stream.tell()
|
60
|
+
size = min(size, max_available_size)
|
61
|
+
return self.stream.readline(size)
|
62
|
+
|
63
|
+
def __iter__(self):
|
64
|
+
return self
|
65
|
+
|
66
|
+
def __next__(self):
|
67
|
+
result = self.stream.__next__()
|
68
|
+
if result[:16] > self.end_date:
|
69
|
+
raise StopIteration
|
70
|
+
return result
|
71
|
+
|
72
|
+
def __getattr__(self, name):
|
73
|
+
return getattr(self.stream, name)
|
74
|
+
|
75
|
+
|
76
|
+
class MetadataWriter:
|
77
|
+
def __init__(self, f, htimeseries, version):
|
78
|
+
self.version = version
|
79
|
+
self.htimeseries = htimeseries
|
80
|
+
self.f = f
|
81
|
+
|
82
|
+
def write_meta(self):
|
83
|
+
if self.version == 2:
|
84
|
+
self.f.write("Version=2\r\n")
|
85
|
+
self.write_simple("unit")
|
86
|
+
self.write_count()
|
87
|
+
self.write_simple("title")
|
88
|
+
self.write_comment()
|
89
|
+
self.write_timezone()
|
90
|
+
self.write_time_step()
|
91
|
+
self.write_simple("interval_type")
|
92
|
+
self.write_simple("variable")
|
93
|
+
self.write_simple("precision")
|
94
|
+
self.write_location()
|
95
|
+
self.write_altitude()
|
96
|
+
|
97
|
+
def write_simple(self, parm):
|
98
|
+
value = getattr(self.htimeseries, parm, None)
|
99
|
+
if value is not None:
|
100
|
+
self.f.write("{}={}\r\n".format(parm.capitalize(), value))
|
101
|
+
|
102
|
+
def write_count(self):
|
103
|
+
self.f.write("Count={}\r\n".format(len(self.htimeseries.data)))
|
104
|
+
|
105
|
+
def write_comment(self):
|
106
|
+
if hasattr(self.htimeseries, "comment"):
|
107
|
+
for line in self.htimeseries.comment.splitlines():
|
108
|
+
self.f.write("Comment={}\r\n".format(line))
|
109
|
+
|
110
|
+
def write_timezone(self):
|
111
|
+
offset = self.htimeseries.data.index.tz.utcoffset(None)
|
112
|
+
sign = "-+"[offset >= dt.timedelta(0)]
|
113
|
+
offset = abs(offset)
|
114
|
+
hours = offset.seconds // 3600
|
115
|
+
minutes = offset.seconds % 3600 // 60
|
116
|
+
self.f.write(f"Timezone={sign}{hours:02}{minutes:02}\r\n")
|
117
|
+
|
118
|
+
def write_location(self):
|
119
|
+
if self.version <= 2 or not getattr(self.htimeseries, "location", None):
|
120
|
+
return
|
121
|
+
self.f.write(
|
122
|
+
"Location={:.6f} {:.6f} {}\r\n".format(
|
123
|
+
*[
|
124
|
+
self.htimeseries.location[x]
|
125
|
+
for x in ["abscissa", "ordinate", "srid"]
|
126
|
+
]
|
127
|
+
)
|
128
|
+
)
|
129
|
+
|
130
|
+
def write_altitude(self):
|
131
|
+
no_altitude = (
|
132
|
+
(self.version <= 2)
|
133
|
+
or not getattr(self.htimeseries, "location", None)
|
134
|
+
or (self.htimeseries.location.get("altitude") is None)
|
135
|
+
)
|
136
|
+
if no_altitude:
|
137
|
+
return
|
138
|
+
altitude = self.htimeseries.location["altitude"]
|
139
|
+
asrid = (
|
140
|
+
self.htimeseries.location["asrid"]
|
141
|
+
if "asrid" in self.htimeseries.location
|
142
|
+
else None
|
143
|
+
)
|
144
|
+
fmt = (
|
145
|
+
"Altitude={altitude:.2f} {asrid}\r\n"
|
146
|
+
if asrid
|
147
|
+
else "Altitude={altitude:.2f}\r\n"
|
148
|
+
)
|
149
|
+
self.f.write(fmt.format(altitude=altitude, asrid=asrid))
|
150
|
+
|
151
|
+
def write_time_step(self):
|
152
|
+
if getattr(self.htimeseries, "time_step", ""):
|
153
|
+
self._write_nonempty_time_step()
|
154
|
+
|
155
|
+
def _write_nonempty_time_step(self):
|
156
|
+
if self.version is None or self.version >= 5:
|
157
|
+
self.f.write("Time_step={}\r\n".format(self.htimeseries.time_step))
|
158
|
+
else:
|
159
|
+
self._write_old_time_step()
|
160
|
+
|
161
|
+
def _write_old_time_step(self):
|
162
|
+
try:
|
163
|
+
old_time_step = self._get_old_time_step_in_minutes()
|
164
|
+
except ValueError:
|
165
|
+
old_time_step = self._get_old_time_step_in_months()
|
166
|
+
self.f.write("Time_step={}\r\n".format(old_time_step))
|
167
|
+
|
168
|
+
def _get_old_time_step_in_minutes(self):
|
169
|
+
td = pd.to_timedelta(to_offset(self.htimeseries.time_step))
|
170
|
+
return str(int(td.total_seconds() / 60)) + ",0"
|
171
|
+
|
172
|
+
def _get_old_time_step_in_months(self):
|
173
|
+
time_step = self.htimeseries.time_step
|
174
|
+
try:
|
175
|
+
value, unit = self._split_time_step_string(time_step)
|
176
|
+
value = value or 1
|
177
|
+
if unit in ("M", "ME"):
|
178
|
+
return "0," + str(int(value))
|
179
|
+
elif unit in ("A", "Y", "YE"):
|
180
|
+
return "0," + str(12 * int(value))
|
181
|
+
except (IndexError, ValueError):
|
182
|
+
pass
|
183
|
+
raise ValueError('Cannot format time step "{}"'.format(time_step))
|
184
|
+
|
185
|
+
def _split_time_step_string(self, time_step_string):
|
186
|
+
value = ""
|
187
|
+
for i, char in enumerate(time_step_string):
|
188
|
+
if not char.isdigit():
|
189
|
+
return value, time_step_string[i:]
|
190
|
+
value += char
|
191
|
+
|
192
|
+
|
193
|
+
class MetadataReader:
|
194
|
+
def __init__(self, f):
|
195
|
+
f = _BacktrackableFile(f)
|
196
|
+
|
197
|
+
# Check if file contains headers
|
198
|
+
first_line = f.readline()
|
199
|
+
f.backtrack(first_line)
|
200
|
+
if isinstance(first_line, bytes):
|
201
|
+
first_line = first_line.decode("utf-8-sig")
|
202
|
+
has_headers = not first_line[0].isdigit()
|
203
|
+
|
204
|
+
# Read file, with its headers if needed
|
205
|
+
self.meta = {}
|
206
|
+
if has_headers:
|
207
|
+
self.read_meta(f)
|
208
|
+
|
209
|
+
def read_meta(self, f):
|
210
|
+
"""Read the headers of a file in file format and place them in the
|
211
|
+
self.meta dictionary.
|
212
|
+
"""
|
213
|
+
if not isinstance(f, _BacktrackableFile):
|
214
|
+
f = _BacktrackableFile(f)
|
215
|
+
|
216
|
+
try:
|
217
|
+
(name, value) = self.read_meta_line(f)
|
218
|
+
while name:
|
219
|
+
method_name = "get_{}".format(name)
|
220
|
+
if hasattr(self, method_name):
|
221
|
+
method = getattr(self, method_name)
|
222
|
+
method(name, value)
|
223
|
+
name, value = self.read_meta_line(f)
|
224
|
+
if not name and not value:
|
225
|
+
break
|
226
|
+
except ParsingError as e:
|
227
|
+
e.args = e.args + (f.line_number,)
|
228
|
+
raise
|
229
|
+
|
230
|
+
def get_unit(self, name, value):
|
231
|
+
self.meta[name] = value
|
232
|
+
|
233
|
+
get_title = get_unit
|
234
|
+
get_variable = get_unit
|
235
|
+
|
236
|
+
def get_time_step(self, name, value):
|
237
|
+
if value and "," in value:
|
238
|
+
minutes, months = self.read_minutes_months(value)
|
239
|
+
self.meta[name] = self._time_step_from_minutes_months(minutes, months)
|
240
|
+
else:
|
241
|
+
self.meta[name] = value
|
242
|
+
|
243
|
+
def get_timezone(self, name, value):
|
244
|
+
self.meta["_timezone"] = value
|
245
|
+
|
246
|
+
def _time_step_from_minutes_months(self, minutes, months):
|
247
|
+
if minutes != 0 and months != 0:
|
248
|
+
raise ParsingError("Invalid time step")
|
249
|
+
elif minutes != 0:
|
250
|
+
return str(minutes) + "min"
|
251
|
+
else:
|
252
|
+
return str(months) + "M"
|
253
|
+
|
254
|
+
def get_interval_type(self, name, value):
|
255
|
+
value = value.lower()
|
256
|
+
if value not in ("sum", "average", "maximum", "minimum", "vector_average"):
|
257
|
+
raise ParsingError(("Invalid interval type"))
|
258
|
+
self.meta[name] = value
|
259
|
+
|
260
|
+
def get_precision(self, name, value):
|
261
|
+
try:
|
262
|
+
self.meta[name] = int(value)
|
263
|
+
except ValueError as e:
|
264
|
+
raise ParsingError(e.args)
|
265
|
+
|
266
|
+
def get_comment(self, name, value):
|
267
|
+
if "comment" in self.meta:
|
268
|
+
self.meta["comment"] += "\n"
|
269
|
+
else:
|
270
|
+
self.meta["comment"] = ""
|
271
|
+
self.meta["comment"] += value
|
272
|
+
|
273
|
+
def get_location(self, name, value):
|
274
|
+
self._ensure_location_attribute_exists()
|
275
|
+
try:
|
276
|
+
items = value.split()
|
277
|
+
self.meta["location"]["abscissa"] = float(items[0])
|
278
|
+
self.meta["location"]["ordinate"] = float(items[1])
|
279
|
+
self.meta["location"]["srid"] = int(items[2])
|
280
|
+
except (IndexError, ValueError):
|
281
|
+
raise ParsingError("Invalid location")
|
282
|
+
|
283
|
+
def _ensure_location_attribute_exists(self):
|
284
|
+
if "location" not in self.meta:
|
285
|
+
self.meta["location"] = {}
|
286
|
+
|
287
|
+
def get_altitude(self, name, value):
|
288
|
+
self._ensure_location_attribute_exists()
|
289
|
+
try:
|
290
|
+
items = value.split()
|
291
|
+
self.meta["location"]["altitude"] = float(items[0])
|
292
|
+
self.meta["location"]["asrid"] = int(items[1]) if len(items) > 1 else None
|
293
|
+
except (IndexError, ValueError):
|
294
|
+
raise ParsingError("Invalid altitude")
|
295
|
+
|
296
|
+
def read_minutes_months(self, s):
|
297
|
+
"""Return a (minutes, months) tuple after parsing a "M,N" string."""
|
298
|
+
try:
|
299
|
+
(minutes, months) = [int(x.strip()) for x in s.split(",")]
|
300
|
+
return minutes, months
|
301
|
+
except Exception:
|
302
|
+
raise ParsingError(('Value should be "minutes, months"'))
|
303
|
+
|
304
|
+
def read_meta_line(self, f):
|
305
|
+
"""Read one line from a file format header and return a (name, value)
|
306
|
+
tuple, where name is lowercased. Returns ('', '') if the next line is
|
307
|
+
blank. Raises ParsingError if next line in f is not a valid header
|
308
|
+
line."""
|
309
|
+
line = f.readline()
|
310
|
+
if isinstance(line, bytes):
|
311
|
+
line = line.decode("utf-8-sig")
|
312
|
+
name, value = "", ""
|
313
|
+
if line.isspace():
|
314
|
+
return (name, value)
|
315
|
+
if line.find("=") > 0:
|
316
|
+
name, value = line.split("=", 1)
|
317
|
+
name = name.rstrip().lower()
|
318
|
+
value = value.strip()
|
319
|
+
name = "" if any([c.isspace() for c in name]) else name
|
320
|
+
if not name:
|
321
|
+
raise ParsingError("Invalid file header line")
|
322
|
+
return (name, value)
|
323
|
+
|
324
|
+
|
325
|
+
class HTimeseries:
|
326
|
+
TEXT = "TEXT"
|
327
|
+
FILE = "FILE"
|
328
|
+
args = {
|
329
|
+
"format": None,
|
330
|
+
"start_date": None,
|
331
|
+
"end_date": None,
|
332
|
+
"default_tzinfo": None,
|
333
|
+
}
|
334
|
+
|
335
|
+
def __init__(self, data=None, **kwargs):
|
336
|
+
extra_parms = set(kwargs) - set(self.args)
|
337
|
+
if extra_parms:
|
338
|
+
raise TypeError(
|
339
|
+
"HTimeseries.__init__() got an unexpected keyword argument "
|
340
|
+
f"'{extra_parms.pop()}'"
|
341
|
+
)
|
342
|
+
for arg, default_value in self.args.items():
|
343
|
+
kwargs.setdefault(arg, default_value)
|
344
|
+
if data is None:
|
345
|
+
if not kwargs["default_tzinfo"]:
|
346
|
+
kwargs["default_tzinfo"] = dt.timezone.utc
|
347
|
+
self._read_filelike(StringIO(), **kwargs)
|
348
|
+
elif isinstance(data, pd.DataFrame):
|
349
|
+
self._check_dataframe(data)
|
350
|
+
self.data = data
|
351
|
+
else:
|
352
|
+
self._read_filelike(data, **kwargs)
|
353
|
+
|
354
|
+
def _check_dataframe(self, data):
|
355
|
+
if data.index.tz is None:
|
356
|
+
raise TypeError("data.index.tz must exist")
|
357
|
+
|
358
|
+
def _read_filelike(self, *args, **kwargs):
|
359
|
+
reader = TimeseriesStreamReader(*args, **kwargs)
|
360
|
+
self.__dict__.update(reader.get_metadata())
|
361
|
+
try:
|
362
|
+
tzinfo = TzinfoFromString(self._timezone)
|
363
|
+
except AttributeError:
|
364
|
+
tzinfo = kwargs["default_tzinfo"]
|
365
|
+
self.data = reader.get_data(tzinfo)
|
366
|
+
if self.data.size and (tzinfo is None):
|
367
|
+
raise TypeError(
|
368
|
+
"Cannot read filelike object without timezone or default_tzinfo "
|
369
|
+
"specified"
|
370
|
+
)
|
371
|
+
|
372
|
+
def write(self, f, format=TEXT, version=5):
|
373
|
+
writer = TimeseriesStreamWriter(self, f, format=format, version=version)
|
374
|
+
writer.write()
|
375
|
+
|
376
|
+
|
377
|
+
class TimeseriesStreamReader:
|
378
|
+
def __init__(self, f, **kwargs):
|
379
|
+
self.f = f
|
380
|
+
self.specified_format = kwargs["format"]
|
381
|
+
self.start_date = kwargs["start_date"]
|
382
|
+
self.end_date = kwargs["end_date"]
|
383
|
+
self.default_tzinfo = kwargs["default_tzinfo"]
|
384
|
+
|
385
|
+
def get_metadata(self):
|
386
|
+
if self.format == HTimeseries.FILE:
|
387
|
+
return MetadataReader(self.f).meta
|
388
|
+
else:
|
389
|
+
return {}
|
390
|
+
|
391
|
+
@property
|
392
|
+
def format(self):
|
393
|
+
if self.specified_format is None:
|
394
|
+
return self.autodetected_format
|
395
|
+
else:
|
396
|
+
return self.specified_format
|
397
|
+
|
398
|
+
@property
|
399
|
+
def autodetected_format(self):
|
400
|
+
if not hasattr(self, "_stored_autodetected_format"):
|
401
|
+
self._stored_autodetected_format = FormatAutoDetector(self.f).detect()
|
402
|
+
return self._stored_autodetected_format
|
403
|
+
|
404
|
+
def get_data(self, tzinfo):
|
405
|
+
return TimeseriesRecordsReader(
|
406
|
+
self.f, self.start_date, self.end_date, tzinfo=tzinfo
|
407
|
+
).read()
|
408
|
+
|
409
|
+
|
410
|
+
def _check_timeseries_index_has_no_duplicates(data, error_message_prefix):
|
411
|
+
duplicate_dates = data.index[data.index.duplicated()].tolist()
|
412
|
+
if duplicate_dates:
|
413
|
+
dates_str = ", ".join([str(x) for x in duplicate_dates])
|
414
|
+
raise ValueError(
|
415
|
+
f"{error_message_prefix}: the following timestamps appear more than "
|
416
|
+
f"once: {dates_str}"
|
417
|
+
)
|
418
|
+
|
419
|
+
|
420
|
+
class TimeseriesRecordsReader:
|
421
|
+
def __init__(self, f, start_date, end_date, tzinfo):
|
422
|
+
self.f = f
|
423
|
+
self.start_date = start_date
|
424
|
+
self.end_date = end_date
|
425
|
+
self.tzinfo = tzinfo
|
426
|
+
|
427
|
+
def read(self):
|
428
|
+
start_date, end_date = self._get_bounding_dates_as_strings()
|
429
|
+
f2 = _FilePart(self.f, start_date, end_date)
|
430
|
+
data = self._read_data_from_stream(f2)
|
431
|
+
self._check_there_are_no_duplicates(data)
|
432
|
+
return data
|
433
|
+
|
434
|
+
def _get_bounding_dates_as_strings(self):
|
435
|
+
start_date = "0001-01-01 00:00" if self.start_date is None else self.start_date
|
436
|
+
end_date = "9999-12-31 00:00" if self.end_date is None else self.end_date
|
437
|
+
if isinstance(start_date, dt.datetime):
|
438
|
+
start_date = start_date.strftime("%Y-%m-%d %H:%M")
|
439
|
+
if isinstance(end_date, dt.datetime):
|
440
|
+
end_date = end_date.strftime("%Y-%m-%d %H:%M")
|
441
|
+
return start_date, end_date
|
442
|
+
|
443
|
+
def _read_data_from_stream(self, f):
|
444
|
+
dates, values, flags = self._read_csv(f)
|
445
|
+
dates = self._localize_dates(dates)
|
446
|
+
result = pd.DataFrame(
|
447
|
+
{
|
448
|
+
"value": np.array(values, dtype=np.float64),
|
449
|
+
"flags": np.array(flags, dtype=str),
|
450
|
+
},
|
451
|
+
index=dates,
|
452
|
+
)
|
453
|
+
result.index.name = "date"
|
454
|
+
return result
|
455
|
+
|
456
|
+
def _localize_dates(self, dates):
|
457
|
+
try:
|
458
|
+
result = pd.to_datetime(dates)
|
459
|
+
except ValueError:
|
460
|
+
raise ValueError(
|
461
|
+
"Could not parse timestamps correctly. Maybe the CSV contains mixed "
|
462
|
+
"aware and naive timestamps."
|
463
|
+
)
|
464
|
+
if len(result) == 0 or (len(result) > 0 and result[0].tzinfo is None):
|
465
|
+
result = pd.to_datetime(dates).tz_localize(
|
466
|
+
self.tzinfo, ambiguous=len(dates) * [True]
|
467
|
+
)
|
468
|
+
return result
|
469
|
+
|
470
|
+
def _read_csv(self, f):
|
471
|
+
dates, values, flags = [], [], []
|
472
|
+
for row in csv.reader(f): # We don't use pd.read_csv() because it's much slower
|
473
|
+
if not len(row):
|
474
|
+
continue
|
475
|
+
dates.append(row[0])
|
476
|
+
if len(row) > 1 and row[1]:
|
477
|
+
values.append(row[1])
|
478
|
+
else:
|
479
|
+
values.append("NaN")
|
480
|
+
flags.append(row[2] if len(row) > 2 else "")
|
481
|
+
return dates, values, flags
|
482
|
+
|
483
|
+
def _check_there_are_no_duplicates(self, data):
|
484
|
+
_check_timeseries_index_has_no_duplicates(
|
485
|
+
data, error_message_prefix="Can't read time series"
|
486
|
+
)
|
487
|
+
|
488
|
+
|
489
|
+
class FormatAutoDetector:
|
490
|
+
def __init__(self, f):
|
491
|
+
self.f = f
|
492
|
+
|
493
|
+
def detect(self):
|
494
|
+
original_position = self.f.tell()
|
495
|
+
result = self._guess_format_from_first_nonempty_line()
|
496
|
+
self.f.seek(original_position)
|
497
|
+
return result
|
498
|
+
|
499
|
+
def _guess_format_from_first_nonempty_line(self):
|
500
|
+
line = self._get_first_nonempty_line()
|
501
|
+
if line and not line[0].isdigit():
|
502
|
+
return HTimeseries.FILE
|
503
|
+
else:
|
504
|
+
return HTimeseries.TEXT
|
505
|
+
|
506
|
+
def _get_first_nonempty_line(self):
|
507
|
+
for line in self.f:
|
508
|
+
if line.strip():
|
509
|
+
return line
|
510
|
+
return ""
|
511
|
+
|
512
|
+
|
513
|
+
class TimeseriesStreamWriter:
|
514
|
+
def __init__(self, htimeseries, f, *, format, version):
|
515
|
+
self.htimeseries = htimeseries
|
516
|
+
self.f = f
|
517
|
+
self.format = format
|
518
|
+
self.version = version
|
519
|
+
|
520
|
+
def write(self):
|
521
|
+
self._write_metadata()
|
522
|
+
self._write_records()
|
523
|
+
|
524
|
+
def _write_metadata(self):
|
525
|
+
if self.format == HTimeseries.FILE:
|
526
|
+
MetadataWriter(self.f, self.htimeseries, version=self.version).write_meta()
|
527
|
+
self.f.write("\r\n")
|
528
|
+
|
529
|
+
def _write_records(self):
|
530
|
+
TimeseriesRecordsWriter(self.htimeseries, self.f).write()
|
531
|
+
|
532
|
+
|
533
|
+
class TimeseriesRecordsWriter:
|
534
|
+
def __init__(self, htimeseries, f):
|
535
|
+
self.htimeseries = htimeseries
|
536
|
+
self.f = f
|
537
|
+
|
538
|
+
def write(self):
|
539
|
+
if self.htimeseries.data.empty:
|
540
|
+
return
|
541
|
+
self._check_there_are_no_duplicates()
|
542
|
+
self._setup_precision()
|
543
|
+
self._write_records()
|
544
|
+
|
545
|
+
def _check_there_are_no_duplicates(self):
|
546
|
+
_check_timeseries_index_has_no_duplicates(
|
547
|
+
self.htimeseries.data, error_message_prefix="Can't write time series"
|
548
|
+
)
|
549
|
+
|
550
|
+
def _setup_precision(self):
|
551
|
+
precision = getattr(self.htimeseries, "precision", None)
|
552
|
+
if precision is None:
|
553
|
+
self.float_format = "%f"
|
554
|
+
elif self.htimeseries.precision >= 0:
|
555
|
+
self.float_format = "%.{}f".format(self.htimeseries.precision)
|
556
|
+
else:
|
557
|
+
self.float_format = "%.0f"
|
558
|
+
self._prepare_records_for_negative_precision(precision)
|
559
|
+
|
560
|
+
def _prepare_records_for_negative_precision(self, precision):
|
561
|
+
assert precision < 0
|
562
|
+
datacol = self.htimeseries.data.columns[0]
|
563
|
+
m = 10 ** (-self.htimeseries.precision)
|
564
|
+
self.htimeseries.data[datacol] = np.rint(self.htimeseries.data[datacol] / m) * m
|
565
|
+
|
566
|
+
def _write_records(self):
|
567
|
+
self.htimeseries.data.to_csv(
|
568
|
+
self.f,
|
569
|
+
float_format=self.float_format,
|
570
|
+
header=False,
|
571
|
+
mode="w",
|
572
|
+
lineterminator="\r\n",
|
573
|
+
date_format="%Y-%m-%d %H:%M",
|
574
|
+
)
|
@@ -0,0 +1,44 @@
|
|
1
|
+
import datetime as dt
|
2
|
+
|
3
|
+
|
4
|
+
class TzinfoFromString(dt.tzinfo):
|
5
|
+
def __init__(self, string):
|
6
|
+
self.offset = None
|
7
|
+
self.name = ""
|
8
|
+
if not string:
|
9
|
+
return
|
10
|
+
|
11
|
+
# If string contains brackets, set tzname to whatever is before the
|
12
|
+
# brackets and retrieve the part inside the brackets.
|
13
|
+
i = string.find("(")
|
14
|
+
if i > 0:
|
15
|
+
self.name = string[:i].strip()
|
16
|
+
start = i + 1
|
17
|
+
s = string[start:]
|
18
|
+
i = s.find(")")
|
19
|
+
i = len(s) if i < 0 else i
|
20
|
+
s = s[:i]
|
21
|
+
|
22
|
+
# Remove any preceeding 'UTC' (as in "UTC+0200")
|
23
|
+
s = s[3:] if s.startswith("UTC") else s
|
24
|
+
|
25
|
+
# s should be in +0000 format
|
26
|
+
try:
|
27
|
+
if len(s) != 5:
|
28
|
+
raise ValueError()
|
29
|
+
sign = {"+": 1, "-": -1}[s[0]]
|
30
|
+
hours = int(s[1:3])
|
31
|
+
minutes = int(s[3:5])
|
32
|
+
except (ValueError, IndexError):
|
33
|
+
raise ValueError("Time zone {} is invalid".format(string))
|
34
|
+
|
35
|
+
self.offset = sign * dt.timedelta(hours=hours, minutes=minutes)
|
36
|
+
|
37
|
+
def utcoffset(self, adatetime):
|
38
|
+
return self.offset
|
39
|
+
|
40
|
+
def dst(self, adatetime):
|
41
|
+
return dt.timedelta(0)
|
42
|
+
|
43
|
+
def tzname(self, adatetime):
|
44
|
+
return self.name
|
pthelma/__init__.py
ADDED
File without changes
|
pthelma/_version.py
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# file generated by setuptools_scm
|
2
|
+
# don't change, don't track in version control
|
3
|
+
TYPE_CHECKING = False
|
4
|
+
if TYPE_CHECKING:
|
5
|
+
from typing import Tuple, Union
|
6
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
7
|
+
else:
|
8
|
+
VERSION_TUPLE = object
|
9
|
+
|
10
|
+
version: str
|
11
|
+
__version__: str
|
12
|
+
__version_tuple__: VERSION_TUPLE
|
13
|
+
version_tuple: VERSION_TUPLE
|
14
|
+
|
15
|
+
__version__ = version = '1.0.0'
|
16
|
+
__version_tuple__ = version_tuple = (1, 0, 0)
|
@@ -0,0 +1,34 @@
|
|
1
|
+
===================
|
2
|
+
License and credits
|
3
|
+
===================
|
4
|
+
|
5
|
+
| Copyright (C) 2013-2018 TEI of Epirus
|
6
|
+
| Copyright (C) 2014-2016 Antonis Christofides
|
7
|
+
| Copyright (C) 2019 University of Ioannina
|
8
|
+
| Copyright (C) 2018-2021 National Technical University of Athens
|
9
|
+
| Copyright (C) 2018-2021 Institute of Communications and Computer Systems
|
10
|
+
| Copyright (C) 2022-2024 IRMASYS P.C.
|
11
|
+
|
12
|
+
pthelma is free software: you can redistribute it and/or modify it
|
13
|
+
under the terms of the GNU General Public License as published by the
|
14
|
+
Free Software Foundation, either version 3 of the License, or (at your
|
15
|
+
option) any later version.
|
16
|
+
|
17
|
+
This program is distributed in the hope that it will be useful, but
|
18
|
+
WITHOUT ANY WARRANTY; without even the implied warranty of
|
19
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
20
|
+
General Public License for more details.
|
21
|
+
|
22
|
+
You should have received a copy of the GNU General Public License along
|
23
|
+
with this program. If not, see <http://www.gnu.org/licenses/>.
|
24
|
+
|
25
|
+
Pthelma was funded by several organizations:
|
26
|
+
|
27
|
+
* In 2013-2018 by the `TEI of Epirus`_ as part of the IRMA_ project.
|
28
|
+
* In 2018-2021 by NTUA_ and ICCS_ as part of the OpenHi_ project.
|
29
|
+
|
30
|
+
.. _ntua: http://www.ntua.gr/
|
31
|
+
.. _tei of epirus: http://www.teiep.gr/en/
|
32
|
+
.. _irma: http://www.irrigation-management.eu/
|
33
|
+
.. _iccs: https://www.iccs.gr
|
34
|
+
.. _openhi: https://openhi.net
|