Skip to content

Commit b5c359b

Browse files
committed
RDBC-1079 Python API: Revisions Operations
Add the missing revisions operations, ported from the C# client (v7.2): - EnforceRevisionsConfigurationOperation (IOperation<OperationIdResult>) - AdoptOrphanedRevisionsOperation (IOperation<OperationIdResult>) - DeleteRevisionsOperation (by id / date range / change vectors) - RevertRevisionsByIdOperation - ConfigureRevisionsBinCleanerOperation (+ RevisionsBinConfiguration) - ConfigureRevisionsForConflictsOperation (server-wide) - Supporting types: RevisionsOperationParameters, RevisionsOperationContinuationParameters, result classes Surface the full revisions operations set from the top-level ravendb package and add real-DB tests for each operation.
1 parent 1ab9583 commit b5c359b

5 files changed

Lines changed: 617 additions & 10 deletions

File tree

ravendb/__init__.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,23 @@
173173
from ravendb.documents.operations.revisions import (
174174
RevisionsCollectionConfiguration,
175175
RevisionsConfiguration,
176+
RevisionsResult,
177+
ConfigureRevisionsOperation,
178+
ConfigureRevisionsOperationResult,
179+
GetRevisionsOperation,
180+
EnforceRevisionsConfigurationOperation,
181+
AdoptOrphanedRevisionsOperation,
182+
DeleteRevisionsOperation,
183+
RevertRevisionsByIdOperation,
184+
ConfigureRevisionsBinCleanerOperation,
185+
ConfigureRevisionsBinCleanerOperationResult,
186+
RevisionsBinConfiguration,
187+
RevisionsOperationParameters,
188+
RevisionsOperationContinuationParameters,
189+
)
190+
from ravendb.serverwide.operations.revisions import (
191+
ConfigureRevisionsForConflictsOperation,
192+
ConfigureRevisionsForConflictsResult,
176193
)
177194
from ravendb.documents.operations.statistics import (
178195
GetCollectionStatisticsOperation,
@@ -340,7 +357,6 @@
340357

341358
# todo: Serverwide
342359
# ReorderDatabaseMembersOperation
343-
# ConfigureRevisionsForConflictsOperation
344360
# UpdateDatabaseOperation
345361
# GetServerWideBackupConfigurationOperation
346362
# SetDatabaseDynamicDistributionOperation
@@ -420,9 +436,6 @@
420436
# LazyRevisionOperation
421437
# LazyRevisionOperations
422438
# StreamOperation
423-
# ConfigureRevisionsOperation
424-
# GetRevisionsOperation
425-
# RevisionsResult
426439
# GetConnectionStringsOperation
427440
# RemoveConnectionStringOperation
428441
# SqlEtlTable

ravendb/documents/operations/revisions.py

Lines changed: 337 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,18 @@
88
import requests
99

1010
from ravendb.documents.commands.revisions import GetRevisionsCommand
11-
from ravendb.documents.operations.definitions import IOperation, MaintenanceOperation
12-
from ravendb.http.raven_command import RavenCommand
11+
from ravendb.documents.operations.definitions import (
12+
IOperation,
13+
MaintenanceOperation,
14+
OperationIdResult,
15+
VoidOperation,
16+
)
17+
from ravendb.http.raven_command import RavenCommand, VoidRavenCommand
1318
from ravendb.util.util import RaftIdGenerator
1419
from ravendb.http.topology import RaftCommand
1520
from ravendb.documents.session.entity_to_json import EntityToJsonStatic
1621
from ravendb.documents.conventions import DocumentConventions
22+
from ravendb.tools.utils import Utils
1723

1824
if TYPE_CHECKING:
1925
from ravendb.http.http_cache import HttpCache
@@ -216,3 +222,332 @@ def set_response(self, response: Optional[str], from_cache: bool) -> None:
216222

217223
def get_raft_unique_request_id(self) -> str:
218224
return RaftIdGenerator().new_id()
225+
226+
227+
class RevisionsOperationContinuationParameters:
228+
"""State for resuming an interrupted revisions operation (etags are node-local)."""
229+
230+
def __init__(
231+
self,
232+
start_from_etags: Dict[str, int] = None,
233+
etag_barriers: Dict[str, int] = None,
234+
node_tags: Dict[str, str] = None,
235+
):
236+
self.start_from_etags = start_from_etags
237+
self.etag_barriers = etag_barriers
238+
self.node_tags = node_tags
239+
240+
def to_json(self) -> Dict[str, Any]:
241+
return {
242+
"StartFromEtags": self.start_from_etags,
243+
"EtagBarriers": self.etag_barriers,
244+
"NodeTags": self.node_tags,
245+
}
246+
247+
248+
class RevisionsOperationParameters:
249+
"""Base parameters shared by enforce-configuration and adopt-orphaned operations."""
250+
251+
def __init__(
252+
self,
253+
collections: List[str] = None,
254+
continuation_parameters: RevisionsOperationContinuationParameters = None,
255+
):
256+
self.collections = collections
257+
self.continuation_parameters = continuation_parameters
258+
259+
def to_json(self) -> Dict[str, Any]:
260+
return {
261+
"Collections": self.collections,
262+
"ContinuationParameters": (
263+
self.continuation_parameters.to_json() if self.continuation_parameters else None
264+
),
265+
}
266+
267+
268+
class EnforceRevisionsConfigurationOperation(IOperation[OperationIdResult]):
269+
"""Applies the current revisions configuration to all existing revisions (long-running:
270+
send via ``store.operations.send_async`` and await completion)."""
271+
272+
class Parameters(RevisionsOperationParameters):
273+
def __init__(
274+
self,
275+
include_force_created: bool = False,
276+
max_ops_per_second: int = None,
277+
collections: List[str] = None,
278+
continuation_parameters: RevisionsOperationContinuationParameters = None,
279+
):
280+
super().__init__(collections, continuation_parameters)
281+
if max_ops_per_second is not None and max_ops_per_second <= 0:
282+
raise ValueError("max_ops_per_second must be greater than 0")
283+
self.include_force_created = include_force_created
284+
self.max_ops_per_second = max_ops_per_second
285+
286+
def to_json(self) -> Dict[str, Any]:
287+
json_dict = super().to_json()
288+
json_dict["IncludeForceCreated"] = self.include_force_created
289+
json_dict["MaxOpsPerSecond"] = self.max_ops_per_second
290+
return json_dict
291+
292+
def __init__(self, parameters: Optional["EnforceRevisionsConfigurationOperation.Parameters"] = None):
293+
self._parameters = parameters if parameters is not None else EnforceRevisionsConfigurationOperation.Parameters()
294+
295+
def get_command(
296+
self, store: DocumentStore, conventions: DocumentConventions, cache: HttpCache
297+
) -> RavenCommand[OperationIdResult]:
298+
return self.EnforceRevisionsConfigurationCommand(self._parameters)
299+
300+
class EnforceRevisionsConfigurationCommand(RavenCommand[OperationIdResult]):
301+
def __init__(self, parameters: "EnforceRevisionsConfigurationOperation.Parameters"):
302+
super().__init__(OperationIdResult)
303+
self._parameters = parameters
304+
305+
def is_read_request(self) -> bool:
306+
return False
307+
308+
def create_request(self, node: ServerNode) -> requests.Request:
309+
url = f"{node.url}/databases/{node.database}/admin/revisions/config/enforce"
310+
request = requests.Request("POST", url)
311+
request.data = self._parameters.to_json()
312+
return request
313+
314+
def set_response(self, response: Optional[str], from_cache: bool) -> None:
315+
if response is None:
316+
self._throw_invalid_response()
317+
self.result = OperationIdResult.from_json(json.loads(response))
318+
319+
320+
class AdoptOrphanedRevisionsOperation(IOperation[OperationIdResult]):
321+
"""Re-attaches orphaned revisions to their documents (long-running: send via
322+
``store.operations.send_async`` and await completion)."""
323+
324+
class Parameters(RevisionsOperationParameters):
325+
pass
326+
327+
def __init__(self, parameters: Optional["AdoptOrphanedRevisionsOperation.Parameters"] = None):
328+
self._parameters = parameters if parameters is not None else AdoptOrphanedRevisionsOperation.Parameters()
329+
330+
def get_command(
331+
self, store: DocumentStore, conventions: DocumentConventions, cache: HttpCache
332+
) -> RavenCommand[OperationIdResult]:
333+
return self.AdoptOrphanedRevisionsCommand(self._parameters)
334+
335+
class AdoptOrphanedRevisionsCommand(RavenCommand[OperationIdResult]):
336+
def __init__(self, parameters: "AdoptOrphanedRevisionsOperation.Parameters"):
337+
super().__init__(OperationIdResult)
338+
self._parameters = parameters
339+
340+
def is_read_request(self) -> bool:
341+
return False
342+
343+
def create_request(self, node: ServerNode) -> requests.Request:
344+
url = f"{node.url}/databases/{node.database}/admin/revisions/orphaned/adopt"
345+
request = requests.Request("POST", url)
346+
request.data = self._parameters.to_json()
347+
return request
348+
349+
def set_response(self, response: Optional[str], from_cache: bool) -> None:
350+
if response is None:
351+
self._throw_invalid_response()
352+
self.result = OperationIdResult.from_json(json.loads(response))
353+
354+
355+
class DeleteRevisionsOperation(MaintenanceOperation["DeleteRevisionsOperation.Result"]):
356+
"""Explicitly deletes revisions for one or more documents - by document id(s), by a
357+
date range, or by specific revision change-vectors."""
358+
359+
class Result:
360+
def __init__(self, total_deletes: int = None):
361+
self.total_deletes = total_deletes
362+
363+
@classmethod
364+
def from_json(cls, json_dict: Dict[str, Any]) -> DeleteRevisionsOperation.Result:
365+
return cls(json_dict["TotalDeletes"])
366+
367+
class Parameters:
368+
def __init__(
369+
self,
370+
document_ids: List[str] = None,
371+
remove_force_created_revisions: bool = False,
372+
revisions_change_vectors: List[str] = None,
373+
from_date: datetime.datetime = None,
374+
to_date: datetime.datetime = None,
375+
):
376+
self.document_ids = document_ids
377+
self.remove_force_created_revisions = remove_force_created_revisions
378+
self.revisions_change_vectors = revisions_change_vectors
379+
self.from_date = from_date
380+
self.to_date = to_date
381+
382+
def validate(self) -> None:
383+
if not self.document_ids:
384+
raise ValueError("Document ids cannot be None or empty")
385+
386+
for document_id in self.document_ids:
387+
if not document_id or document_id.isspace():
388+
raise ValueError("Document id cannot be None or whitespace")
389+
390+
if self.revisions_change_vectors:
391+
if len(self.document_ids) != 1:
392+
raise ValueError("The number of document ids must be 1 when using revisions change vectors")
393+
if self.from_date is not None or self.to_date is not None:
394+
raise ValueError("Can't use revisions change vectors and date range in the same request")
395+
elif self.from_date is not None and self.to_date is not None and self.to_date <= self.from_date:
396+
raise ValueError("To date must be greater than From date")
397+
398+
def to_json(self) -> Dict[str, Any]:
399+
return {
400+
"DocumentIds": self.document_ids,
401+
"RevisionsChangeVectors": self.revisions_change_vectors,
402+
"From": Utils.datetime_to_string(self.from_date) if self.from_date is not None else None,
403+
"To": Utils.datetime_to_string(self.to_date) if self.to_date is not None else None,
404+
"RemoveForceCreatedRevisions": self.remove_force_created_revisions,
405+
}
406+
407+
def __init__(
408+
self,
409+
document_id: str = None,
410+
revisions_change_vectors: List[str] = None,
411+
from_date: datetime.datetime = None,
412+
to_date: datetime.datetime = None,
413+
remove_force_created_revisions: bool = False,
414+
document_ids: List[str] = None,
415+
parameters: Optional["DeleteRevisionsOperation.Parameters"] = None,
416+
):
417+
if parameters is None:
418+
if document_ids is None and document_id is not None:
419+
document_ids = [document_id]
420+
parameters = DeleteRevisionsOperation.Parameters(
421+
document_ids,
422+
remove_force_created_revisions,
423+
revisions_change_vectors,
424+
from_date,
425+
to_date,
426+
)
427+
parameters.validate()
428+
self._parameters = parameters
429+
430+
def get_command(self, conventions: DocumentConventions) -> RavenCommand[DeleteRevisionsOperation.Result]:
431+
return self.DeleteRevisionsCommand(self._parameters)
432+
433+
class DeleteRevisionsCommand(RavenCommand["DeleteRevisionsOperation.Result"], RaftCommand):
434+
def __init__(self, parameters: "DeleteRevisionsOperation.Parameters"):
435+
super().__init__(DeleteRevisionsOperation.Result)
436+
self._parameters = parameters
437+
438+
def is_read_request(self) -> bool:
439+
return False
440+
441+
def create_request(self, node: ServerNode) -> requests.Request:
442+
url = f"{node.url}/databases/{node.database}/admin/revisions"
443+
request = requests.Request("DELETE", url)
444+
request.data = self._parameters.to_json()
445+
return request
446+
447+
def set_response(self, response: Optional[str], from_cache: bool) -> None:
448+
if response is None:
449+
self._throw_invalid_response()
450+
self.result = DeleteRevisionsOperation.Result.from_json(json.loads(response))
451+
452+
def get_raft_unique_request_id(self) -> str:
453+
return RaftIdGenerator().new_id()
454+
455+
456+
class RevertRevisionsByIdOperation(VoidOperation):
457+
"""Reverts one or more documents to a specific revision identified by its change-vector."""
458+
459+
def __init__(self, id_to_change_vector: Dict[str, str] = None, id_: str = None, change_vector: str = None):
460+
if id_to_change_vector is None:
461+
if not id_:
462+
raise ValueError("Id cannot be None or empty")
463+
if not change_vector:
464+
raise ValueError("Change vector cannot be None or empty")
465+
id_to_change_vector = {id_: change_vector}
466+
467+
if not id_to_change_vector:
468+
raise ValueError("id_to_change_vector cannot be None or empty")
469+
470+
self._id_to_change_vector = id_to_change_vector
471+
472+
def get_command(self, store: DocumentStore, conventions: DocumentConventions, cache: HttpCache) -> VoidRavenCommand:
473+
return self.RevertRevisionsByIdCommand(self._id_to_change_vector)
474+
475+
class RevertRevisionsByIdCommand(VoidRavenCommand):
476+
def __init__(self, id_to_change_vector: Dict[str, str]):
477+
super().__init__()
478+
self._id_to_change_vector = id_to_change_vector
479+
480+
def is_read_request(self) -> bool:
481+
return False
482+
483+
def create_request(self, node: ServerNode) -> requests.Request:
484+
url = f"{node.url}/databases/{node.database}/revisions/revert/docs"
485+
request = requests.Request("POST", url)
486+
request.data = {"IdToChangeVector": self._id_to_change_vector}
487+
return request
488+
489+
490+
class RevisionsBinConfiguration:
491+
"""Configuration for the automatic revisions-bin cleaner."""
492+
493+
def __init__(
494+
self,
495+
disabled: bool = False,
496+
minimum_entries_age_to_keep_in_min: int = 43200,
497+
cleaner_frequency_in_sec: int = 300,
498+
):
499+
self.disabled = disabled
500+
self.minimum_entries_age_to_keep_in_min = minimum_entries_age_to_keep_in_min
501+
self.cleaner_frequency_in_sec = cleaner_frequency_in_sec
502+
503+
def to_json(self) -> Dict[str, Any]:
504+
return {
505+
"Disabled": self.disabled,
506+
"MinimumEntriesAgeToKeepInMin": self.minimum_entries_age_to_keep_in_min,
507+
"CleanerFrequencyInSec": self.cleaner_frequency_in_sec,
508+
}
509+
510+
511+
class ConfigureRevisionsBinCleanerOperationResult:
512+
def __init__(self, raft_command_index: int = None):
513+
self.raft_command_index = raft_command_index
514+
515+
@classmethod
516+
def from_json(cls, json_dict: Dict[str, Any]) -> ConfigureRevisionsBinCleanerOperationResult:
517+
return cls(json_dict["RaftCommandIndex"])
518+
519+
520+
class ConfigureRevisionsBinCleanerOperation(MaintenanceOperation[ConfigureRevisionsBinCleanerOperationResult]):
521+
"""Enables / configures the automatic revisions-bin cleaner for the database."""
522+
523+
def __init__(self, configuration: RevisionsBinConfiguration):
524+
if configuration is None:
525+
raise ValueError("Configuration cannot be None")
526+
self._configuration = configuration
527+
528+
def get_command(
529+
self, conventions: DocumentConventions
530+
) -> RavenCommand[ConfigureRevisionsBinCleanerOperationResult]:
531+
return self.ConfigureRevisionsBinCleanerCommand(self._configuration)
532+
533+
class ConfigureRevisionsBinCleanerCommand(RavenCommand[ConfigureRevisionsBinCleanerOperationResult], RaftCommand):
534+
def __init__(self, configuration: RevisionsBinConfiguration):
535+
super().__init__(ConfigureRevisionsBinCleanerOperationResult)
536+
self._configuration = configuration
537+
538+
def is_read_request(self) -> bool:
539+
return False
540+
541+
def create_request(self, node: ServerNode) -> requests.Request:
542+
url = f"{node.url}/databases/{node.database}/admin/revisions/bin-cleaner/config"
543+
request = requests.Request("POST", url)
544+
request.data = self._configuration.to_json()
545+
return request
546+
547+
def set_response(self, response: Optional[str], from_cache: bool) -> None:
548+
if response is None:
549+
self._throw_invalid_response()
550+
self.result = ConfigureRevisionsBinCleanerOperationResult.from_json(json.loads(response))
551+
552+
def get_raft_unique_request_id(self) -> str:
553+
return RaftIdGenerator().new_id()

0 commit comments

Comments
 (0)