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