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