|
8 | 8 | import requests |
9 | 9 |
|
10 | 10 | 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 |
13 | 18 | from ravendb.util.util import RaftIdGenerator |
14 | 19 | from ravendb.http.topology import RaftCommand |
15 | 20 | from ravendb.documents.session.entity_to_json import EntityToJsonStatic |
16 | 21 | from ravendb.documents.conventions import DocumentConventions |
| 22 | +from ravendb.tools.utils import Utils |
17 | 23 |
|
18 | 24 | if TYPE_CHECKING: |
19 | 25 | from ravendb.http.http_cache import HttpCache |
@@ -216,3 +222,332 @@ def set_response(self, response: Optional[str], from_cache: bool) -> None: |
216 | 222 |
|
217 | 223 | def get_raft_unique_request_id(self) -> str: |
218 | 224 | 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