Skip to content

Commit 5f9d1a1

Browse files
committed
Add timeseries operations (Get, Batch, Append, Remove)
1 parent 839d791 commit 5f9d1a1

7 files changed

Lines changed: 243 additions & 6 deletions

File tree

pyravendb/connection/requests_executor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def __init__(self, database_name, certificate, conventions=None, **kwargs):
4141
self._last_known_urls = None
4242

4343
self.headers = {"Accept": "application/json",
44-
"Raven-Client-Version": "4.0.0.0"}
44+
"Raven-Client-Version": "5.0.0.0"}
4545

4646
self.update_topology_lock = Lock()
4747
self.update_timer_lock = Lock()
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
from typing import Optional, Iterable, List, Generator
2+
from pyravendb.commands.raven_commands import *
3+
from pyravendb.tools.utils import Utils
4+
from .operations import Operation
5+
from datetime import datetime
6+
7+
import sys
8+
9+
10+
class TimeSeriesRange:
11+
def __init__(self, name, from_date: Optional[datetime] = None, to_date: Optional[datetime] = None):
12+
if not name:
13+
raise ValueError("TimeSeriesRange must include name")
14+
self.name = name
15+
self.from_date = from_date
16+
self.to_date = to_date
17+
18+
19+
class GetTimeSeriesOperation(Operation):
20+
def __init__(self, document_id: str, ranges: Iterable[TimeSeriesRange] or TimeSeriesRange = None,
21+
start: int = 0, page_size: int = sys.maxsize):
22+
"""
23+
Build the time series get operations construct with ranges or from_date and to_date.
24+
"""
25+
super().__init__()
26+
if isinstance(ranges, TimeSeriesRange):
27+
ranges = [ranges]
28+
29+
self._ranges = ranges
30+
self._document_id = document_id
31+
self._start = start
32+
self._page_size = page_size
33+
34+
def get_command(self, store, conventions, cache=None):
35+
return self._GetTimeSeriesCommand(self._document_id, self._ranges, self._start, self._page_size)
36+
37+
class _GetTimeSeriesCommand(RavenCommand):
38+
def __init__(self, document_id: str, ranges: Iterable[TimeSeriesRange],
39+
start: int = 0, page_size: int = sys.maxsize):
40+
super().__init__(method="GET", is_read_request=True)
41+
42+
if not document_id:
43+
raise ValueError("Invalid document_id")
44+
45+
self._document_id = document_id
46+
self._ranges = ranges
47+
self._start = start
48+
self._page_size = page_size
49+
50+
def create_request(self, server_node):
51+
self.url = (f"{server_node.url}/databases/{server_node.database}"
52+
f"/timeseries?id={Utils.quote_key(self._document_id)}"
53+
f"{f'&start={self._start}' if self._start > 0 else ''}"
54+
f"{f'&pageSize={self._page_size}' if self._page_size < sys.maxsize else ''}")
55+
56+
if self._ranges is not None:
57+
for range_ in self._ranges:
58+
self.url += (f"&name={Utils.quote_key(range_.name)}"
59+
f"&from={Utils.datetime_to_string(range_.from_date)}"
60+
f"&to={Utils.datetime_to_string(range_.to_date)}")
61+
62+
def set_response(self, response):
63+
if response is None:
64+
return None
65+
try:
66+
response = response.json()
67+
if "Error" in response:
68+
raise exceptions.ErrorResponseException(response["Error"])
69+
except ValueError as e:
70+
raise exceptions.ErrorResponseException(e)
71+
72+
return response
73+
74+
75+
class TimeSeriesOperation:
76+
77+
def __init__(self, name: str, appends: Optional[List["RemoveOperation"]] = None,
78+
removals: [List["RemoveOperation"]] = None):
79+
self.name = name
80+
self.appends = [] if not appends else Utils.sort_iterable(appends, key=lambda ao: ao.timestamp.timestamp())
81+
self.removals = [] if removals is None else removals
82+
83+
def append(self, timestamp: datetime, values: List[float] or float, tag: Optional[str] = None):
84+
self.appends.append(TimeSeriesOperation.AppendOperation(timestamp, values, tag))
85+
86+
def remove(self, from_date: datetime, to_date: datetime):
87+
self.removals.append(TimeSeriesOperation.RemoveOperation(from_date, to_date))
88+
89+
def to_json(self):
90+
if self.appends and isinstance(self.appends, Generator):
91+
self.appends = next(self.appends)
92+
return {"Name": self.name, "Appends": [a.to_json() for a in self.appends] if self.appends else self.appends,
93+
"Removals": [r.to_json() for r in self.removals] if self.removals else self.removals}
94+
95+
class AppendOperation:
96+
def __init__(self, timestamp: datetime, values: List[float] or float, tag: Optional[str] = None):
97+
if not timestamp:
98+
raise ValueError("Missing timestamp property")
99+
self.timestamp = timestamp
100+
if values is None:
101+
raise ValueError("Missing values property")
102+
try:
103+
if not isinstance(values, list):
104+
values = [float(values)]
105+
else:
106+
values = [float(value) for value in values]
107+
except ValueError:
108+
raise (f"One or more values from values are not numbers %s" % values)
109+
self.values = values
110+
self.tag = tag
111+
112+
def to_json(self):
113+
_dict = {"Timestamp": Utils.datetime_to_string(self.timestamp), "Values": self.values}
114+
if self.tag:
115+
_dict["Tag"] = self.tag
116+
return _dict
117+
118+
class RemoveOperation:
119+
def __init__(self, from_date: datetime, to_date: datetime):
120+
if not from_date:
121+
raise ValueError("Missing from_date property")
122+
if not to_date:
123+
raise ValueError("Missing to_date property")
124+
125+
self.from_date = from_date
126+
self.to_date = to_date
127+
128+
def to_json(self):
129+
return {"From": self.from_date, "To": self.to_date}
130+
131+
132+
class TimeSeriesBatchOperation(Operation):
133+
def __init__(self, document_id: str, operation: TimeSeriesOperation):
134+
135+
super().__init__()
136+
self._document_id = document_id
137+
self.__operation = operation
138+
139+
def get_command(self, store, conventions, cache=None):
140+
return self._TimeSeriesBatchCommand(self._document_id, self.__operation)
141+
142+
class _TimeSeriesBatchCommand(RavenCommand):
143+
def __init__(self, document_id: str, operation: TimeSeriesOperation):
144+
super().__init__(method="POST")
145+
146+
if not document_id:
147+
raise ValueError("Invalid document_id")
148+
149+
self._document_id = document_id
150+
self._operation = operation
151+
152+
def create_request(self, server_node):
153+
self.url = (f"{server_node.url}/databases/{server_node.database}"
154+
f"/timeseries?id={Utils.quote_key(self._document_id)}")
155+
156+
self.data = self._operation.to_json()
157+
158+
def set_response(self, response):
159+
if response and response.status_code != 204:
160+
response = response.json()
161+
raise exceptions.ErrorResponseException(response["Error"])

