1__all__ = ["Foxx"] 2 3import os 4from typing import Any, BinaryIO, Dict, Optional, Tuple, Union 5 6from requests_toolbelt import MultipartEncoder 7 8from arango.api import ApiGroup 9from arango.exceptions import ( 10 FoxxCommitError, 11 FoxxConfigGetError, 12 FoxxConfigReplaceError, 13 FoxxConfigUpdateError, 14 FoxxDependencyGetError, 15 FoxxDependencyReplaceError, 16 FoxxDependencyUpdateError, 17 FoxxDevModeDisableError, 18 FoxxDevModeEnableError, 19 FoxxDownloadError, 20 FoxxReadmeGetError, 21 FoxxScriptListError, 22 FoxxScriptRunError, 23 FoxxServiceCreateError, 24 FoxxServiceDeleteError, 25 FoxxServiceGetError, 26 FoxxServiceListError, 27 FoxxServiceReplaceError, 28 FoxxServiceUpdateError, 29 FoxxSwaggerGetError, 30 FoxxTestRunError, 31) 32from arango.formatter import format_service_data 33from arango.request import Request 34from arango.response import Response 35from arango.result import Result 36from arango.typings import Json, Jsons, Params 37 38 39class Foxx(ApiGroup): 40 """Foxx API wrapper.""" 41 42 def __repr__(self) -> str: 43 return f"<Foxx in {self._conn.db_name}>" 44 45 def _encode( 46 self, 47 filename: str, 48 config: Optional[Json] = None, 49 dependencies: Optional[Json] = None, 50 ) -> MultipartEncoder: 51 """Encode file, configuration and dependencies into multipart data. 52 53 :param filename: Full path to the javascript file or zip bundle. 54 :type filename: str 55 :param config: Configuration values. 56 :type config: dict | None 57 :param dependencies: Dependency settings. 58 :type dependencies: dict | None 59 :return: Multipart encoder object 60 :rtype: requests_toolbelt.MultipartEncoder 61 """ 62 extension = os.path.splitext(filename)[1] 63 if extension == ".js": # pragma: no cover 64 source_type = "application/javascript" 65 elif extension == ".zip": 66 source_type = "application/zip" 67 else: 68 raise ValueError("File extension must be .zip or .js") 69 70 fields: Dict[str, Union[bytes, Tuple[None, BinaryIO, str]]] = { 71 "source": (None, open(filename, "rb"), source_type) 72 } 73 74 if config is not None: 75 fields["configuration"] = self._conn.serialize(config).encode("utf-8") 76 77 if dependencies is not None: 78 fields["dependencies"] = self._conn.serialize(dependencies).encode("utf-8") 79 80 return MultipartEncoder(fields=fields) 81 82 def services(self, exclude_system: bool = False) -> Result[Jsons]: 83 """List installed services. 84 85 :param exclude_system: If set to True, system services are excluded. 86 :type exclude_system: bool 87 :return: List of installed service. 88 :rtype: [dict] 89 :raise arango.exceptions.FoxxServiceListError: If retrieval fails. 90 """ 91 request = Request( 92 method="get", 93 endpoint="/_api/foxx", 94 params={"excludeSystem": exclude_system}, 95 ) 96 97 def response_handler(resp: Response) -> Jsons: 98 if resp.is_success: 99 return [format_service_data(service) for service in resp.body] 100 raise FoxxServiceListError(resp, request) 101 102 return self._execute(request, response_handler) 103 104 def service(self, mount: str) -> Result[Json]: 105 """Return service metadata. 106 107 :param mount: Service mount path (e.g "/_admin/aardvark"). 108 :type mount: str 109 :return: Service metadata. 110 :rtype: dict 111 :raise arango.exceptions.FoxxServiceGetError: If retrieval fails. 112 """ 113 request = Request( 114 method="get", endpoint="/_api/foxx/service", params={"mount": mount} 115 ) 116 117 def response_handler(resp: Response) -> Json: 118 if resp.is_success: 119 return format_service_data(resp.body) 120 raise FoxxServiceGetError(resp, request) 121 122 return self._execute(request, response_handler) 123 124 def create_service( 125 self, 126 mount: str, 127 source: str, 128 config: Optional[Json] = None, 129 dependencies: Optional[Json] = None, 130 development: Optional[bool] = None, 131 setup: Optional[bool] = None, 132 legacy: Optional[bool] = None, 133 ) -> Result[Json]: 134 """Install a new service using JSON definition. 135 136 :param mount: Service mount path (e.g "/_admin/aardvark"). 137 :type mount: str 138 :param source: Fully qualified URL or absolute path on the server file 139 system. Must be accessible by the server, or by all servers if in 140 a cluster. 141 :type source: str 142 :param config: Configuration values. 143 :type config: dict | None 144 :param dependencies: Dependency settings. 145 :type dependencies: dict | None 146 :param development: Enable development mode. 147 :type development: bool | None 148 :param setup: Run service setup script. 149 :type setup: bool | None 150 :param legacy: Install the service in 2.8 legacy compatibility mode. 151 :type legacy: bool | None 152 :return: Service metadata. 153 :rtype: dict 154 :raise arango.exceptions.FoxxServiceCreateError: If install fails. 155 """ 156 params: Params = {"mount": mount} 157 if development is not None: 158 params["development"] = development 159 if setup is not None: 160 params["setup"] = setup 161 if legacy is not None: 162 params["legacy"] = legacy 163 164 data: Json = {"source": source} 165 if config is not None: 166 data["configuration"] = config 167 if dependencies is not None: 168 data["dependencies"] = dependencies 169 170 request = Request( 171 method="post", 172 endpoint="/_api/foxx", 173 params=params, 174 data=data, 175 ) 176 177 def response_handler(resp: Response) -> Json: 178 if resp.is_success: 179 return format_service_data(resp.body) 180 raise FoxxServiceCreateError(resp, request) 181 182 return self._execute(request, response_handler) 183 184 def create_service_with_file( 185 self, 186 mount: str, 187 filename: str, 188 development: Optional[bool] = None, 189 setup: Optional[bool] = None, 190 legacy: Optional[bool] = None, 191 config: Optional[Json] = None, 192 dependencies: Optional[Json] = None, 193 ) -> Result[Json]: 194 """Install a new service using a javascript file or zip bundle. 195 196 :param mount: Service mount path (e.g "/_admin/aardvark"). 197 :type mount: str 198 :param filename: Full path to the javascript file or zip bundle. 199 :type filename: str 200 :param development: Enable development mode. 201 :type development: bool | None 202 :param setup: Run service setup script. 203 :type setup: bool | None 204 :param legacy: Install the service in 2.8 legacy compatibility mode. 205 :type legacy: bool | None 206 :param config: Configuration values. 207 :type config: dict | None 208 :param dependencies: Dependency settings. 209 :type dependencies: dict | None 210 :return: Service metadata. 211 :rtype: dict 212 :raise arango.exceptions.FoxxServiceCreateError: If install fails. 213 """ 214 params: Params = {"mount": mount} 215 if development is not None: 216 params["development"] = development 217 if setup is not None: 218 params["setup"] = setup 219 if legacy is not None: 220 params["legacy"] = legacy 221 222 data = self._encode(filename, config, dependencies) 223 request = Request( 224 method="post", 225 endpoint="/_api/foxx", 226 params=params, 227 data=data, 228 headers={"content-type": data.content_type}, 229 ) 230 231 def response_handler(resp: Response) -> Json: 232 if resp.is_success: 233 return format_service_data(resp.body) 234 raise FoxxServiceCreateError(resp, request) 235 236 return self._execute(request, response_handler) 237 238 def update_service( 239 self, 240 mount: str, 241 source: str, 242 config: Optional[Json] = None, 243 dependencies: Optional[Json] = None, 244 teardown: Optional[bool] = None, 245 setup: Optional[bool] = None, 246 legacy: Optional[bool] = None, 247 force: Optional[bool] = None, 248 ) -> Result[Json]: 249 """Update (upgrade) a service. 250 251 :param mount: Service mount path (e.g "/_admin/aardvark"). 252 :type mount: str 253 :param source: Fully qualified URL or absolute path on the server file 254 system. Must be accessible by the server, or by all servers if in 255 a cluster. 256 :type source: str 257 :param config: Configuration values. 258 :type config: dict | None 259 :param dependencies: Dependency settings. 260 :type dependencies: dict | None 261 :param teardown: Run service teardown script. 262 :type teardown: bool | None 263 :param setup: Run service setup script. 264 :type setup: bool | None 265 :param legacy: Update the service in 2.8 legacy compatibility mode. 266 :type legacy: bool | None 267 :param force: Force update if no service is found. 268 :type force: bool | None 269 :return: Updated service metadata. 270 :rtype: dict 271 :raise arango.exceptions.FoxxServiceUpdateError: If update fails. 272 """ 273 params: Params = {"mount": mount} 274 if teardown is not None: 275 params["teardown"] = teardown 276 if setup is not None: 277 params["setup"] = setup 278 if legacy is not None: 279 params["legacy"] = legacy 280 if force is not None: 281 params["force"] = force 282 283 data: Json = {} 284 if source is not None: 285 data["source"] = source 286 if config is not None: 287 data["configuration"] = config 288 if dependencies is not None: 289 data["dependencies"] = dependencies 290 291 request = Request( 292 method="patch", 293 endpoint="/_api/foxx/service", 294 params=params, 295 data=data, 296 ) 297 298 def response_handler(resp: Response) -> Json: 299 if resp.is_success: 300 return format_service_data(resp.body) 301 raise FoxxServiceUpdateError(resp, request) 302 303 return self._execute(request, response_handler) 304 305 def update_service_with_file( 306 self, 307 mount: str, 308 filename: str, 309 teardown: Optional[bool] = None, 310 setup: Optional[bool] = None, 311 legacy: Optional[bool] = None, 312 force: Optional[bool] = None, 313 config: Optional[Json] = None, 314 dependencies: Optional[Json] = None, 315 ) -> Result[Json]: 316 """Update (upgrade) a service using a javascript file or zip bundle. 317 318 :param mount: Service mount path (e.g "/_admin/aardvark"). 319 :type mount: str 320 :param filename: Full path to the javascript file or zip bundle. 321 :type filename: str 322 :param teardown: Run service teardown script. 323 :type teardown: bool | None 324 :param setup: Run service setup script. 325 :type setup: bool | None 326 :param legacy: Update the service in 2.8 legacy compatibility mode. 327 :type legacy: bool | None 328 :param force: Force update if no service is found. 329 :type force: bool | None 330 :param config: Configuration values. 331 :type config: dict | None 332 :param dependencies: Dependency settings. 333 :type dependencies: dict | None 334 :return: Updated service metadata. 335 :rtype: dict 336 :raise arango.exceptions.FoxxServiceUpdateError: If update fails. 337 """ 338 params: Params = {"mount": mount} 339 if teardown is not None: 340 params["teardown"] = teardown 341 if setup is not None: 342 params["setup"] = setup 343 if legacy is not None: 344 params["legacy"] = legacy 345 if force is not None: 346 params["force"] = force 347 348 data = self._encode(filename, config, dependencies) 349 request = Request( 350 method="patch", 351 endpoint="/_api/foxx/service", 352 params=params, 353 data=data, 354 headers={"content-type": data.content_type}, 355 ) 356 357 def response_handler(resp: Response) -> Json: 358 if resp.is_success: 359 return format_service_data(resp.body) 360 raise FoxxServiceUpdateError(resp, request) 361 362 return self._execute(request, response_handler) 363 364 def replace_service( 365 self, 366 mount: str, 367 source: str, 368 config: Optional[Json] = None, 369 dependencies: Optional[Json] = None, 370 teardown: Optional[bool] = None, 371 setup: Optional[bool] = None, 372 legacy: Optional[bool] = None, 373 force: Optional[bool] = None, 374 ) -> Result[Json]: 375 """Replace a service by removing the old one and installing a new one. 376 377 :param mount: Service mount path (e.g "/_admin/aardvark"). 378 :type mount: str 379 :param source: Fully qualified URL or absolute path on the server file 380 system. Must be accessible by the server, or by all servers if in 381 a cluster. 382 :type source: str 383 :param config: Configuration values. 384 :type config: dict | None 385 :param dependencies: Dependency settings. 386 :type dependencies: dict | None 387 :param teardown: Run service teardown script. 388 :type teardown: bool | None 389 :param setup: Run service setup script. 390 :type setup: bool | None 391 :param legacy: Replace the service in 2.8 legacy compatibility mode. 392 :type legacy: bool | None 393 :param force: Force install if no service is found. 394 :type force: bool | None 395 :return: Replaced service metadata. 396 :rtype: dict 397 :raise arango.exceptions.FoxxServiceReplaceError: If replace fails. 398 """ 399 params: Params = {"mount": mount} 400 if teardown is not None: 401 params["teardown"] = teardown 402 if setup is not None: 403 params["setup"] = setup 404 if legacy is not None: 405 params["legacy"] = legacy 406 if force is not None: 407 params["force"] = force 408 409 data: Json = {} 410 if source is not None: 411 data["source"] = source 412 if config is not None: 413 data["configuration"] = config 414 if dependencies is not None: 415 data["dependencies"] = dependencies 416 417 request = Request( 418 method="put", 419 endpoint="/_api/foxx/service", 420 params=params, 421 data=data, 422 ) 423 424 def response_handler(resp: Response) -> Json: 425 if resp.is_success: 426 return format_service_data(resp.body) 427 raise FoxxServiceReplaceError(resp, request) 428 429 return self._execute(request, response_handler) 430 431 def replace_service_with_file( 432 self, 433 mount: str, 434 filename: str, 435 teardown: Optional[bool] = None, 436 setup: Optional[bool] = None, 437 legacy: Optional[bool] = None, 438 force: Optional[bool] = None, 439 config: Optional[Json] = None, 440 dependencies: Optional[Json] = None, 441 ) -> Result[Json]: 442 """Replace a service using a javascript file or zip bundle. 443 444 :param mount: Service mount path (e.g "/_admin/aardvark"). 445 :type mount: str 446 :param filename: Full path to the javascript file or zip bundle. 447 :type filename: str 448 :param teardown: Run service teardown script. 449 :type teardown: bool | None 450 :param setup: Run service setup script. 451 :type setup: bool | None 452 :param legacy: Replace the service in 2.8 legacy compatibility mode. 453 :type legacy: bool | None 454 :param force: Force install if no service is found. 455 :type force: bool | None 456 :param config: Configuration values. 457 :type config: dict | None 458 :param dependencies: Dependency settings. 459 :type dependencies: dict | None 460 :return: Replaced service metadata. 461 :rtype: dict 462 :raise arango.exceptions.FoxxServiceReplaceError: If replace fails. 463 """ 464 params: Params = {"mount": mount} 465 if teardown is not None: 466 params["teardown"] = teardown 467 if setup is not None: 468 params["setup"] = setup 469 if legacy is not None: 470 params["legacy"] = legacy 471 if force is not None: 472 params["force"] = force 473 474 data = self._encode(filename, config, dependencies) 475 request = Request( 476 method="put", 477 endpoint="/_api/foxx/service", 478 params=params, 479 data=data, 480 headers={"content-type": data.content_type}, 481 ) 482 483 def response_handler(resp: Response) -> Json: 484 if resp.is_success: 485 return format_service_data(resp.body) 486 raise FoxxServiceReplaceError(resp, request) 487 488 return self._execute(request, response_handler) 489 490 def delete_service( 491 self, mount: str, teardown: Optional[bool] = None 492 ) -> Result[bool]: 493 """Uninstall a service. 494 495 :param mount: Service mount path (e.g "/_admin/aardvark"). 496 :type mount: str 497 :param teardown: Run service teardown script. 498 :type teardown: bool | None 499 :return: True if service was deleted successfully. 500 :rtype: bool 501 :raise arango.exceptions.FoxxServiceDeleteError: If delete fails. 502 """ 503 params: Params = {"mount": mount} 504 if teardown is not None: 505 params["teardown"] = teardown 506 507 request = Request(method="delete", endpoint="/_api/foxx/service", params=params) 508 509 def response_handler(resp: Response) -> bool: 510 if resp.is_success: 511 return True 512 raise FoxxServiceDeleteError(resp, request) 513 514 return self._execute(request, response_handler) 515 516 def config(self, mount: str) -> Result[Json]: 517 """Return service configuration. 518 519 :param mount: Service mount path (e.g "/_admin/aardvark"). 520 :type mount: str 521 :return: Configuration values. 522 :rtype: dict 523 :raise arango.exceptions.FoxxConfigGetError: If retrieval fails. 524 """ 525 request = Request( 526 method="get", 527 endpoint="/_api/foxx/configuration", 528 params={"mount": mount}, 529 ) 530 531 def response_handler(resp: Response) -> Json: 532 if resp.is_success: 533 return format_service_data(resp.body) 534 raise FoxxConfigGetError(resp, request) 535 536 return self._execute(request, response_handler) 537 538 def update_config(self, mount: str, config: Json) -> Result[Json]: 539 """Update service configuration. 540 541 :param mount: Service mount path (e.g "/_admin/aardvark"). 542 :type mount: str 543 :param config: Configuration values. Omitted options are ignored. 544 :type config: dict 545 :return: Updated configuration values. 546 :rtype: dict 547 :raise arango.exceptions.FoxxConfigUpdateError: If update fails. 548 """ 549 request = Request( 550 method="patch", 551 endpoint="/_api/foxx/configuration", 552 params={"mount": mount}, 553 data=config, 554 ) 555 556 def response_handler(resp: Response) -> Json: 557 if resp.is_success: 558 return format_service_data(resp.body) 559 raise FoxxConfigUpdateError(resp, request) 560 561 return self._execute(request, response_handler) 562 563 def replace_config(self, mount: str, config: Json) -> Result[Json]: 564 """Replace service configuration. 565 566 :param mount: Service mount path (e.g "/_admin/aardvark"). 567 :type mount: str 568 :param config: Configuration values. Omitted options are reset to their 569 default values or marked as un-configured. 570 :type config: dict 571 :return: Replaced configuration values. 572 :rtype: dict 573 :raise arango.exceptions.FoxxConfigReplaceError: If replace fails. 574 """ 575 request = Request( 576 method="put", 577 endpoint="/_api/foxx/configuration", 578 params={"mount": mount}, 579 data=config, 580 ) 581 582 def response_handler(resp: Response) -> Json: 583 if resp.is_success: 584 return format_service_data(resp.body) 585 raise FoxxConfigReplaceError(resp, request) 586 587 return self._execute(request, response_handler) 588 589 def dependencies(self, mount: str) -> Result[Json]: 590 """Return service dependencies. 591 592 :param mount: Service mount path (e.g "/_admin/aardvark"). 593 :type mount: str 594 :return: Dependency settings. 595 :rtype: dict 596 :raise arango.exceptions.FoxxDependencyGetError: If retrieval fails. 597 """ 598 request = Request( 599 method="get", 600 endpoint="/_api/foxx/dependencies", 601 params={"mount": mount}, 602 ) 603 604 def response_handler(resp: Response) -> Json: 605 if resp.is_success: 606 return format_service_data(resp.body) 607 raise FoxxDependencyGetError(resp, request) 608 609 return self._execute(request, response_handler) 610 611 def update_dependencies(self, mount: str, dependencies: Json) -> Result[Json]: 612 """Update service dependencies. 613 614 :param mount: Service mount path (e.g "/_admin/aardvark"). 615 :type mount: str 616 :param dependencies: Dependencies settings. Omitted ones are ignored. 617 :type dependencies: dict 618 :return: Updated dependency settings. 619 :rtype: dict 620 :raise arango.exceptions.FoxxDependencyUpdateError: If update fails. 621 """ 622 request = Request( 623 method="patch", 624 endpoint="/_api/foxx/dependencies", 625 params={"mount": mount}, 626 data=dependencies, 627 ) 628 629 def response_handler(resp: Response) -> Json: 630 if resp.is_success: 631 return format_service_data(resp.body) 632 raise FoxxDependencyUpdateError(resp, request) 633 634 return self._execute(request, response_handler) 635 636 def replace_dependencies(self, mount: str, dependencies: Json) -> Result[Json]: 637 """Replace service dependencies. 638 639 :param mount: Service mount path (e.g "/_admin/aardvark"). 640 :type mount: str 641 :param dependencies: Dependencies settings. Omitted ones are disabled. 642 :type dependencies: dict 643 :return: Replaced dependency settings. 644 :rtype: dict 645 :raise arango.exceptions.FoxxDependencyReplaceError: If replace fails. 646 """ 647 request = Request( 648 method="put", 649 endpoint="/_api/foxx/dependencies", 650 params={"mount": mount}, 651 data=dependencies, 652 ) 653 654 def response_handler(resp: Response) -> Json: 655 if resp.is_success: 656 return format_service_data(resp.body) 657 raise FoxxDependencyReplaceError(resp, request) 658 659 return self._execute(request, response_handler) 660 661 def enable_development(self, mount: str) -> Result[Json]: 662 """Put the service into development mode. 663 664 While the service is running in development mode, it is reloaded from 665 the file system, and its setup script (if any) is re-executed every 666 time the service handles a request. 667 668 In a cluster with multiple coordinators, changes to the filesystem on 669 one coordinator is not reflected across other coordinators. 670 671 :param mount: Service mount path (e.g "/_admin/aardvark"). 672 :type mount: str 673 :return: Service metadata. 674 :rtype: dict 675 :raise arango.exceptions.FoxxDevModeEnableError: If operation fails. 676 """ 677 request = Request( 678 method="post", 679 endpoint="/_api/foxx/development", 680 params={"mount": mount}, 681 ) 682 683 def response_handler(resp: Response) -> Json: 684 if resp.is_success: 685 return format_service_data(resp.body) 686 raise FoxxDevModeEnableError(resp, request) 687 688 return self._execute(request, response_handler) 689 690 def disable_development(self, mount: str) -> Result[Json]: 691 """Put the service into production mode. 692 693 In a cluster with multiple coordinators, the services on all other 694 coordinators are replaced with the version on the calling coordinator. 695 696 :param mount: Service mount path (e.g "/_admin/aardvark"). 697 :type mount: str 698 :return: Service metadata. 699 :rtype: dict 700 :raise arango.exceptions.FoxxDevModeDisableError: If operation fails. 701 """ 702 request = Request( 703 method="delete", 704 endpoint="/_api/foxx/development", 705 params={"mount": mount}, 706 ) 707 708 def response_handler(resp: Response) -> Json: 709 if resp.is_success: 710 return format_service_data(resp.body) 711 raise FoxxDevModeDisableError(resp, request) 712 713 return self._execute(request, response_handler) 714 715 def readme(self, mount: str) -> Result[str]: 716 """Return the service readme. 717 718 :param mount: Service mount path (e.g "/_admin/aardvark"). 719 :type mount: str 720 :return: Service readme. 721 :rtype: str 722 :raise arango.exceptions.FoxxReadmeGetError: If retrieval fails. 723 """ 724 request = Request( 725 method="get", 726 endpoint="/_api/foxx/readme", 727 params={"mount": mount}, 728 ) 729 730 def response_handler(resp: Response) -> str: 731 if resp.is_success: 732 return resp.raw_body 733 raise FoxxReadmeGetError(resp, request) 734 735 return self._execute(request, response_handler) 736 737 def swagger(self, mount: str) -> Result[Json]: 738 """Return the Swagger API description for the given service. 739 740 :param mount: Service mount path (e.g "/_admin/aardvark"). 741 :type mount: str 742 :return: Swagger API description. 743 :rtype: dict 744 :raise arango.exceptions.FoxxSwaggerGetError: If retrieval fails. 745 """ 746 request = Request( 747 method="get", endpoint="/_api/foxx/swagger", params={"mount": mount} 748 ) 749 750 def response_handler(resp: Response) -> Json: 751 if not resp.is_success: 752 raise FoxxSwaggerGetError(resp, request) 753 754 result: Json = resp.body 755 if "basePath" in result: 756 result["base_path"] = result.pop("basePath") 757 return result 758 759 return self._execute(request, response_handler) 760 761 def download(self, mount: str) -> Result[str]: 762 """Download service bundle. 763 764 When development mode is enabled, a new bundle is created every time. 765 Otherwise, the bundle represents the version of the service installed 766 on the server. 767 768 :param mount: Service mount path (e.g "/_admin/aardvark"). 769 :type mount: str 770 :return: Service bundle in raw string form. 771 :rtype: str 772 :raise arango.exceptions.FoxxDownloadError: If download fails. 773 """ 774 request = Request( 775 method="post", endpoint="/_api/foxx/download", params={"mount": mount} 776 ) 777 778 def response_handler(resp: Response) -> str: 779 if resp.is_success: 780 return resp.raw_body 781 raise FoxxDownloadError(resp, request) 782 783 return self._execute(request, response_handler) 784 785 def commit(self, replace: Optional[bool] = None) -> Result[bool]: 786 """Commit local service state of the coordinator to the database. 787 788 This can be used to resolve service conflicts between coordinators 789 that cannot be fixed automatically due to missing data. 790 791 :param replace: Overwrite any existing service files in database. 792 :type replace: bool | None 793 :return: True if the state was committed successfully. 794 :rtype: bool 795 :raise arango.exceptions.FoxxCommitError: If commit fails. 796 """ 797 params: Params = {} 798 if replace is not None: 799 params["replace"] = replace 800 801 request = Request(method="post", endpoint="/_api/foxx/commit", params=params) 802 803 def response_handler(resp: Response) -> bool: 804 if resp.is_success: 805 return True 806 raise FoxxCommitError(resp, request) 807 808 return self._execute(request, response_handler) 809 810 def scripts(self, mount: str) -> Result[Json]: 811 """List service scripts. 812 813 :param mount: Service mount path (e.g "/_admin/aardvark"). 814 :type mount: str 815 :return: Service scripts. 816 :rtype: dict 817 :raise arango.exceptions.FoxxScriptListError: If retrieval fails. 818 """ 819 request = Request( 820 method="get", 821 endpoint="/_api/foxx/scripts", 822 params={"mount": mount}, 823 ) 824 825 def response_handler(resp: Response) -> Json: 826 if resp.is_success: 827 return format_service_data(resp.body) 828 raise FoxxScriptListError(resp, request) 829 830 return self._execute(request, response_handler) 831 832 def run_script(self, mount: str, name: str, arg: Any = None) -> Result[Any]: 833 """Run a service script. 834 835 :param mount: Service mount path (e.g "/_admin/aardvark"). 836 :type mount: str 837 :param name: Script name. 838 :type name: str 839 :param arg: Arbitrary value passed into the script as first argument. 840 :type arg: Any 841 :return: Result of the script, if any. 842 :rtype: Any 843 :raise arango.exceptions.FoxxScriptRunError: If script fails. 844 """ 845 request = Request( 846 method="post", 847 endpoint=f"/_api/foxx/scripts/{name}", 848 params={"mount": mount}, 849 data=arg, 850 ) 851 852 def response_handler(resp: Response) -> Any: 853 if resp.is_success: 854 return resp.body 855 raise FoxxScriptRunError(resp, request) 856 857 return self._execute(request, response_handler) 858 859 def run_tests( 860 self, 861 mount: str, 862 reporter: str = "default", 863 idiomatic: Optional[bool] = None, 864 output_format: Optional[str] = None, 865 name_filter: Optional[str] = None, 866 ) -> Result[str]: 867 """Run service tests. 868 869 :param mount: Service mount path (e.g "/_admin/aardvark"). 870 :type mount: str 871 :param reporter: Test reporter. Allowed values are "default" (simple 872 list of test cases), "suite" (object of test cases nested in 873 suites), "stream" (raw stream of test results), "xunit" (XUnit or 874 JUnit compatible structure), or "tap" (raw TAP compatible stream). 875 :type reporter: str 876 :param idiomatic: Use matching format for the reporter, regardless of 877 the value of parameter **output_format**. 878 :type: bool 879 :param output_format: Used to further control format. Allowed values 880 are "x-ldjson", "xml" and "text". When using "stream" reporter, 881 setting this to "x-ldjson" returns newline-delimited JSON stream. 882 When using "tap" reporter, setting this to "text" returns plain 883 text TAP report. When using "xunit" reporter, settings this to 884 "xml" returns an XML instead of JSONML. 885 :type output_format: str 886 :param name_filter: Only run tests whose full name (test suite and 887 test case) matches the given string. 888 :type name_filter: str 889 :return: Reporter output (e.g. raw JSON string, XML, plain text). 890 :rtype: str 891 :raise arango.exceptions.FoxxTestRunError: If test fails. 892 """ 893 params: Params = {"mount": mount, "reporter": reporter} 894 if idiomatic is not None: 895 params["idiomatic"] = idiomatic 896 if name_filter is not None: 897 params["filter"] = name_filter 898 899 headers = {} 900 if output_format == "x-ldjson": 901 headers["Accept"] = "application/x-ldjson" 902 elif output_format == "xml": 903 headers["Accept"] = "application/xml" 904 elif output_format == "text": 905 headers["Accept"] = "text/plain" 906 907 request = Request( 908 method="post", endpoint="/_api/foxx/tests", params=params, headers=headers 909 ) 910 911 def response_handler(resp: Response) -> str: 912 if resp.is_success: 913 return resp.raw_body 914 raise FoxxTestRunError(resp, request) 915 916 return self._execute(request, response_handler) 917