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