pyravendb/store/document_session.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from pyravendb.commands.commands_data import PutAttachmentCommandData, DeleteAttachmentCommandData, DeleteCommandData
77
from pyravendb.tools.utils import Utils
88
from pyravendb.data.operation import AttachmentType
9+
from .session_timeseries import TimeSeries
910
from copy import deepcopy
1011

1112

@@ -32,6 +33,7 @@ def __init__(self, database, document_store, requests_executor, session_id, **kw
3233
self._included_documents_by_id = {}
3334
self._deleted_entities = set()
3435
self._documents_by_entity = {}
36+
self._time_series_by_document_id = {}
3537
self._known_missing_ids = set()
3638
self.id_value = None
3739
self._defer_commands = set()
@@ -453,6 +455,14 @@ def clear(self):
453455
self.session.included_documents_by_id.clear()
454456
self.session.known_missing_ids.clear()
455457

458+
# TODO: complete time_series in the session
459+
def time_series_for(self, entity_or_document_id, name):
460+
"""
461+
Get A time series object associated with the document
462+
"""
463+
raise NotImplemented("Time-series from the session is not implemented")
464+
# return TimeSeries(self, entity_or_document_id, name)
465+
456466

457467
class _Attachment:
458468
def __init__(self, session):
@@ -471,7 +481,7 @@ def _command_exists(self, document_id):
471481
def throw_not_in_session(self, entity):
472482
raise ValueError(
473483
repr(entity) + " is not associated with the session, cannot add attachment to it. "
474-
"Use documentId instead or track the entity in the session.")
484+
"Use document Id instead or track the entity in the session.")
475485

