Skip to content

Commit f51e2b2

Browse files
authored
Merge pull request #171 from poissoncorp/v5.2
RDBC-679/680 Python - counters
2 parents dd3fd60 + d7b5ab6 commit f51e2b2

File tree

17 files changed

+2080
-184
lines changed

17 files changed

+2080
-184
lines changed

ravendb/documents/commands/batches.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import requests
1010

11-
from ravendb.documents.operations.counters.operation import (
11+
from ravendb.documents.operations.counters import (
1212
CounterOperation,
1313
DocumentCountersOperation,
1414
CounterOperationType,
@@ -492,7 +492,7 @@ def __init__(
492492

493493
super().__init__(key=document_id, command_type=CommandType.COUNTERS, change_vector=change_vector)
494494
self._from_etl = from_etl
495-
self._counters = DocumentCountersOperation(self.key, *counter_operations)
495+
self._counters = DocumentCountersOperation(self.key, counter_operations)
496496

497497
def has_increment(self, counter_name):
498498
self.has_operation_of_type(CounterOperationType.INCREMENT, counter_name)
@@ -517,7 +517,7 @@ def serialize(self, conventions: DocumentConventions) -> Dict:
517517
json_dict = {
518518
"Id": self.key,
519519
"Counters": self.counters.to_json(),
520-
"Type": self.type,
520+
"Type": self.command_type,
521521
}
522522
if self._from_etl:
523523
json_dict["FromEtl"] = self._from_etl

ravendb/documents/operations/batch.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,10 @@ def get_command_type(obj_node: dict) -> CommandType:
116116
self.__handle_attachment_move(batch_result)
117117
elif command_type == CommandType.ATTACHMENT_COPY:
118118
self.__handle_attachment_copy(batch_result)
119-
elif (
120-
command_type == CommandType.COMPARE_EXCHANGE_PUT
121-
or CommandType.COMPARE_EXCHANGE_DELETE
122-
or CommandType.FORCE_REVISION_CREATION
119+
elif command_type in (
120+
CommandType.COMPARE_EXCHANGE_PUT,
121+
CommandType.COMPARE_EXCHANGE_DELETE,
122+
CommandType.FORCE_REVISION_CREATION,
123123
):
124124
pass
125125
elif command_type == CommandType.COUNTERS:
@@ -301,31 +301,30 @@ def __handle_metadata_modifications(
301301

302302
def __handle_counters(self, batch_result: dict) -> None:
303303
doc_id = self.__get_string_field(batch_result, CommandType.COUNTERS, "Id")
304-
counters_detail: dict = batch_result.get("CountersDetail")
304+
counters_detail: dict = batch_result.get("CountersDetail", None)
305305
if counters_detail is None:
306306
self.__throw_missing_field(CommandType.COUNTERS, "CountersDetail")
307307

308-
counters = counters_detail.get("Counters")
308+
counters = counters_detail.get("Counters", None)
309309
if counters is None:
310310
self.__throw_missing_field(CommandType.COUNTERS, "Counters")
311311

312-
cache = self.__session.counters_by_doc_id[doc_id]
312+
cache = self.__session.counters_by_doc_id.get(doc_id, None)
313313
if cache is None:
314314
cache = [False, CaseInsensitiveDict()]
315315
self.__session.counters_by_doc_id[doc_id] = cache
316316

317317
change_vector = self.__get_string_field(batch_result, CommandType.COUNTERS, "DocumentChangeVector", False)
318318
if change_vector is not None:
319-
document_info = self.__session._documents_by_id.get(doc_id)
319+
document_info = self.__session._documents_by_id.get(doc_id, None)
320320
if document_info is not None:
321321
document_info.change_vector = change_vector
322322

323323
for counter in counters:
324-
counter: dict
325-
name = counter.get("CounterName")
326-
value = counter.get("TotalValue")
324+
name = counter.get("CounterName", None)
325+
value = counter.get("TotalValue", None)
327326

328-
if not name and not value:
327+
if name is not None and value is not None:
329328
cache[1][name] = value
330329

331330
def __handle_attachment_put(self, batch_result: dict) -> None:
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
from __future__ import annotations
2+
import enum
3+
import json
4+
from typing import Optional, List, Dict, TYPE_CHECKING, Union, Tuple
5+
6+
import requests
7+
8+
from ravendb.documents.operations.definitions import IOperation
9+
from ravendb.http.http_cache import HttpCache
10+
from ravendb.http.raven_command import RavenCommand, ServerNode
11+
from ravendb.tools.utils import Utils
12+
13+
if TYPE_CHECKING:
14+
from ravendb.documents.store.definition import DocumentStore
15+
from ravendb.documents.conventions import DocumentConventions
16+
17+
18+
class CounterOperationType(enum.Enum):
19+
NONE = "None"
20+
INCREMENT = "Increment"
21+
DELETE = "Delete"
22+
GET = "Get"
23+
PUT = "Put"
24+
25+
26+
class CounterOperation:
27+
def __init__(
28+
self,
29+
counter_name: str,
30+
counter_operation_type: CounterOperationType,
31+
delta: Optional[int] = None,
32+
):
33+
if not counter_name:
34+
raise ValueError("Missing counter_name property")
35+
if not counter_operation_type:
36+
raise ValueError(f"Missing counter_operation_type property in counter {counter_name}")
37+
if delta is None and counter_operation_type == CounterOperationType.INCREMENT:
38+
raise ValueError(f"Missing delta property in counter {counter_name} of type {counter_operation_type}")
39+
self.counter_name = counter_name
40+
self.delta = delta
41+
self.counter_operation_type = counter_operation_type
42+
self._change_vector = None
43+
self._document_id = None
44+
45+
@classmethod
46+
def create(cls, counter_name: str, counter_operation_type: CounterOperationType, delta: Optional[int] = None):
47+
return cls(counter_name, counter_operation_type, delta)
48+
49+
def to_json(self):
50+
return {
51+
"Type": self.counter_operation_type.value,
52+
"CounterName": self.counter_name,
53+
"Delta": self.delta,
54+
}
55+
56+
57+
class DocumentCountersOperation:
58+
def __init__(self, document_id: str, operations: List[CounterOperation]):
59+
if not document_id:
60+
raise ValueError("Missing document_id property")
61+
62+
self._document_id = document_id
63+
self._operations = operations
64+
65+
def add_operations(self, operation: CounterOperation):
66+
if self._operations is None:
67+
self._operations = []
68+
69+
self._operations.append(operation)
70+
71+
@property
72+
def operations(self):
73+
return self._operations
74+
75+
def to_json(self):
76+
if not self._operations:
77+
raise ValueError("Missing operations property on Counters")
78+
return {
79+
"DocumentId": self._document_id,
80+
"Operations": [operation.to_json() for operation in self._operations],
81+
}
82+
83+
84+
class CounterBatch:
85+
def __init__(
86+
self,
87+
reply_with_all_nodes_values: Optional[bool] = None,
88+
documents: Optional[List[DocumentCountersOperation]] = None,
89+
from_etl: Optional[bool] = None,
90+
):
91+
self.reply_with_all_nodes_values = reply_with_all_nodes_values
92+
self.documents = documents
93+
self.from_etl = from_etl
94+
95+
def to_json(self) -> Dict:
96+
return {
97+
"ReplyWithAllNodesValues": self.reply_with_all_nodes_values,
98+
"Documents": [document_counters_operation.to_json() for document_counters_operation in self.documents],
99+
"FromEtl": self.from_etl,
100+
}
101+
102+
103+
class CounterDetail:
104+
def __init__(
105+
self,
106+
document_id: Optional[str] = None,
107+
counter_name: Optional[str] = None,
108+
total_value: Optional[int] = None,
109+
etag: Optional[int] = None,
110+
counter_values: Optional[Dict[str, int]] = None,
111+
):
112+
self.document_id = document_id
113+
self.counter_name = counter_name
114+
self.total_value = total_value
115+
self.etag = etag
116+
self.counter_values = counter_values
117+
118+
def to_json(self) -> Dict:
119+
return {
120+
"DocumentId": self.document_id,
121+
"CounterName": self.counter_name,
122+
"TotalValue": self.total_value,
123+
"Etag": self.etag,
124+
"CounterValues": self.counter_values,
125+
}
126+
127+
@classmethod
128+
def from_json(cls, json_dict: Dict) -> Optional[CounterDetail]:
129+
return (
130+
cls(
131+
json_dict["DocumentId"],
132+
json_dict["CounterName"],
133+
json_dict["TotalValue"],
134+
json_dict.get("Etag", None),
135+
json_dict["CounterValues"],
136+
)
137+
if json_dict is not None
138+
else None
139+
)
140+
141+
142+
class CountersDetail:
143+
def __init__(self, counters: List[CounterDetail]):
144+
self.counters = counters
145+
146+
def to_json(self) -> Dict:
147+
return {"Counters": [counter.to_json() for counter in self.counters]}
148+
149+
@classmethod
150+
def from_json(cls, json_dict: Dict) -> CountersDetail:
151+
return cls([CounterDetail.from_json(counter_detail_json) for counter_detail_json in json_dict["Counters"]])
152+
153+
154+
class CounterBatchOperation(IOperation[CountersDetail]):
155+
def __init__(self, counter_batch: CounterBatch):
156+
self._counter_batch = counter_batch
157+
158+
def get_command(
159+
self, store: "DocumentStore", conventions: "DocumentConventions", cache: HttpCache
160+
) -> RavenCommand[CountersDetail]:
161+
return CounterBatchOperation.CounterBatchCommand(self._counter_batch)
162+
163+
class CounterBatchCommand(RavenCommand[CountersDetail]):
164+
def __init__(self, counter_batch: CounterBatch):
165+
super().__init__(CountersDetail)
166+
167+
if counter_batch is None:
168+
raise ValueError("Counter batch cannot be None")
169+
170+
self._counter_batch = counter_batch
171+
172+
def create_request(self, node: ServerNode) -> requests.Request:
173+
return requests.Request(
174+
"POST", f"{node.url}/databases/{node.database}/counters", data=self._counter_batch.to_json()
175+
)
176+
177+
def set_response(self, response: Optional[str], from_cache: bool) -> None:
178+
if response is None:
179+
return
180+
181+
self.result = CountersDetail.from_json(json.loads(response))
182+
183+
def is_read_request(self) -> bool:
184+
return False
185+
186+
187+
class GetCountersOperation(IOperation[CountersDetail]):
188+
def __init__(
189+
self, doc_id: str, counters: Optional[Union[str, List[str]]] = None, return_full_results: Optional[bool] = None
190+
):
191+
self._doc_id = doc_id
192+
self._counters = [] if counters is None else counters if isinstance(counters, list) else [counters]
193+
self._return_full_results = return_full_results
194+
195+
def get_command(
196+
self, store: "DocumentStore", conventions: "DocumentConventions", cache: HttpCache
197+
) -> RavenCommand[CounterDetail]:
198+
return GetCountersOperation.GetCounterValuesCommand(
199+
self._doc_id, self._counters, self._return_full_results, conventions
200+
)
201+
202+
class GetCounterValuesCommand(RavenCommand[CountersDetail]):
203+
def __init__(
204+
self, doc_id: str, counters: List[str], return_full_results: bool, conventions: DocumentConventions
205+
):
206+
super(GetCountersOperation.GetCounterValuesCommand, self).__init__(CountersDetail)
207+
208+
if doc_id is None:
209+
raise ValueError("Doc id cannot be None")
210+
211+
self._doc_id = doc_id
212+
self._counters = counters
213+
self._return_full_results = return_full_results
214+
self._conventions = conventions
215+
216+
def create_request(self, node: ServerNode) -> requests.Request:
217+
path_builder = [node.url]
218+
path_builder.append("/databases/")
219+
path_builder.append(node.database)
220+
path_builder.append("/counters?docId=")
221+
path_builder.append(Utils.quote_key(self._doc_id))
222+
223+
request = requests.Request("GET")
224+
225+
if self._counters:
226+
if len(self._counters) > 1:
227+
request = self._prepare_request_with_multiple_counters(path_builder, request)
228+
else:
229+
path_builder.append("&counter=")
230+
path_builder.append(Utils.quote_key(self._counters[0]))
231+
232+
if self._return_full_results and request.method == "GET":
233+
path_builder.append("&full=true")
234+
235+
request.url = "".join(path_builder)
236+
237+
return request
238+
239+
def _prepare_request_with_multiple_counters(
240+
self, path_builder: List[str], request: requests.Request
241+
) -> requests.Request:
242+
unique_names, sum_length = self._get_ordered_unique_names()
243+
244+
# if it is too big, we drop to POST (note that means that we can't use the HTTP cache any longer)
245+
# we are fine with that, such requests are going to be rare
246+
if sum_length < 1024:
247+
for unique_name in unique_names:
248+
path_builder.append("&counter=")
249+
path_builder.append(Utils.quote_key(unique_name or ""))
250+
else:
251+
request = requests.Request("POST")
252+
253+
doc_ops = DocumentCountersOperation(self._doc_id, [])
254+
255+
for counter in unique_names:
256+
counter_operation = CounterOperation(counter, CounterOperationType.GET)
257+
doc_ops.operations.append(counter_operation)
258+
259+
batch = CounterBatch()
260+
batch.documents = [doc_ops]
261+
batch.reply_with_all_nodes_values = self._return_full_results
262+
263+
request.data = batch.to_json()
264+
return request
265+
266+
def set_response(self, response: Optional[str], from_cache: bool) -> None:
267+
if response is None:
268+
return
269+
270+
self.result = CountersDetail.from_json(json.loads(response))
271+
272+
def _get_ordered_unique_names(self) -> Tuple[List[str], int]:
273+
unique_names = set(self._counters)
274+
sum_length = sum([len(counter) if counter else 0 for counter in unique_names])
275+
return list(unique_names), sum_length
276+
277+
def is_read_request(self) -> bool:
278+
return True

0 commit comments

Comments
 (0)