loopix-sdk 2.30.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- loopix/__init__.py +260 -0
- loopix/api/__init__.py +287 -0
- loopix/api/client/__init__.py +8 -0
- loopix/api/client/api/__init__.py +1 -0
- loopix/api/client/api/sandboxes/__init__.py +1 -0
- loopix/api/client/api/sandboxes/delete_sandboxes_sandbox_id.py +161 -0
- loopix/api/client/api/sandboxes/get_sandboxes.py +176 -0
- loopix/api/client/api/sandboxes/get_sandboxes_metrics.py +173 -0
- loopix/api/client/api/sandboxes/get_sandboxes_sandbox_id.py +163 -0
- loopix/api/client/api/sandboxes/get_sandboxes_sandbox_id_logs.py +199 -0
- loopix/api/client/api/sandboxes/get_sandboxes_sandbox_id_metrics.py +212 -0
- loopix/api/client/api/sandboxes/get_v2_sandboxes.py +230 -0
- loopix/api/client/api/sandboxes/get_v_2_sandboxes_sandbox_id_logs.py +254 -0
- loopix/api/client/api/sandboxes/post_sandboxes.py +172 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_connect.py +193 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_pause.py +187 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_refreshes.py +181 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_resume.py +189 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_snapshots.py +195 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_timeout.py +193 -0
- loopix/api/client/api/sandboxes/put_sandboxes_sandbox_id_network.py +199 -0
- loopix/api/client/api/snapshots/__init__.py +1 -0
- loopix/api/client/api/snapshots/get_snapshots.py +202 -0
- loopix/api/client/api/tags/__init__.py +1 -0
- loopix/api/client/api/tags/delete_templates_tags.py +174 -0
- loopix/api/client/api/tags/get_templates_template_id_tags.py +172 -0
- loopix/api/client/api/tags/post_templates_tags.py +176 -0
- loopix/api/client/api/templates/__init__.py +1 -0
- loopix/api/client/api/templates/delete_templates_template_id.py +157 -0
- loopix/api/client/api/templates/get_templates.py +172 -0
- loopix/api/client/api/templates/get_templates_aliases_alias.py +167 -0
- loopix/api/client/api/templates/get_templates_template_id.py +195 -0
- loopix/api/client/api/templates/get_templates_template_id_builds_build_id_logs.py +272 -0
- loopix/api/client/api/templates/get_templates_template_id_builds_build_id_status.py +232 -0
- loopix/api/client/api/templates/get_templates_template_id_files_hash.py +180 -0
- loopix/api/client/api/templates/patch_templates_template_id.py +183 -0
- loopix/api/client/api/templates/patch_v_2_templates_template_id.py +185 -0
- loopix/api/client/api/templates/post_templates.py +172 -0
- loopix/api/client/api/templates/post_templates_template_id.py +181 -0
- loopix/api/client/api/templates/post_templates_template_id_builds_build_id.py +170 -0
- loopix/api/client/api/templates/post_v2_templates.py +172 -0
- loopix/api/client/api/templates/post_v3_templates.py +176 -0
- loopix/api/client/api/templates/post_v_2_templates_template_id_builds_build_id.py +192 -0
- loopix/api/client/api/volumes/__init__.py +1 -0
- loopix/api/client/api/volumes/delete_volumes_volume_id.py +161 -0
- loopix/api/client/api/volumes/get_volumes.py +140 -0
- loopix/api/client/api/volumes/get_volumes_volume_id.py +163 -0
- loopix/api/client/api/volumes/post_volumes.py +172 -0
- loopix/api/client/client.py +286 -0
- loopix/api/client/errors.py +16 -0
- loopix/api/client/models/__init__.py +185 -0
- loopix/api/client/models/admin_build_cancel_result.py +67 -0
- loopix/api/client/models/admin_sandbox_kill_result.py +67 -0
- loopix/api/client/models/assign_template_tags_request.py +67 -0
- loopix/api/client/models/assigned_template_tags.py +68 -0
- loopix/api/client/models/aws_registry.py +85 -0
- loopix/api/client/models/aws_registry_type.py +8 -0
- loopix/api/client/models/build_log_entry.py +89 -0
- loopix/api/client/models/build_status_reason.py +95 -0
- loopix/api/client/models/connect_sandbox.py +59 -0
- loopix/api/client/models/created_access_token.py +100 -0
- loopix/api/client/models/created_team_api_key.py +166 -0
- loopix/api/client/models/delete_template_tags_request.py +67 -0
- loopix/api/client/models/disk_metrics.py +91 -0
- loopix/api/client/models/error.py +67 -0
- loopix/api/client/models/gcp_registry.py +69 -0
- loopix/api/client/models/gcp_registry_type.py +8 -0
- loopix/api/client/models/general_registry.py +77 -0
- loopix/api/client/models/general_registry_type.py +8 -0
- loopix/api/client/models/identifier_masking_details.py +83 -0
- loopix/api/client/models/listed_sandbox.py +179 -0
- loopix/api/client/models/log_level.py +11 -0
- loopix/api/client/models/logs_direction.py +9 -0
- loopix/api/client/models/logs_source.py +9 -0
- loopix/api/client/models/machine_info.py +83 -0
- loopix/api/client/models/max_team_metric.py +78 -0
- loopix/api/client/models/mcp_type_0.py +44 -0
- loopix/api/client/models/new_access_token.py +59 -0
- loopix/api/client/models/new_sandbox.py +224 -0
- loopix/api/client/models/new_team_api_key.py +59 -0
- loopix/api/client/models/new_volume.py +59 -0
- loopix/api/client/models/node.py +160 -0
- loopix/api/client/models/node_detail.py +160 -0
- loopix/api/client/models/node_metrics.py +122 -0
- loopix/api/client/models/node_status.py +12 -0
- loopix/api/client/models/node_status_change.py +82 -0
- loopix/api/client/models/post_sandboxes_sandbox_id_refreshes_body.py +59 -0
- loopix/api/client/models/post_sandboxes_sandbox_id_snapshots_body.py +60 -0
- loopix/api/client/models/post_sandboxes_sandbox_id_timeout_body.py +59 -0
- loopix/api/client/models/resumed_sandbox.py +68 -0
- loopix/api/client/models/sandbox.py +145 -0
- loopix/api/client/models/sandbox_auto_resume_config.py +60 -0
- loopix/api/client/models/sandbox_detail.py +267 -0
- loopix/api/client/models/sandbox_lifecycle.py +70 -0
- loopix/api/client/models/sandbox_log.py +70 -0
- loopix/api/client/models/sandbox_log_entry.py +93 -0
- loopix/api/client/models/sandbox_log_entry_fields.py +44 -0
- loopix/api/client/models/sandbox_logs.py +91 -0
- loopix/api/client/models/sandbox_logs_v2_response.py +73 -0
- loopix/api/client/models/sandbox_metric.py +126 -0
- loopix/api/client/models/sandbox_network_config.py +118 -0
- loopix/api/client/models/sandbox_network_config_rules.py +72 -0
- loopix/api/client/models/sandbox_network_rule.py +74 -0
- loopix/api/client/models/sandbox_network_transform.py +79 -0
- loopix/api/client/models/sandbox_network_transform_headers.py +47 -0
- loopix/api/client/models/sandbox_network_update_config.py +114 -0
- loopix/api/client/models/sandbox_network_update_config_rules.py +71 -0
- loopix/api/client/models/sandbox_on_timeout.py +9 -0
- loopix/api/client/models/sandbox_pause_request.py +62 -0
- loopix/api/client/models/sandbox_state.py +9 -0
- loopix/api/client/models/sandbox_volume_mount.py +67 -0
- loopix/api/client/models/sandboxes_with_metrics.py +59 -0
- loopix/api/client/models/snapshot_info.py +70 -0
- loopix/api/client/models/team.py +83 -0
- loopix/api/client/models/team_api_key.py +158 -0
- loopix/api/client/models/team_metric.py +86 -0
- loopix/api/client/models/team_user.py +75 -0
- loopix/api/client/models/template.py +225 -0
- loopix/api/client/models/template_alias_response.py +67 -0
- loopix/api/client/models/template_build.py +139 -0
- loopix/api/client/models/template_build_file_upload.py +70 -0
- loopix/api/client/models/template_build_info.py +126 -0
- loopix/api/client/models/template_build_logs_response.py +73 -0
- loopix/api/client/models/template_build_request.py +115 -0
- loopix/api/client/models/template_build_request_v2.py +88 -0
- loopix/api/client/models/template_build_request_v3.py +107 -0
- loopix/api/client/models/template_build_start_v2.py +184 -0
- loopix/api/client/models/template_build_status.py +11 -0
- loopix/api/client/models/template_legacy.py +207 -0
- loopix/api/client/models/template_request_response_v3.py +99 -0
- loopix/api/client/models/template_step.py +91 -0
- loopix/api/client/models/template_tag.py +78 -0
- loopix/api/client/models/template_update_request.py +59 -0
- loopix/api/client/models/template_update_response.py +59 -0
- loopix/api/client/models/template_with_builds.py +156 -0
- loopix/api/client/models/update_team_api_key.py +59 -0
- loopix/api/client/models/volume.py +67 -0
- loopix/api/client/models/volume_and_token.py +75 -0
- loopix/api/client/models/volume_token.py +59 -0
- loopix/api/client/py.typed +1 -0
- loopix/api/client/types.py +54 -0
- loopix/api/client_async/__init__.py +74 -0
- loopix/api/client_sync/__init__.py +73 -0
- loopix/api/metadata.py +14 -0
- loopix/connection_config.py +309 -0
- loopix/envd/api.py +170 -0
- loopix/envd/filesystem/filesystem_connect.py +193 -0
- loopix/envd/filesystem/filesystem_pb2.py +80 -0
- loopix/envd/filesystem/filesystem_pb2.pyi +272 -0
- loopix/envd/process/process_connect.py +174 -0
- loopix/envd/process/process_pb2.py +96 -0
- loopix/envd/process/process_pb2.pyi +316 -0
- loopix/envd/rpc.py +139 -0
- loopix/envd/versions.py +11 -0
- loopix/exceptions.py +133 -0
- loopix/io_utils.py +57 -0
- loopix/paginator.py +52 -0
- loopix/py.typed +0 -0
- loopix/sandbox/_git/__init__.py +85 -0
- loopix/sandbox/_git/args.py +363 -0
- loopix/sandbox/_git/auth.py +132 -0
- loopix/sandbox/_git/config.py +32 -0
- loopix/sandbox/_git/parse.py +222 -0
- loopix/sandbox/_git/types.py +149 -0
- loopix/sandbox/commands/command_handle.py +69 -0
- loopix/sandbox/commands/main.py +39 -0
- loopix/sandbox/filesystem/filesystem.py +337 -0
- loopix/sandbox/filesystem/watch_handle.py +70 -0
- loopix/sandbox/main.py +227 -0
- loopix/sandbox/mcp.py +1949 -0
- loopix/sandbox/network.py +8 -0
- loopix/sandbox/sandbox_api.py +624 -0
- loopix/sandbox/signature.py +47 -0
- loopix/sandbox/utils.py +34 -0
- loopix/sandbox_async/commands/command.py +396 -0
- loopix/sandbox_async/commands/command_handle.py +298 -0
- loopix/sandbox_async/commands/pty.py +257 -0
- loopix/sandbox_async/filesystem/filesystem.py +720 -0
- loopix/sandbox_async/filesystem/watch_handle.py +97 -0
- loopix/sandbox_async/git.py +1100 -0
- loopix/sandbox_async/main.py +987 -0
- loopix/sandbox_async/paginator.py +140 -0
- loopix/sandbox_async/sandbox_api.py +504 -0
- loopix/sandbox_async/utils.py +7 -0
- loopix/sandbox_domains.py +5 -0
- loopix/sandbox_sync/commands/command.py +420 -0
- loopix/sandbox_sync/commands/command_handle.py +239 -0
- loopix/sandbox_sync/commands/pty.py +279 -0
- loopix/sandbox_sync/filesystem/filesystem.py +710 -0
- loopix/sandbox_sync/filesystem/watch_handle.py +102 -0
- loopix/sandbox_sync/git.py +1077 -0
- loopix/sandbox_sync/main.py +975 -0
- loopix/sandbox_sync/paginator.py +140 -0
- loopix/sandbox_sync/sandbox_api.py +491 -0
- loopix/template/consts.py +45 -0
- loopix/template/dockerfile_parser.py +286 -0
- loopix/template/logger.py +232 -0
- loopix/template/main.py +1368 -0
- loopix/template/readycmd.py +144 -0
- loopix/template/types.py +194 -0
- loopix/template/utils.py +426 -0
- loopix/template_async/build_api.py +419 -0
- loopix/template_async/main.py +528 -0
- loopix/template_sync/build_api.py +409 -0
- loopix/template_sync/main.py +529 -0
- loopix/volume/client/__init__.py +8 -0
- loopix/volume/client/api/__init__.py +1 -0
- loopix/volume/client/api/volumes/__init__.py +1 -0
- loopix/volume/client/api/volumes/delete_volumecontent_volume_id_path.py +174 -0
- loopix/volume/client/api/volumes/get_volumecontent_volume_id_dir.py +204 -0
- loopix/volume/client/api/volumes/get_volumecontent_volume_id_file.py +179 -0
- loopix/volume/client/api/volumes/get_volumecontent_volume_id_path.py +176 -0
- loopix/volume/client/api/volumes/patch_volumecontent_volume_id_path.py +203 -0
- loopix/volume/client/api/volumes/post_volumecontent_volume_id_dir.py +239 -0
- loopix/volume/client/api/volumes/put_volumecontent_volume_id_file.py +259 -0
- loopix/volume/client/client.py +286 -0
- loopix/volume/client/errors.py +16 -0
- loopix/volume/client/models/__init__.py +13 -0
- loopix/volume/client/models/error.py +67 -0
- loopix/volume/client/models/patch_volumecontent_volume_id_path_body.py +77 -0
- loopix/volume/client/models/volume_entry_stat.py +145 -0
- loopix/volume/client/models/volume_entry_stat_type.py +11 -0
- loopix/volume/client/py.typed +1 -0
- loopix/volume/client/types.py +54 -0
- loopix/volume/client_async/__init__.py +88 -0
- loopix/volume/client_sync/__init__.py +80 -0
- loopix/volume/connection_config.py +145 -0
- loopix/volume/types.py +62 -0
- loopix/volume/utils.py +52 -0
- loopix/volume/volume_async.py +639 -0
- loopix/volume/volume_sync.py +639 -0
- loopix_connect/__init__.py +1 -0
- loopix_connect/client.py +534 -0
- loopix_connect/py.typed +0 -0
- loopix_sdk-2.30.0.dist-info/METADATA +98 -0
- loopix_sdk-2.30.0.dist-info/RECORD +238 -0
- loopix_sdk-2.30.0.dist-info/WHEEL +4 -0
- loopix_sdk-2.30.0.dist-info/licenses/LICENSE +9 -0
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
from typing import IO, Dict, List, Literal, Optional, Union, overload
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
from packaging.version import Version
|
|
6
|
+
|
|
7
|
+
import loopix_connect
|
|
8
|
+
from loopix.api import make_logging_event_hooks
|
|
9
|
+
from loopix.api.client_sync import get_envd_transport
|
|
10
|
+
from loopix.connection_config import (
|
|
11
|
+
KEEPALIVE_PING_HEADER,
|
|
12
|
+
KEEPALIVE_PING_INTERVAL_SEC,
|
|
13
|
+
ConnectionConfig,
|
|
14
|
+
Username,
|
|
15
|
+
default_username,
|
|
16
|
+
)
|
|
17
|
+
from loopix_connect.client import Code
|
|
18
|
+
|
|
19
|
+
from loopix.envd.api import (
|
|
20
|
+
ENVD_API_FILES_ROUTE,
|
|
21
|
+
check_sandbox_health,
|
|
22
|
+
handle_envd_api_exception,
|
|
23
|
+
handle_envd_api_transport_exception_with_health,
|
|
24
|
+
)
|
|
25
|
+
from loopix.envd.filesystem import filesystem_connect, filesystem_pb2
|
|
26
|
+
from loopix.envd.rpc import authentication_header, handle_rpc_exception_with_health
|
|
27
|
+
from loopix.envd.versions import (
|
|
28
|
+
ENVD_DEFAULT_USER,
|
|
29
|
+
ENVD_FILE_METADATA,
|
|
30
|
+
ENVD_OCTET_STREAM_UPLOAD,
|
|
31
|
+
ENVD_VERSION_FS_EVENT_ENTRY_INFO,
|
|
32
|
+
ENVD_VERSION_RECURSIVE_WATCH,
|
|
33
|
+
ENVD_VERSION_WATCH_NETWORK_MOUNTS,
|
|
34
|
+
)
|
|
35
|
+
from loopix.exceptions import (
|
|
36
|
+
FileNotFoundException,
|
|
37
|
+
InvalidArgumentException,
|
|
38
|
+
SandboxException,
|
|
39
|
+
TemplateException,
|
|
40
|
+
)
|
|
41
|
+
from loopix.sandbox.filesystem.filesystem import (
|
|
42
|
+
EntryInfo,
|
|
43
|
+
FileStreamReader,
|
|
44
|
+
WriteEntry,
|
|
45
|
+
WriteInfo,
|
|
46
|
+
_to_httpx_file,
|
|
47
|
+
map_entry_info,
|
|
48
|
+
map_file_type,
|
|
49
|
+
metadata_to_headers,
|
|
50
|
+
to_upload_body,
|
|
51
|
+
validate_metadata,
|
|
52
|
+
)
|
|
53
|
+
from loopix.sandbox_sync.filesystem.watch_handle import WatchHandle
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
_FILESYSTEM_RPC_ERROR_MAP = {
|
|
57
|
+
Code.not_found: FileNotFoundException,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
_FILESYSTEM_HTTP_ERROR_MAP = {
|
|
61
|
+
404: FileNotFoundException,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _handle_filesystem_rpc_exception(e: Exception, envd_api: httpx.Client) -> Exception:
|
|
66
|
+
return handle_rpc_exception_with_health(
|
|
67
|
+
e, lambda: check_sandbox_health(envd_api), _FILESYSTEM_RPC_ERROR_MAP
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _handle_filesystem_envd_api_exception(r):
|
|
72
|
+
return handle_envd_api_exception(r, _FILESYSTEM_HTTP_ERROR_MAP)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class Filesystem:
|
|
76
|
+
"""
|
|
77
|
+
Module for interacting with the filesystem in the sandbox.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
envd_api_url: str,
|
|
83
|
+
envd_version: Version,
|
|
84
|
+
connection_config: ConnectionConfig,
|
|
85
|
+
) -> None:
|
|
86
|
+
self._envd_api_url = envd_api_url
|
|
87
|
+
self._envd_version = envd_version
|
|
88
|
+
self._connection_config = connection_config
|
|
89
|
+
self._thread_local = threading.local()
|
|
90
|
+
|
|
91
|
+
def _create_envd_api(self) -> httpx.Client:
|
|
92
|
+
transport = get_envd_transport(self._connection_config)
|
|
93
|
+
return httpx.Client(
|
|
94
|
+
base_url=self._envd_api_url,
|
|
95
|
+
transport=transport,
|
|
96
|
+
headers=self._connection_config.sandbox_headers,
|
|
97
|
+
event_hooks=make_logging_event_hooks(self._connection_config.logger),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def _create_rpc(self) -> filesystem_connect.FilesystemClient:
|
|
101
|
+
transport = get_envd_transport(self._connection_config)
|
|
102
|
+
return filesystem_connect.FilesystemClient(
|
|
103
|
+
self._envd_api_url,
|
|
104
|
+
# TODO: Fix and enable compression again — the headers compression is not solved for streaming.
|
|
105
|
+
# compressor=loopix_connect.GzipCompressor,
|
|
106
|
+
pool=transport.pool,
|
|
107
|
+
json=True,
|
|
108
|
+
headers=self._connection_config.sandbox_headers,
|
|
109
|
+
logger=self._connection_config.logger,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def _envd_api(self) -> httpx.Client:
|
|
114
|
+
envd_api = getattr(self._thread_local, "envd_api", None)
|
|
115
|
+
if envd_api is None:
|
|
116
|
+
envd_api = self._create_envd_api()
|
|
117
|
+
self._thread_local.envd_api = envd_api
|
|
118
|
+
return envd_api
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def _rpc(self) -> filesystem_connect.FilesystemClient:
|
|
122
|
+
rpc = getattr(self._thread_local, "rpc", None)
|
|
123
|
+
if rpc is None:
|
|
124
|
+
rpc = self._create_rpc()
|
|
125
|
+
self._thread_local.rpc = rpc
|
|
126
|
+
return rpc
|
|
127
|
+
|
|
128
|
+
@overload
|
|
129
|
+
def read(
|
|
130
|
+
self,
|
|
131
|
+
path: str,
|
|
132
|
+
format: Literal["text"] = "text",
|
|
133
|
+
user: Optional[Username] = None,
|
|
134
|
+
request_timeout: Optional[float] = None,
|
|
135
|
+
gzip: bool = False,
|
|
136
|
+
) -> str:
|
|
137
|
+
"""
|
|
138
|
+
Read file content as a `str`.
|
|
139
|
+
|
|
140
|
+
:param path: Path to the file
|
|
141
|
+
:param user: Run the operation as this user
|
|
142
|
+
:param format: Format of the file content—`text` by default
|
|
143
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
144
|
+
:param gzip: Use gzip compression for the request
|
|
145
|
+
|
|
146
|
+
:return: File content as a `str`
|
|
147
|
+
"""
|
|
148
|
+
...
|
|
149
|
+
|
|
150
|
+
@overload
|
|
151
|
+
def read(
|
|
152
|
+
self,
|
|
153
|
+
path: str,
|
|
154
|
+
format: Literal["bytes"],
|
|
155
|
+
user: Optional[Username] = None,
|
|
156
|
+
request_timeout: Optional[float] = None,
|
|
157
|
+
gzip: bool = False,
|
|
158
|
+
) -> bytearray:
|
|
159
|
+
"""
|
|
160
|
+
Read file content as a `bytearray`.
|
|
161
|
+
|
|
162
|
+
:param path: Path to the file
|
|
163
|
+
:param user: Run the operation as this user
|
|
164
|
+
:param format: Format of the file content—`bytes`
|
|
165
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
166
|
+
:param gzip: Use gzip compression for the request
|
|
167
|
+
|
|
168
|
+
:return: File content as a `bytearray`
|
|
169
|
+
"""
|
|
170
|
+
...
|
|
171
|
+
|
|
172
|
+
@overload
|
|
173
|
+
def read(
|
|
174
|
+
self,
|
|
175
|
+
path: str,
|
|
176
|
+
format: Literal["stream"],
|
|
177
|
+
user: Optional[Username] = None,
|
|
178
|
+
request_timeout: Optional[float] = None,
|
|
179
|
+
gzip: bool = False,
|
|
180
|
+
stream_idle_timeout: Optional[float] = None,
|
|
181
|
+
) -> FileStreamReader:
|
|
182
|
+
"""
|
|
183
|
+
Read file content as a `FileStreamReader` (an `Iterator[bytes]`).
|
|
184
|
+
|
|
185
|
+
The request timeout bounds only the initial handshake—the returned
|
|
186
|
+
iterator is not killed by it while being consumed. A stalled stream is
|
|
187
|
+
reclaimed by `stream_idle_timeout` (raising `httpx.ReadTimeout`). The
|
|
188
|
+
reader releases its connection once fully consumed; if you don't read it
|
|
189
|
+
to the end, use it as a context manager or call `close()` for
|
|
190
|
+
deterministic cleanup.
|
|
191
|
+
|
|
192
|
+
:param path: Path to the file
|
|
193
|
+
:param user: Run the operation as this user
|
|
194
|
+
:param format: Format of the file content—`stream`
|
|
195
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
196
|
+
:param gzip: Use gzip compression for the request
|
|
197
|
+
:param stream_idle_timeout: Idle timeout in **seconds** for the streamed
|
|
198
|
+
body—abort if no chunk arrives within this window. Resets on every
|
|
199
|
+
chunk, so it bounds a stalled stream without limiting total transfer
|
|
200
|
+
time. Defaults to the request timeout; pass `0` to disable.
|
|
201
|
+
|
|
202
|
+
:return: File content as a `FileStreamReader`
|
|
203
|
+
"""
|
|
204
|
+
...
|
|
205
|
+
|
|
206
|
+
def read(
|
|
207
|
+
self,
|
|
208
|
+
path: str,
|
|
209
|
+
format: Literal["text", "bytes", "stream"] = "text",
|
|
210
|
+
user: Optional[Username] = None,
|
|
211
|
+
request_timeout: Optional[float] = None,
|
|
212
|
+
gzip: bool = False,
|
|
213
|
+
stream_idle_timeout: Optional[float] = None,
|
|
214
|
+
):
|
|
215
|
+
username = user
|
|
216
|
+
if username is None and self._envd_version < ENVD_DEFAULT_USER:
|
|
217
|
+
username = default_username
|
|
218
|
+
|
|
219
|
+
params = {"path": path}
|
|
220
|
+
if username:
|
|
221
|
+
params["username"] = username
|
|
222
|
+
|
|
223
|
+
headers = {}
|
|
224
|
+
if gzip:
|
|
225
|
+
headers["Accept-Encoding"] = "gzip"
|
|
226
|
+
|
|
227
|
+
timeout = self._connection_config.get_request_timeout(request_timeout)
|
|
228
|
+
|
|
229
|
+
if format == "stream":
|
|
230
|
+
# Stream the response body instead of buffering it in memory.
|
|
231
|
+
request = self._envd_api.build_request(
|
|
232
|
+
"GET",
|
|
233
|
+
ENVD_API_FILES_ROUTE,
|
|
234
|
+
params=params,
|
|
235
|
+
headers=headers,
|
|
236
|
+
timeout=timeout,
|
|
237
|
+
)
|
|
238
|
+
try:
|
|
239
|
+
r = self._envd_api.send(request, stream=True)
|
|
240
|
+
except httpx.RemoteProtocolError as e:
|
|
241
|
+
raise handle_envd_api_transport_exception_with_health(e, self._envd_api)
|
|
242
|
+
|
|
243
|
+
err = _handle_filesystem_envd_api_exception(r)
|
|
244
|
+
if err:
|
|
245
|
+
r.close()
|
|
246
|
+
raise err
|
|
247
|
+
|
|
248
|
+
# The request timeout bounds only the initial handshake; httpx's
|
|
249
|
+
# per-chunk `read` timeout becomes the idle-read timeout for the body
|
|
250
|
+
# (defaults to the request timeout). The timeout dict is shared by
|
|
251
|
+
# reference with the transport and read again when iteration starts.
|
|
252
|
+
idle_timeout = (
|
|
253
|
+
timeout if stream_idle_timeout is None else stream_idle_timeout
|
|
254
|
+
)
|
|
255
|
+
request.extensions.get("timeout", {})["read"] = idle_timeout or None
|
|
256
|
+
|
|
257
|
+
return FileStreamReader(r)
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
r = self._envd_api.get(
|
|
261
|
+
ENVD_API_FILES_ROUTE,
|
|
262
|
+
params=params,
|
|
263
|
+
headers=headers,
|
|
264
|
+
timeout=timeout,
|
|
265
|
+
)
|
|
266
|
+
except httpx.RemoteProtocolError as e:
|
|
267
|
+
raise handle_envd_api_transport_exception_with_health(e, self._envd_api)
|
|
268
|
+
|
|
269
|
+
err = _handle_filesystem_envd_api_exception(r)
|
|
270
|
+
if err:
|
|
271
|
+
raise err
|
|
272
|
+
|
|
273
|
+
if format == "text":
|
|
274
|
+
return r.text
|
|
275
|
+
elif format == "bytes":
|
|
276
|
+
return bytearray(r.content)
|
|
277
|
+
|
|
278
|
+
def write(
|
|
279
|
+
self,
|
|
280
|
+
path: str,
|
|
281
|
+
data: Union[str, bytes, IO],
|
|
282
|
+
user: Optional[Username] = None,
|
|
283
|
+
request_timeout: Optional[float] = None,
|
|
284
|
+
gzip: bool = False,
|
|
285
|
+
use_octet_stream: Optional[bool] = None,
|
|
286
|
+
metadata: Optional[Dict[str, str]] = None,
|
|
287
|
+
) -> WriteInfo:
|
|
288
|
+
"""
|
|
289
|
+
Write content to a file on the path.
|
|
290
|
+
Writing to a file that doesn't exist creates the file.
|
|
291
|
+
Writing to a file that already exists overwrites the file.
|
|
292
|
+
Writing to a file at path that doesn't exist creates the necessary directories.
|
|
293
|
+
|
|
294
|
+
:param path: Path to the file
|
|
295
|
+
:param data: Data to write to the file, can be a `str`, `bytes`, or `IO`. File-like objects are streamed in chunks instead of being buffered in memory.
|
|
296
|
+
:param user: Run the operation as this user
|
|
297
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
298
|
+
:param gzip: Use gzip compression for the upload. Implies the `application/octet-stream` upload. Requires envd 0.5.7 or later — when not supported, the upload falls back to uncompressed `multipart/form-data`.
|
|
299
|
+
:param use_octet_stream: Upload using `application/octet-stream` instead of `multipart/form-data`. Defaults to `None`, which uses octet-stream when `data` is a file-like object (so streamed uploads aren't buffered) and `multipart/form-data` otherwise. Requires envd 0.5.7 or later — when not supported, the upload falls back to `multipart/form-data`.
|
|
300
|
+
:param metadata: User-defined metadata to persist on the uploaded file as extended attributes. Keys are lowercased by the sandbox; invalid keys or values raise an `InvalidArgumentException`. Requires envd 0.6.2 or later.
|
|
301
|
+
|
|
302
|
+
:return: Information about the written file
|
|
303
|
+
"""
|
|
304
|
+
result = self.write_files(
|
|
305
|
+
[WriteEntry(path=path, data=data)],
|
|
306
|
+
user=user,
|
|
307
|
+
request_timeout=request_timeout,
|
|
308
|
+
gzip=gzip,
|
|
309
|
+
use_octet_stream=use_octet_stream,
|
|
310
|
+
metadata=metadata,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
if len(result) != 1:
|
|
314
|
+
raise SandboxException("Received unexpected response from write operation")
|
|
315
|
+
|
|
316
|
+
return result[0]
|
|
317
|
+
|
|
318
|
+
def write_files(
|
|
319
|
+
self,
|
|
320
|
+
files: List[WriteEntry],
|
|
321
|
+
user: Optional[Username] = None,
|
|
322
|
+
request_timeout: Optional[float] = None,
|
|
323
|
+
gzip: bool = False,
|
|
324
|
+
use_octet_stream: Optional[bool] = None,
|
|
325
|
+
metadata: Optional[Dict[str, str]] = None,
|
|
326
|
+
) -> List[WriteInfo]:
|
|
327
|
+
"""
|
|
328
|
+
Writes multiple files.
|
|
329
|
+
|
|
330
|
+
Writes a list of files to the filesystem.
|
|
331
|
+
When writing to a file that doesn't exist, the file will get created.
|
|
332
|
+
When writing to a file that already exists, the file will get overwritten.
|
|
333
|
+
When writing to a file at path that doesn't exist, the necessary directories will be created.
|
|
334
|
+
|
|
335
|
+
:param files: list of files to write as `WriteEntry` objects, each containing `path` and `data`
|
|
336
|
+
:param user: Run the operation as this user
|
|
337
|
+
:param request_timeout: Timeout for the request
|
|
338
|
+
:param gzip: Use gzip compression for the upload. Implies the `application/octet-stream` upload. Requires envd 0.5.7 or later — when not supported, the upload falls back to uncompressed `multipart/form-data`.
|
|
339
|
+
:param use_octet_stream: Upload using `application/octet-stream` instead of `multipart/form-data`. Defaults to `None`, which uses octet-stream when any entry is a file-like object (so streamed uploads aren't buffered) and `multipart/form-data` otherwise. Requires envd 0.5.7 or later — when not supported, the upload falls back to `multipart/form-data`.
|
|
340
|
+
:param metadata: User-defined metadata to persist on each uploaded file as extended attributes; the same map is applied to every file. Keys are lowercased by the sandbox; invalid keys or values raise an `InvalidArgumentException`. Requires envd 0.6.2 or later.
|
|
341
|
+
:return: Information about the written files
|
|
342
|
+
"""
|
|
343
|
+
username = user
|
|
344
|
+
if username is None and self._envd_version < ENVD_DEFAULT_USER:
|
|
345
|
+
username = default_username
|
|
346
|
+
|
|
347
|
+
if len(files) == 0:
|
|
348
|
+
return []
|
|
349
|
+
|
|
350
|
+
validate_metadata(metadata)
|
|
351
|
+
|
|
352
|
+
if metadata and self._envd_version < ENVD_FILE_METADATA:
|
|
353
|
+
raise TemplateException("File metadata requires envd 0.6.2 or later.")
|
|
354
|
+
|
|
355
|
+
# A file-like entry is streamed; str/bytes are sent from memory.
|
|
356
|
+
has_streamable_data = any(
|
|
357
|
+
not isinstance(file["data"], (str, bytes)) for file in files
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
if use_octet_stream is None:
|
|
361
|
+
# Streaming an upload only happens on the octet-stream path; the
|
|
362
|
+
# multipart path buffers file-like data. Default to octet-stream
|
|
363
|
+
# when any entry is a file-like object so a streamed upload isn't
|
|
364
|
+
# silently buffered.
|
|
365
|
+
use_octet_stream = has_streamable_data
|
|
366
|
+
|
|
367
|
+
supports_octet_stream = self._envd_version >= ENVD_OCTET_STREAM_UPLOAD
|
|
368
|
+
# Gzip compression only works with the octet-stream upload (the
|
|
369
|
+
# Content-Encoding header applies to the whole request body), so
|
|
370
|
+
# requesting gzip implies it when envd supports it.
|
|
371
|
+
use_octet_stream = (use_octet_stream or gzip) and supports_octet_stream
|
|
372
|
+
|
|
373
|
+
# Each chunk send is bounded by the request timeout (httpx applies it
|
|
374
|
+
# per write); a stalled upload the per-write timeout can't observe is
|
|
375
|
+
# bounded server-side (envd's per-read idle timeout, envd >= 0.6.7).
|
|
376
|
+
upload_timeout = self._connection_config.get_request_timeout(request_timeout)
|
|
377
|
+
|
|
378
|
+
# Metadata is sent as request-scoped X-Metadata-* headers, so the same
|
|
379
|
+
# metadata is applied to every file in a multi-file upload.
|
|
380
|
+
extra_headers = metadata_to_headers(metadata)
|
|
381
|
+
|
|
382
|
+
results: List[WriteInfo] = []
|
|
383
|
+
|
|
384
|
+
if use_octet_stream:
|
|
385
|
+
for file in files:
|
|
386
|
+
file_path, file_data = file["path"], file["data"]
|
|
387
|
+
|
|
388
|
+
params = {"path": file_path}
|
|
389
|
+
if username:
|
|
390
|
+
params["username"] = username
|
|
391
|
+
|
|
392
|
+
headers = {"Content-Type": "application/octet-stream", **extra_headers}
|
|
393
|
+
if gzip:
|
|
394
|
+
headers["Content-Encoding"] = "gzip"
|
|
395
|
+
|
|
396
|
+
try:
|
|
397
|
+
r = self._envd_api.post(
|
|
398
|
+
ENVD_API_FILES_ROUTE,
|
|
399
|
+
content=to_upload_body(file_data, gzip),
|
|
400
|
+
headers=headers,
|
|
401
|
+
params=params,
|
|
402
|
+
timeout=upload_timeout,
|
|
403
|
+
)
|
|
404
|
+
except httpx.RemoteProtocolError as e:
|
|
405
|
+
raise handle_envd_api_transport_exception_with_health(
|
|
406
|
+
e, self._envd_api
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
err = _handle_filesystem_envd_api_exception(r)
|
|
410
|
+
if err:
|
|
411
|
+
raise err
|
|
412
|
+
|
|
413
|
+
write_result = r.json()
|
|
414
|
+
|
|
415
|
+
if not isinstance(write_result, list) or len(write_result) == 0:
|
|
416
|
+
raise SandboxException(
|
|
417
|
+
"Expected to receive information about written file"
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
results.extend([WriteInfo.from_dict(f) for f in write_result])
|
|
421
|
+
else:
|
|
422
|
+
params = {}
|
|
423
|
+
if username:
|
|
424
|
+
params["username"] = username
|
|
425
|
+
if len(files) == 1:
|
|
426
|
+
params["path"] = files[0]["path"]
|
|
427
|
+
|
|
428
|
+
httpx_files = [_to_httpx_file(file["path"], file["data"]) for file in files]
|
|
429
|
+
|
|
430
|
+
if len(httpx_files) == 0:
|
|
431
|
+
return []
|
|
432
|
+
|
|
433
|
+
try:
|
|
434
|
+
r = self._envd_api.post(
|
|
435
|
+
ENVD_API_FILES_ROUTE,
|
|
436
|
+
files=httpx_files,
|
|
437
|
+
params=params,
|
|
438
|
+
headers=extra_headers,
|
|
439
|
+
timeout=upload_timeout,
|
|
440
|
+
)
|
|
441
|
+
except httpx.RemoteProtocolError as e:
|
|
442
|
+
raise handle_envd_api_transport_exception_with_health(e, self._envd_api)
|
|
443
|
+
|
|
444
|
+
err = _handle_filesystem_envd_api_exception(r)
|
|
445
|
+
if err:
|
|
446
|
+
raise err
|
|
447
|
+
|
|
448
|
+
write_result = r.json()
|
|
449
|
+
|
|
450
|
+
if not isinstance(write_result, list) or len(write_result) == 0:
|
|
451
|
+
raise SandboxException(
|
|
452
|
+
"Expected to receive information about written file"
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
results.extend([WriteInfo.from_dict(f) for f in write_result])
|
|
456
|
+
|
|
457
|
+
return results
|
|
458
|
+
|
|
459
|
+
def list(
|
|
460
|
+
self,
|
|
461
|
+
path: str,
|
|
462
|
+
depth: Optional[int] = 1,
|
|
463
|
+
user: Optional[Username] = None,
|
|
464
|
+
request_timeout: Optional[float] = None,
|
|
465
|
+
) -> List[EntryInfo]:
|
|
466
|
+
"""
|
|
467
|
+
List entries in a directory.
|
|
468
|
+
|
|
469
|
+
:param path: Path to the directory
|
|
470
|
+
:param depth: Depth of the directory to list
|
|
471
|
+
:param user: Run the operation as this user
|
|
472
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
473
|
+
|
|
474
|
+
:return: List of entries in the directory
|
|
475
|
+
"""
|
|
476
|
+
if depth is not None and depth < 1:
|
|
477
|
+
raise InvalidArgumentException("depth should be at least 1")
|
|
478
|
+
|
|
479
|
+
try:
|
|
480
|
+
res = self._rpc.list_dir(
|
|
481
|
+
filesystem_pb2.ListDirRequest(path=path, depth=depth),
|
|
482
|
+
request_timeout=self._connection_config.get_request_timeout(
|
|
483
|
+
request_timeout
|
|
484
|
+
),
|
|
485
|
+
headers=authentication_header(self._envd_version, user),
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
entries: List[EntryInfo] = []
|
|
489
|
+
for entry in res.entries:
|
|
490
|
+
# Skip entries with an unknown file type.
|
|
491
|
+
if map_file_type(entry.type):
|
|
492
|
+
entries.append(map_entry_info(entry))
|
|
493
|
+
|
|
494
|
+
return entries
|
|
495
|
+
except Exception as e:
|
|
496
|
+
raise _handle_filesystem_rpc_exception(e, self._envd_api)
|
|
497
|
+
|
|
498
|
+
def exists(
|
|
499
|
+
self,
|
|
500
|
+
path: str,
|
|
501
|
+
user: Optional[Username] = None,
|
|
502
|
+
request_timeout: Optional[float] = None,
|
|
503
|
+
) -> bool:
|
|
504
|
+
"""
|
|
505
|
+
Check if a file or a directory exists.
|
|
506
|
+
|
|
507
|
+
:param path: Path to a file or a directory
|
|
508
|
+
:param user: Run the operation as this user
|
|
509
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
510
|
+
|
|
511
|
+
:return: `True` if the file or directory exists, `False` otherwise
|
|
512
|
+
"""
|
|
513
|
+
try:
|
|
514
|
+
self._rpc.stat(
|
|
515
|
+
filesystem_pb2.StatRequest(path=path),
|
|
516
|
+
request_timeout=self._connection_config.get_request_timeout(
|
|
517
|
+
request_timeout
|
|
518
|
+
),
|
|
519
|
+
headers=authentication_header(self._envd_version, user),
|
|
520
|
+
)
|
|
521
|
+
return True
|
|
522
|
+
|
|
523
|
+
except Exception as e:
|
|
524
|
+
if isinstance(e, loopix_connect.ConnectException):
|
|
525
|
+
if e.status == loopix_connect.Code.not_found:
|
|
526
|
+
return False
|
|
527
|
+
raise _handle_filesystem_rpc_exception(e, self._envd_api)
|
|
528
|
+
|
|
529
|
+
def get_info(
|
|
530
|
+
self,
|
|
531
|
+
path: str,
|
|
532
|
+
user: Optional[Username] = None,
|
|
533
|
+
request_timeout: Optional[float] = None,
|
|
534
|
+
) -> EntryInfo:
|
|
535
|
+
"""
|
|
536
|
+
Get information about a file or directory.
|
|
537
|
+
|
|
538
|
+
:param path: Path to a file or a directory
|
|
539
|
+
:param user: Run the operation as this user
|
|
540
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
541
|
+
|
|
542
|
+
:return: Information about the file or directory like name, type, and path
|
|
543
|
+
"""
|
|
544
|
+
try:
|
|
545
|
+
r = self._rpc.stat(
|
|
546
|
+
filesystem_pb2.StatRequest(path=path),
|
|
547
|
+
request_timeout=self._connection_config.get_request_timeout(
|
|
548
|
+
request_timeout
|
|
549
|
+
),
|
|
550
|
+
headers=authentication_header(self._envd_version, user),
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
return map_entry_info(r.entry)
|
|
554
|
+
except Exception as e:
|
|
555
|
+
raise _handle_filesystem_rpc_exception(e, self._envd_api)
|
|
556
|
+
|
|
557
|
+
def remove(
|
|
558
|
+
self,
|
|
559
|
+
path: str,
|
|
560
|
+
user: Optional[Username] = None,
|
|
561
|
+
request_timeout: Optional[float] = None,
|
|
562
|
+
) -> None:
|
|
563
|
+
"""
|
|
564
|
+
Remove a file or a directory.
|
|
565
|
+
|
|
566
|
+
:param path: Path to a file or a directory
|
|
567
|
+
:param user: Run the operation as this user
|
|
568
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
569
|
+
"""
|
|
570
|
+
try:
|
|
571
|
+
self._rpc.remove(
|
|
572
|
+
filesystem_pb2.RemoveRequest(path=path),
|
|
573
|
+
request_timeout=self._connection_config.get_request_timeout(
|
|
574
|
+
request_timeout
|
|
575
|
+
),
|
|
576
|
+
headers=authentication_header(self._envd_version, user),
|
|
577
|
+
)
|
|
578
|
+
except Exception as e:
|
|
579
|
+
raise _handle_filesystem_rpc_exception(e, self._envd_api)
|
|
580
|
+
|
|
581
|
+
def rename(
|
|
582
|
+
self,
|
|
583
|
+
old_path: str,
|
|
584
|
+
new_path: str,
|
|
585
|
+
user: Optional[Username] = None,
|
|
586
|
+
request_timeout: Optional[float] = None,
|
|
587
|
+
) -> EntryInfo:
|
|
588
|
+
"""
|
|
589
|
+
Rename a file or directory.
|
|
590
|
+
|
|
591
|
+
:param old_path: Path to the file or directory to rename
|
|
592
|
+
:param new_path: New path to the file or directory
|
|
593
|
+
:param user: Run the operation as this user
|
|
594
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
595
|
+
|
|
596
|
+
:return: Information about the renamed file or directory
|
|
597
|
+
"""
|
|
598
|
+
try:
|
|
599
|
+
r = self._rpc.move(
|
|
600
|
+
filesystem_pb2.MoveRequest(
|
|
601
|
+
source=old_path,
|
|
602
|
+
destination=new_path,
|
|
603
|
+
),
|
|
604
|
+
request_timeout=self._connection_config.get_request_timeout(
|
|
605
|
+
request_timeout
|
|
606
|
+
),
|
|
607
|
+
headers=authentication_header(self._envd_version, user),
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
return map_entry_info(r.entry)
|
|
611
|
+
except Exception as e:
|
|
612
|
+
raise _handle_filesystem_rpc_exception(e, self._envd_api)
|
|
613
|
+
|
|
614
|
+
def make_dir(
|
|
615
|
+
self,
|
|
616
|
+
path: str,
|
|
617
|
+
user: Optional[Username] = None,
|
|
618
|
+
request_timeout: Optional[float] = None,
|
|
619
|
+
) -> bool:
|
|
620
|
+
"""
|
|
621
|
+
Create a new directory and all directories along the way if needed on the specified path.
|
|
622
|
+
|
|
623
|
+
:param path: Path to a new directory. For example '/dirA/dirB' when creating 'dirB'.
|
|
624
|
+
:param user: Run the operation as this user
|
|
625
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
626
|
+
|
|
627
|
+
:return: `True` if the directory was created, `False` if the directory already exists
|
|
628
|
+
"""
|
|
629
|
+
try:
|
|
630
|
+
self._rpc.make_dir(
|
|
631
|
+
filesystem_pb2.MakeDirRequest(path=path),
|
|
632
|
+
request_timeout=self._connection_config.get_request_timeout(
|
|
633
|
+
request_timeout
|
|
634
|
+
),
|
|
635
|
+
headers=authentication_header(self._envd_version, user),
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
return True
|
|
639
|
+
except Exception as e:
|
|
640
|
+
if isinstance(e, loopix_connect.ConnectException):
|
|
641
|
+
if e.status == loopix_connect.Code.already_exists:
|
|
642
|
+
return False
|
|
643
|
+
raise _handle_filesystem_rpc_exception(e, self._envd_api)
|
|
644
|
+
|
|
645
|
+
def watch_dir(
|
|
646
|
+
self,
|
|
647
|
+
path: str,
|
|
648
|
+
user: Optional[Username] = None,
|
|
649
|
+
request_timeout: Optional[float] = None,
|
|
650
|
+
recursive: bool = False,
|
|
651
|
+
include_entry: bool = False,
|
|
652
|
+
allow_network_mounts: bool = False,
|
|
653
|
+
) -> WatchHandle:
|
|
654
|
+
"""
|
|
655
|
+
Watch directory for filesystem events.
|
|
656
|
+
|
|
657
|
+
:param path: Path to a directory to watch
|
|
658
|
+
:param user: Run the operation as this user
|
|
659
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
660
|
+
:param recursive: Watch directory recursively
|
|
661
|
+
:param include_entry: Include the `EntryInfo` of the affected entry in each event, when available. Requires envd 0.6.3 or later
|
|
662
|
+
:param allow_network_mounts: Allow watching paths on network filesystem mounts (NFS, CIFS, SMB, FUSE), which are rejected by default. Events on network mounts may be unreliable or not delivered at all. Requires envd 0.6.4 or later
|
|
663
|
+
|
|
664
|
+
:return: `WatchHandle` object for stopping watching directory
|
|
665
|
+
"""
|
|
666
|
+
if recursive and self._envd_version < ENVD_VERSION_RECURSIVE_WATCH:
|
|
667
|
+
raise TemplateException(
|
|
668
|
+
"You need to update the template to use recursive watching."
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
if include_entry and self._envd_version < ENVD_VERSION_FS_EVENT_ENTRY_INFO:
|
|
672
|
+
raise TemplateException(
|
|
673
|
+
"You need to update the template to include entry info in watch events."
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
if (
|
|
677
|
+
allow_network_mounts
|
|
678
|
+
and self._envd_version < ENVD_VERSION_WATCH_NETWORK_MOUNTS
|
|
679
|
+
):
|
|
680
|
+
raise TemplateException(
|
|
681
|
+
"You need to update the template to watch directories on network mounts."
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
try:
|
|
685
|
+
r = self._rpc.create_watcher(
|
|
686
|
+
filesystem_pb2.CreateWatcherRequest(
|
|
687
|
+
path=path,
|
|
688
|
+
recursive=recursive,
|
|
689
|
+
include_entry=include_entry,
|
|
690
|
+
allow_network_mounts=allow_network_mounts,
|
|
691
|
+
),
|
|
692
|
+
request_timeout=self._connection_config.get_request_timeout(
|
|
693
|
+
request_timeout
|
|
694
|
+
),
|
|
695
|
+
headers={
|
|
696
|
+
**authentication_header(self._envd_version, user),
|
|
697
|
+
KEEPALIVE_PING_HEADER: str(KEEPALIVE_PING_INTERVAL_SEC),
|
|
698
|
+
},
|
|
699
|
+
)
|
|
700
|
+
except Exception as e:
|
|
701
|
+
raise _handle_filesystem_rpc_exception(e, self._envd_api)
|
|
702
|
+
|
|
703
|
+
return WatchHandle(
|
|
704
|
+
lambda: self._rpc,
|
|
705
|
+
r.watcher_id,
|
|
706
|
+
self._connection_config,
|
|
707
|
+
self._envd_version,
|
|
708
|
+
user,
|
|
709
|
+
lambda: check_sandbox_health(self._envd_api),
|
|
710
|
+
)
|