476486
def store(self, entity_or_document_id, name, stream, content_type=None, change_vector=None):
477487
if not isinstance(entity_or_document_id, str):
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from datetime import datetime
2+
import sys
3+
4+
5+
class TimeSeries:
6+
7+
@staticmethod
8+
def throw_not_in_session(entity):
9+
raise ValueError(
10+
repr(entity) + " is not associated with the session, cannot add time-series to it. "
11+
"Use document Id instead or track the entity in the session.")
12+
13+
def __init__(self, session, entity_or_document_id, name):
14+
self._session = session
15+
if not isinstance(entity_or_document_id, str):
16+
entity = self._session.documents_by_entity.get(entity_or_document_id, None)
17+
if not entity:
18+
self.throw_not_in_session(entity_or_document_id)
19+
entity_or_document_id = entity["metadata"]["@id"]
20+
21+
if not entity_or_document_id:
22+
raise ValueError(entity_or_document_id)
23+
if not name:
24+
raise ValueError(name)
25+
26+
def get(self, from_date: datetime, to_date: datetime, start: int = 0, page_size: int = sys.maxsize):
27+
raise NotImplemented("Time-series from the session is not implemented")

pyravendb/tools/utils.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ def dict_to_string(dictionary):
199199

200200
@staticmethod
201201
def datetime_to_string(datetime_obj):
202-
return datetime_obj.strftime("%Y-%m-%dT%H:%M:%S.%f0")
202+
return datetime_obj.strftime("%Y-%m-%dT%H:%M:%S.%f0") if datetime_obj else ''
203203

204204
@staticmethod
205205
def start_a_timer(interval, function, args=None, name=None, daemon=False):
@@ -359,3 +359,14 @@ def contains_any(text, any_of):
359359
if c in text:
360360
return True
361361
return False
362+
363+
@staticmethod
364+
def sort_iterable(iterable, key=None):
365+
"""
366+
This function will take an iterable and try to sort it by the key
367+
if the key is not given will use the sorted default one.
368+
return a generator
369+
"""
370+
if not iterable:
371+
return iterable
372+
yield sorted(iterable, key=key)

pyravendb/tryouts/program.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,31 @@
11
from pyravendb.store.document_store import DocumentStore
2+
from pyravendb.raven_operations.timeseries_operations import \
3+
GetTimeSeriesOperation, TimeSeriesRange, TimeSeriesBatchOperation, TimeSeriesOperation
4+
from datetime import datetime, timedelta
25

36
if __name__ == "__main__":
4-
pass
7+
with DocumentStore(urls=["http://localhost:8080"], database="NorthWind") as store:
8+
store.initialize()
9+
# Add time_series to user/1-A document
10+
time_series_operation = TimeSeriesOperation(name="Heartrate")
11+
time_series_operation.append(datetime.now(), 73, tag="heart/rates")
12+
time_series_operation.append(datetime.now() + timedelta(minutes=5), 78, tag="heart/rates")
13+
14+
time_series_batch_operation = TimeSeriesBatchOperation(document_id="user/1-A", operation=time_series_operation)
15+
store.operations.send(time_series_batch_operation)
16+
17+
# Fetch all time_series from the document
18+
tsr = TimeSeriesRange("Heartrate", datetime.now() - timedelta(days=2), datetime.now() + timedelta(days=2))
19+
time_series = store.operations.send(GetTimeSeriesOperation("user/1-A", ranges=tsr))
20+
print(len(time_series['Values']["Heartrate"][0]['Entries']))
21+
22+
# Remove all time_series from the document
23+
time_series_operation = TimeSeriesOperation(name="Heartrate")
24+
time_series_operation.remove(datetime.now() - timedelta(days=2), datetime.now() + timedelta(days=2))
25+
26+
time_series_batch_operation = TimeSeriesBatchOperation(document_id="user/1-A", operation=time_series_operation)
27+
store.operations.send(time_series_batch_operation)
28+
29+
tsr = TimeSeriesRange("Heartrate", datetime.now() - timedelta(days=2), datetime.now() + timedelta(days=2))
30+
time_series = store.operations.send(GetTimeSeriesOperation("user/1-A", ranges=tsr))
31+
print(len(time_series['Values']["Heartrate"][0]['Entries']))

setup.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33
setup(
44
name='pyravendb',
55
packages=find_packages(),
6-
version='4.0.6.2',
6+
version='5.0.0.0',
77
long_description=open("README.rst").read(),
8-
description='This is the official python client for RavenDB v4.0 document database',
8+
description='This is the official python client for RavenDB v5.0 document database',
99
author='RavenDB',
1010
author_email='support@ravendb.net',
1111
url='https://github.com/ravendb/RavenDB-Python-Client',
1212
license='MIT',
1313
keywords='pyravendb',
14+
python_requires='~=3.5',
1415
install_requires=
1516
[
1617
'requests >= 2.18.4',

0 commit comments

Comments
 (0)