M test/test_api.py +58 -0
@@ 242,6 242,64 @@ 2020-01-04 16:00+00:00, 800.0
pd.testing.assert_series_equal(computed_ts, expected_ts, check_names=False)
+def test_inferred_freq(tsx):
+ tsx.delete('infer_freq')
+
+ ts = pd.Series(
+ [1, 2, 3],
+ index=pd.date_range(
+ pd.Timestamp('2023-1-1'),
+ freq='D',
+ periods=3
+ )
+ )
+ tsx.update(
+ 'infer_freq',
+ ts,
+ 'Babar',
+ insertion_date=pd.Timestamp('2023-5-1', tz='utc')
+ )
+
+ assert tsx.inferred_freq('infer_freq') == (
+ pd.Timedelta(days=1),
+ 1
+ )
+ assert tsx.inferred_freq(
+ 'infer_freq',
+ from_value_date=pd.Timestamp('2023-1-3')
+ ) is None
+ assert tsx.inferred_freq('no-such-series') is None
+
+ # frequence change
+ ts = pd.Series(
+ [1, 2, 3, 4, 5],
+ index=pd.date_range(
+ pd.Timestamp('2023-1-3T01:00:00'),
+ freq='H',
+ periods=5
+ )
+ )
+ tsx.update(
+ 'infer_freq',
+ ts,
+ 'Babar',
+ insertion_date=pd.Timestamp('2023-5-2', tz='utc')
+ )
+
+ assert tsx.inferred_freq('infer_freq') == (
+ pd.Timedelta(hours=1),
+ 0.7142857142857143
+ )
+ freq = tsx.inferred_freq(
+ 'infer_freq',
+ revision_date=pd.Timestamp('2023-5-1')
+ )
+ assert freq == (
+ pd.Timedelta(days=1),
+ 1
+ )
+
+
def test_log(tsx):
for name in ('log-me',):
tsx.delete(name)
M tshistory/api.py +22 -0
@@ 624,6 624,28 @@ class mainsource:
return self.othersources.interval(name)
return ival
+ def inferred_freq(self,
+ name: str,
+ revision_date: Optional[pd.Timestamp]=None,
+ from_value_date: Optional[pd.Timestamp]=None,
+ to_value_date: Optional[pd.Timestamp]=None
+ ) -> Optional[Tuple[pd.Timedelta, float]]:
+ """Return a tuple of timedelta, float (between 0 and 1).
+
+ The timedelta represents the period (or 'freq' in pandas
+ parlance) and the number the quality of the period, which may vary
+ because of the irregularity of the series.
+
+ """
+ with self.engine.begin() as cn:
+ return self.tsh.infer_freq(
+ cn,
+ name,
+ revision_date,
+ from_value_date,
+ to_value_date
+ )
+
def metadata(self,
name: str,
all: bool=None) -> Optional[Dict[str, Any]]:
M tshistory/http/client.py +29 -0
@@ 13,6 13,7 @@ from tshistory.util import (
logme,
pack_group,
pack_series,
+ parse_delta,
series_metadata,
ts,
tzaware_serie,
@@ 239,6 240,34 @@ class httpclient:
return res
@unwraperror
+ def inferred_freq(self, name,
+ revision_date=None,
+ from_value_date=None,
+ to_value_date=None):
+ args = {
+ 'name': name
+ }
+ if revision_date:
+ args['revision_date'] = strft(revision_date)
+ if from_value_date:
+ args['from_value_date'] = strft(from_value_date)
+ if to_value_date:
+ args['to_value_date'] = strft(to_value_date)
+
+ res = self.session.get(
+ f'{self.uri}/series/freq', params=args
+ )
+ if res.status_code == 404:
+ return None
+ if res.status_code == 200:
+ data = res.json()
+ if data is None:
+ return
+ ifreq = data['inferred_freq']
+ return parse_delta(ifreq[0]), float(ifreq[1])
+ return res
+
+ @unwraperror
def get(self, name,
revision_date=None,
from_value_date=None,
M tshistory/http/server.py +40 -0
@@ 107,6 107,18 @@ put_metadata.add_argument(
help='set new metadata for a series'
)
+inferred_freq = base.copy()
+inferred_freq.add_argument(
+ 'revision_date', type=utcdt, default=None,
+)
+inferred_freq.add_argument(
+ 'from_value_date', type=utcdt, default=None
+)
+inferred_freq.add_argument(
+ 'to_value_date', type=utcdt, default=None
+)
+
+
insertion_dates = base.copy()
insertion_dates.add_argument(
'from_insertion_date', type=utcdt, default=None
@@ 516,6 528,34 @@ class httpapi:
return '', 200
+ @nss.route('/freq')
+ class timeseries_freq(Resource):
+
+ @api.expect(inferred_freq)
+ @onerror
+ def get(self):
+ args = inferred_freq.parse_args()
+ if not tsa.exists(args.name):
+ api.abort(404, f'`{args.name}` does not exists')
+
+ freq_qa = tsa.inferred_freq(
+ args.name,
+ args.revision_date,
+ args.from_value_date,
+ args.to_value_date
+ )
+
+ if freq_qa is None:
+ return make_response('null')
+
+ freq = util.delta_isoformat(freq_qa[0])
+ response = make_response(
+ {
+ 'inferred_freq': (freq, str(freq_qa[1]))
+ }
+ )
+ response.headers['Content-Type'] = 'text/json'
+ return response
@nss.route('/state')
class timeseries_state(Resource):
M tshistory/testutil.py +5 -0
@@ 272,6 272,11 @@ class with_http_bridge:
)
resp.add_callback(
+ responses.GET, uri + '/series/freq',
+ callback=partial(read_request_bridge, wsgitester)
+ )
+
+ resp.add_callback(
responses.GET, uri + '/series/log',
callback=partial(read_request_bridge, wsgitester)
)
M tshistory/util.py +19 -0
@@ 3,6 3,7 @@ import os
import math
import struct
import json
+import re
from array import array
from collections import defaultdict
import logging
@@ 329,6 330,24 @@ def infer_freq(ts):
return freq, conform_intervals / len(deltas)
+# timedelta (de)serialisation
+
+def delta_isoformat(td):
+ return f'P{td.days}DT0H0M{td.seconds}S'
+
+
+_DELTA = re.compile('P(.*)DT(.*)H(.*)M(.*)S')
+def parse_delta(td):
+ match = _DELTA.match(td)
+ if not match:
+ raise Exception(f'unparseable time delta `{td}`')
+ days, hours, minutes, seconds = match.groups()
+ return pd.Timedelta(
+ days=int(days), hours=int(hours),
+ minutes=int(minutes), seconds=int(seconds)
+ )
+
+
# metadata
def series_metadata(ts):