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,419 @@
1
+ import asyncio
2
+ import os
3
+ from types import TracebackType
4
+ from typing import Callable, Optional, List, Union
5
+
6
+ import httpx
7
+
8
+ from loopix.api import handle_api_exception
9
+ from loopix.io_utils import aiter_io_chunks
10
+ from loopix.api.client.api.templates import (
11
+ post_v3_templates,
12
+ get_templates_template_id_files_hash,
13
+ post_v_2_templates_template_id_builds_build_id,
14
+ get_templates_template_id_builds_build_id_status,
15
+ get_templates_aliases_alias,
16
+ )
17
+ from loopix.api.client.api.tags import (
18
+ post_templates_tags,
19
+ delete_templates_tags,
20
+ get_templates_template_id_tags,
21
+ )
22
+ from loopix.api.client.client import AuthenticatedClient
23
+ from loopix.api.client.models import (
24
+ TemplateBuildRequestV3,
25
+ TemplateBuildStartV2,
26
+ TemplateBuildFileUpload,
27
+ Error,
28
+ AssignTemplateTagsRequest,
29
+ DeleteTemplateTagsRequest,
30
+ )
31
+ from loopix.api.client.types import UNSET, Unset
32
+ from loopix.exceptions import BuildException, FileUploadException, TemplateException
33
+ from loopix.template.logger import LogEntry
34
+ from loopix.template.types import (
35
+ TemplateType,
36
+ BuildStatusReason,
37
+ TemplateBuildStatus,
38
+ TemplateBuildStatusResponse,
39
+ TemplateTag,
40
+ TemplateTagInfo,
41
+ )
42
+ from loopix.template.consts import FILE_UPLOAD_TIMEOUT_SECONDS
43
+ from loopix.template.utils import get_build_step_index, tar_file_stream
44
+
45
+
46
+ async def request_build(
47
+ client: AuthenticatedClient,
48
+ name: str,
49
+ tags: Optional[List[str]],
50
+ cpu_count: int,
51
+ memory_mb: int,
52
+ ):
53
+ res = await post_v3_templates.asyncio_detailed(
54
+ client=client,
55
+ body=TemplateBuildRequestV3(
56
+ name=name,
57
+ tags=tags if tags else UNSET,
58
+ cpu_count=cpu_count,
59
+ memory_mb=memory_mb,
60
+ ),
61
+ )
62
+
63
+ if res.status_code >= 300:
64
+ raise handle_api_exception(res, BuildException)
65
+
66
+ if isinstance(res.parsed, Error):
67
+ raise BuildException(f"API error: {res.parsed.message}")
68
+
69
+ if res.parsed is None:
70
+ raise BuildException("Failed to request build")
71
+
72
+ return res.parsed
73
+
74
+
75
+ async def get_file_upload_link(
76
+ client: AuthenticatedClient,
77
+ template_id: str,
78
+ files_hash: str,
79
+ stack_trace: Optional[TracebackType] = None,
80
+ ) -> TemplateBuildFileUpload:
81
+ res = await get_templates_template_id_files_hash.asyncio_detailed(
82
+ template_id=template_id,
83
+ hash_=files_hash,
84
+ client=client,
85
+ )
86
+
87
+ if res.status_code >= 300:
88
+ raise handle_api_exception(res, FileUploadException, stack_trace)
89
+
90
+ if isinstance(res.parsed, Error):
91
+ raise FileUploadException(f"API error: {res.parsed.message}").with_traceback(
92
+ stack_trace
93
+ )
94
+
95
+ if res.parsed is None:
96
+ raise FileUploadException("Failed to get file upload link").with_traceback(
97
+ stack_trace
98
+ )
99
+
100
+ return res.parsed
101
+
102
+
103
+ async def upload_file(
104
+ api_client: AuthenticatedClient,
105
+ file_name: str,
106
+ context_path: str,
107
+ url: str,
108
+ ignore_patterns: List[str],
109
+ resolve_symlinks: bool,
110
+ gzip: bool,
111
+ stack_trace: Optional[TracebackType],
112
+ request_timeout: Optional[float] = None,
113
+ ):
114
+ # Uploading a large build-context archive can take far longer than the 60s
115
+ # general API timeout, so default to a 1-hour upload timeout unless the
116
+ # caller set an explicit request_timeout. Matches the JS SDK
117
+ # (FILE_UPLOAD_TIMEOUT_MS).
118
+ upload_timeout = (
119
+ request_timeout if request_timeout is not None else FILE_UPLOAD_TIMEOUT_SECONDS
120
+ )
121
+ try:
122
+ tar_file = tar_file_stream(
123
+ file_name, context_path, ignore_patterns, resolve_symlinks, gzip
124
+ )
125
+ try:
126
+ size = os.fstat(tar_file.fileno()).st_size
127
+
128
+ async with httpx.AsyncClient(
129
+ timeout=httpx.Timeout(upload_timeout),
130
+ verify=api_client._verify_ssl,
131
+ follow_redirects=api_client._follow_redirects,
132
+ proxy=getattr(api_client, "_proxy", None),
133
+ http2=False,
134
+ ) as client:
135
+ # Stream the archive from disk via an async iterator. The
136
+ # explicit Content-Length suppresses chunked transfer
137
+ # encoding, which S3 presigned URLs reject.
138
+ response = await client.put(
139
+ url,
140
+ content=aiter_io_chunks(tar_file),
141
+ headers={"Content-Length": str(size)},
142
+ )
143
+ response.raise_for_status()
144
+ finally:
145
+ # Closing the spooled temp file is best-effort: a failure here
146
+ # must not mask a successful upload as a FileUploadException,
147
+ # nor overwrite a real upload error.
148
+ try:
149
+ tar_file.close()
150
+ except Exception:
151
+ pass
152
+ except httpx.HTTPStatusError as e:
153
+ raise FileUploadException(f"Failed to upload file: {e}").with_traceback(
154
+ stack_trace
155
+ )
156
+ except Exception as e:
157
+ raise FileUploadException(f"Failed to upload file: {e}").with_traceback(
158
+ stack_trace
159
+ )
160
+
161
+
162
+ async def trigger_build(
163
+ client: AuthenticatedClient,
164
+ template_id: str,
165
+ build_id: str,
166
+ template: TemplateType,
167
+ ) -> None:
168
+ # Convert template dict to TemplateBuildStartV2 model using from_dict
169
+ template_data = TemplateBuildStartV2.from_dict(template)
170
+
171
+ res = await post_v_2_templates_template_id_builds_build_id.asyncio_detailed(
172
+ template_id=template_id,
173
+ build_id=build_id,
174
+ client=client,
175
+ body=template_data,
176
+ )
177
+
178
+ if res.status_code >= 300:
179
+ raise handle_api_exception(res, BuildException)
180
+
181
+
182
+ def _map_log_entry(entry) -> LogEntry:
183
+ """Map API log entry to LogEntry type."""
184
+ return LogEntry(
185
+ timestamp=entry.timestamp,
186
+ level=entry.level.value,
187
+ message=entry.message,
188
+ )
189
+
190
+
191
+ def _map_build_status_reason(reason) -> Optional[BuildStatusReason]:
192
+ """Map API build status reason to custom BuildStatusReason type."""
193
+ if reason is None or isinstance(reason, Unset):
194
+ return None
195
+ return BuildStatusReason(
196
+ message=reason.message,
197
+ step=reason.step if not isinstance(reason.step, Unset) else None,
198
+ log_entries=[
199
+ _map_log_entry(e)
200
+ for e in (
201
+ reason.log_entries
202
+ if not isinstance(reason.log_entries, Unset) and reason.log_entries
203
+ else []
204
+ )
205
+ ],
206
+ )
207
+
208
+
209
+ async def get_build_status(
210
+ client: AuthenticatedClient, template_id: str, build_id: str, logs_offset: int
211
+ ) -> TemplateBuildStatusResponse:
212
+ res = await get_templates_template_id_builds_build_id_status.asyncio_detailed(
213
+ template_id=template_id,
214
+ build_id=build_id,
215
+ client=client,
216
+ logs_offset=logs_offset,
217
+ )
218
+
219
+ if res.status_code >= 300:
220
+ raise handle_api_exception(res, BuildException)
221
+
222
+ if isinstance(res.parsed, Error):
223
+ raise BuildException(f"API error: {res.parsed.message}")
224
+
225
+ if res.parsed is None:
226
+ raise BuildException("Failed to get build status")
227
+
228
+ return TemplateBuildStatusResponse(
229
+ build_id=res.parsed.build_id,
230
+ template_id=res.parsed.template_id,
231
+ status=TemplateBuildStatus(res.parsed.status.value),
232
+ log_entries=[_map_log_entry(e) for e in res.parsed.log_entries],
233
+ logs=res.parsed.logs,
234
+ reason=_map_build_status_reason(res.parsed.reason),
235
+ )
236
+
237
+
238
+ async def wait_for_build_finish(
239
+ client: AuthenticatedClient,
240
+ template_id: str,
241
+ build_id: str,
242
+ on_build_logs: Optional[Callable[[LogEntry], None]] = None,
243
+ logs_refresh_frequency: float = 0.2,
244
+ stack_traces: List[Union[TracebackType, None]] = [],
245
+ ):
246
+ logs_offset = 0
247
+ status = TemplateBuildStatus.BUILDING
248
+
249
+ async def poll_status() -> TemplateBuildStatusResponse:
250
+ nonlocal logs_offset
251
+ build_status = await get_build_status(
252
+ client, template_id, build_id, logs_offset
253
+ )
254
+
255
+ logs_offset += len(build_status.log_entries)
256
+
257
+ for log_entry in build_status.log_entries:
258
+ if on_build_logs:
259
+ on_build_logs(log_entry)
260
+
261
+ return build_status
262
+
263
+ while status in [TemplateBuildStatus.BUILDING, TemplateBuildStatus.WAITING]:
264
+ build_status = await poll_status()
265
+
266
+ status = build_status.status
267
+
268
+ if status in [TemplateBuildStatus.READY, TemplateBuildStatus.ERROR]:
269
+ # The status endpoint returns at most 100 log entries per call, so
270
+ # the terminal response may not include the last logs - keep
271
+ # fetching until they are drained.
272
+ tail_status = build_status
273
+ while len(tail_status.log_entries) > 0:
274
+ tail_status = await poll_status()
275
+
276
+ if status == TemplateBuildStatus.READY:
277
+ return
278
+
279
+ traceback = None
280
+ if build_status.reason and build_status.reason.step:
281
+ # Find the corresponding stack trace for the failed step
282
+ step_index = get_build_step_index(
283
+ build_status.reason.step, len(stack_traces)
284
+ )
285
+ if step_index < len(stack_traces):
286
+ traceback = stack_traces[step_index]
287
+
288
+ raise BuildException(
289
+ build_status.reason.message if build_status.reason else "Build failed"
290
+ ).with_traceback(traceback)
291
+
292
+ # Wait for a short period before checking the status again
293
+ await asyncio.sleep(logs_refresh_frequency)
294
+
295
+ raise BuildException("Unknown build error occurred.")
296
+
297
+
298
+ async def check_alias_exists(client: AuthenticatedClient, alias: str) -> bool:
299
+ """
300
+ Check if a template with the given alias exists.
301
+
302
+ Args:
303
+ client: Authenticated API client
304
+ alias: Template alias to check
305
+
306
+ Returns:
307
+ True if the alias exists, False otherwise
308
+ """
309
+ res = await get_templates_aliases_alias.asyncio_detailed(
310
+ alias=alias,
311
+ client=client,
312
+ )
313
+
314
+ # If we get a NotFound, the alias doesn't exist
315
+ if res.status_code == 404:
316
+ return False
317
+
318
+ # If we get a Forbidden, alias exists, but you are not owner
319
+ if res.status_code == 403:
320
+ return True
321
+
322
+ # Handle other errors
323
+ if res.status_code >= 300:
324
+ raise handle_api_exception(res, TemplateException)
325
+
326
+ # If we get Ok with data, you are owner and the alias exists
327
+ return res.parsed is not None
328
+
329
+
330
+ async def assign_tags(
331
+ client: AuthenticatedClient, target_name: str, tags: List[str]
332
+ ) -> TemplateTagInfo:
333
+ """
334
+ Assign tag(s) to an existing template build.
335
+
336
+ Args:
337
+ client: Authenticated API client
338
+ target_name: Template name in 'name:tag' format (the source build to tag from)
339
+ tags: Tags to assign
340
+
341
+ Returns:
342
+ TemplateTagInfo with build_id and assigned tags
343
+ """
344
+ res = await post_templates_tags.asyncio_detailed(
345
+ client=client,
346
+ body=AssignTemplateTagsRequest(
347
+ target=target_name,
348
+ tags=tags,
349
+ ),
350
+ )
351
+
352
+ if res.status_code >= 300:
353
+ raise handle_api_exception(res, TemplateException)
354
+
355
+ if isinstance(res.parsed, Error):
356
+ raise TemplateException(f"API error: {res.parsed.message}")
357
+
358
+ if res.parsed is None:
359
+ raise TemplateException("Failed to assign tags")
360
+
361
+ return TemplateTagInfo(
362
+ build_id=str(res.parsed.build_id),
363
+ tags=res.parsed.tags,
364
+ )
365
+
366
+
367
+ async def remove_tags(client: AuthenticatedClient, name: str, tags: List[str]) -> None:
368
+ """
369
+ Remove tag(s) from a template.
370
+
371
+ Args:
372
+ client: Authenticated API client
373
+ name: Template name
374
+ tags: List of tags to remove
375
+ """
376
+ res = await delete_templates_tags.asyncio_detailed(
377
+ client=client,
378
+ body=DeleteTemplateTagsRequest(
379
+ name=name,
380
+ tags=tags,
381
+ ),
382
+ )
383
+
384
+ if res.status_code >= 300:
385
+ raise handle_api_exception(res, TemplateException)
386
+
387
+
388
+ async def get_template_tags(
389
+ client: AuthenticatedClient, template_id: str
390
+ ) -> List[TemplateTag]:
391
+ """
392
+ Get all tags for a template.
393
+
394
+ Args:
395
+ client: Authenticated API client
396
+ template_id: Template ID or name
397
+ """
398
+ res = await get_templates_template_id_tags.asyncio_detailed(
399
+ template_id=template_id,
400
+ client=client,
401
+ )
402
+
403
+ if res.status_code >= 300:
404
+ raise handle_api_exception(res, TemplateException)
405
+
406
+ if isinstance(res.parsed, Error):
407
+ raise TemplateException(f"API error: {res.parsed.message}")
408
+
409
+ if res.parsed is None:
410
+ raise TemplateException("Failed to get template tags")
411
+
412
+ return [
413
+ TemplateTag(
414
+ tag=item.tag,
415
+ build_id=str(item.build_id),
416
+ created_at=item.created_at,
417
+ )
418
+ for item in res.parsed
419
+ ]