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,528 @@
1
+ from datetime import datetime
2
+ from typing import Callable, List, Optional, Union
3
+
4
+ from typing_extensions import Unpack
5
+
6
+ from loopix.api.client.client import AuthenticatedClient
7
+ from loopix.connection_config import ApiParams, ConnectionConfig
8
+ from loopix.template.consts import GZIP, RESOLVE_SYMLINKS
9
+ from loopix.template.logger import LogEntry, LogEntryEnd, LogEntryStart
10
+ from loopix.template.main import TemplateBase, TemplateClass
11
+ from loopix.template.types import BuildInfo, InstructionType, TemplateTag, TemplateTagInfo
12
+ from loopix.template.utils import normalize_build_arguments, read_dockerignore
13
+
14
+ from .build_api import (
15
+ assign_tags,
16
+ check_alias_exists,
17
+ get_template_tags,
18
+ remove_tags,
19
+ get_build_status,
20
+ get_file_upload_link,
21
+ request_build,
22
+ trigger_build,
23
+ upload_file,
24
+ wait_for_build_finish,
25
+ )
26
+ from loopix.api.client_async import get_api_client
27
+
28
+
29
+ class AsyncTemplate(TemplateBase):
30
+ """
31
+ Asynchronous template builder for Loopix sandboxes.
32
+ """
33
+
34
+ @staticmethod
35
+ async def _build(
36
+ api_client: AuthenticatedClient,
37
+ template: TemplateClass,
38
+ name: str,
39
+ tags: Optional[List[str]] = None,
40
+ cpu_count: int = 2,
41
+ memory_mb: int = 1024,
42
+ skip_cache: bool = False,
43
+ on_build_logs: Optional[Callable[[LogEntry], None]] = None,
44
+ request_timeout: Optional[float] = None,
45
+ ) -> BuildInfo:
46
+ """
47
+ Internal implementation of the template build process
48
+
49
+ :param api_client: Authenticated API client
50
+ :param template: The template to build
51
+ :param name: Name for the template
52
+ :param tags: Optional tags for the template
53
+ :param cpu_count: Number of CPUs allocated to the sandbox
54
+ :param memory_mb: Amount of memory in MB allocated to the sandbox
55
+ :param skip_cache: If True, forces a complete rebuild ignoring cache
56
+ :param on_build_logs: Callback function to receive build logs during the build process
57
+ """
58
+ if skip_cache:
59
+ template._template._force = True
60
+
61
+ # Create template
62
+ if on_build_logs:
63
+ tags_msg = f" with tags: {', '.join(tags)}" if tags else ""
64
+ on_build_logs(
65
+ LogEntry(
66
+ timestamp=datetime.now(),
67
+ level="info",
68
+ message=f"Requesting build for template: {name}{tags_msg}",
69
+ )
70
+ )
71
+
72
+ response = await request_build(
73
+ api_client,
74
+ name=name,
75
+ cpu_count=cpu_count,
76
+ memory_mb=memory_mb,
77
+ tags=tags,
78
+ )
79
+
80
+ template_id = response.template_id
81
+ build_id = response.build_id
82
+ response_tags = response.tags
83
+
84
+ if on_build_logs:
85
+ on_build_logs(
86
+ LogEntry(
87
+ timestamp=datetime.now(),
88
+ level="info",
89
+ message=f"Template created with ID: {template_id}, Build ID: {build_id}",
90
+ )
91
+ )
92
+
93
+ instructions_with_hashes = template._template._instructions_with_hashes()
94
+
95
+ # Upload files
96
+ for index, file_upload in enumerate(instructions_with_hashes):
97
+ if file_upload["type"] != InstructionType.COPY:
98
+ continue
99
+
100
+ args = file_upload.get("args", [])
101
+ src = args[0] if len(args) > 0 else None
102
+ force_upload = file_upload.get("forceUpload")
103
+ files_hash = file_upload.get("filesHash", None)
104
+ resolve_symlinks = file_upload.get("resolveSymlinks")
105
+ if resolve_symlinks is None:
106
+ resolve_symlinks = RESOLVE_SYMLINKS
107
+ gzip = file_upload.get("gzip")
108
+ if gzip is None:
109
+ gzip = GZIP
110
+
111
+ if src is None or files_hash is None:
112
+ raise ValueError("Source path and files hash are required")
113
+
114
+ stack_trace = None
115
+ if index + 1 < len(template._template._stack_traces):
116
+ stack_trace = template._template._stack_traces[index + 1]
117
+
118
+ file_info = await get_file_upload_link(
119
+ api_client, template_id, files_hash, stack_trace
120
+ )
121
+
122
+ if (force_upload and file_info.url) or (
123
+ file_info.present is False and file_info.url
124
+ ):
125
+ await upload_file(
126
+ api_client,
127
+ src,
128
+ template._template._file_context_path,
129
+ file_info.url,
130
+ [
131
+ *template._template._file_ignore_patterns,
132
+ *read_dockerignore(template._template._file_context_path),
133
+ ],
134
+ resolve_symlinks,
135
+ gzip,
136
+ stack_trace,
137
+ request_timeout=request_timeout,
138
+ )
139
+ if on_build_logs:
140
+ on_build_logs(
141
+ LogEntry(
142
+ timestamp=datetime.now(),
143
+ level="info",
144
+ message=f"Uploaded '{src}'",
145
+ )
146
+ )
147
+ else:
148
+ if on_build_logs:
149
+ on_build_logs(
150
+ LogEntry(
151
+ timestamp=datetime.now(),
152
+ level="info",
153
+ message=f"Skipping upload of '{src}', already cached",
154
+ )
155
+ )
156
+
157
+ if on_build_logs:
158
+ on_build_logs(
159
+ LogEntry(
160
+ timestamp=datetime.now(),
161
+ level="info",
162
+ message="All file uploads completed",
163
+ )
164
+ )
165
+
166
+ # Start build
167
+ if on_build_logs:
168
+ on_build_logs(
169
+ LogEntry(
170
+ timestamp=datetime.now(),
171
+ level="info",
172
+ message="Starting building...",
173
+ )
174
+ )
175
+
176
+ await trigger_build(
177
+ api_client,
178
+ template_id,
179
+ build_id,
180
+ template._template._serialize(instructions_with_hashes),
181
+ )
182
+
183
+ return BuildInfo(
184
+ template_id=template_id,
185
+ build_id=build_id,
186
+ alias=name,
187
+ name=name,
188
+ tags=response_tags,
189
+ )
190
+
191
+ @staticmethod
192
+ async def build(
193
+ template: TemplateClass,
194
+ name: Optional[str] = None,
195
+ *,
196
+ alias: Optional[str] = None,
197
+ tags: Optional[List[str]] = None,
198
+ cpu_count: int = 2,
199
+ memory_mb: int = 1024,
200
+ skip_cache: bool = False,
201
+ on_build_logs: Optional[Callable[[LogEntry], None]] = None,
202
+ **opts: Unpack[ApiParams],
203
+ ) -> BuildInfo:
204
+ """
205
+ Build and deploy a template to Loopix infrastructure.
206
+
207
+ :param template: The template to build
208
+ :param name: Template name in 'name' or 'name:tag' format
209
+ :param alias: (Deprecated) Alias name for the template. Use name instead.
210
+ :param tags: Optional additional tags to assign to the template
211
+ :param cpu_count: Number of CPUs allocated to the sandbox
212
+ :param memory_mb: Amount of memory in MB allocated to the sandbox
213
+ :param skip_cache: If True, forces a complete rebuild ignoring cache
214
+ :param on_build_logs: Callback function to receive build logs during the build process
215
+
216
+ Example
217
+ ```python
218
+ from loopix import AsyncTemplate
219
+
220
+ template = (
221
+ AsyncTemplate()
222
+ .from_python_image('3')
223
+ .copy('requirements.txt', '/home/user/')
224
+ .run_cmd('pip install -r /home/user/requirements.txt')
225
+ )
226
+
227
+ # Build with single tag
228
+ await AsyncTemplate.build(template, 'my-python-env:v1.0')
229
+
230
+ # Build with multiple tags
231
+ await AsyncTemplate.build(template, 'my-python-env', tags=['v1.1.0', 'stable'])
232
+ ```
233
+ """
234
+ name = normalize_build_arguments(name, alias)
235
+
236
+ try:
237
+ if on_build_logs:
238
+ on_build_logs(
239
+ LogEntryStart(
240
+ timestamp=datetime.now(),
241
+ message="Build started",
242
+ )
243
+ )
244
+
245
+ config = ConnectionConfig(**opts)
246
+ api_client = get_api_client(
247
+ config,
248
+ )
249
+
250
+ data = await AsyncTemplate._build(
251
+ api_client,
252
+ template,
253
+ name,
254
+ tags=tags,
255
+ cpu_count=cpu_count,
256
+ memory_mb=memory_mb,
257
+ skip_cache=skip_cache,
258
+ on_build_logs=on_build_logs,
259
+ # Only honor an explicitly set request_timeout for uploads;
260
+ # otherwise upload_file applies its 1-hour default.
261
+ request_timeout=opts.get("request_timeout"),
262
+ )
263
+
264
+ if on_build_logs:
265
+ on_build_logs(
266
+ LogEntry(
267
+ timestamp=datetime.now(),
268
+ level="info",
269
+ message="Waiting for logs...",
270
+ )
271
+ )
272
+
273
+ await wait_for_build_finish(
274
+ api_client,
275
+ data.template_id,
276
+ data.build_id,
277
+ on_build_logs,
278
+ logs_refresh_frequency=TemplateBase._logs_refresh_frequency,
279
+ stack_traces=template._template._stack_traces,
280
+ )
281
+
282
+ return data
283
+ finally:
284
+ if on_build_logs:
285
+ on_build_logs(
286
+ LogEntryEnd(
287
+ timestamp=datetime.now(),
288
+ message="Build finished",
289
+ )
290
+ )
291
+
292
+ @staticmethod
293
+ async def build_in_background(
294
+ template: TemplateClass,
295
+ name: Optional[str] = None,
296
+ *,
297
+ alias: Optional[str] = None,
298
+ tags: Optional[List[str]] = None,
299
+ cpu_count: int = 2,
300
+ memory_mb: int = 1024,
301
+ skip_cache: bool = False,
302
+ on_build_logs: Optional[Callable[[LogEntry], None]] = None,
303
+ **opts: Unpack[ApiParams],
304
+ ) -> BuildInfo:
305
+ """
306
+ Build and deploy a template to Loopix infrastructure without waiting for completion.
307
+
308
+ :param template: The template to build
309
+ :param name: Template name in 'name' or 'name:tag' format
310
+ :param alias: (Deprecated) Alias name for the template. Use name instead.
311
+ :param tags: Optional additional tags to assign to the template
312
+ :param cpu_count: Number of CPUs allocated to the sandbox
313
+ :param memory_mb: Amount of memory in MB allocated to the sandbox
314
+ :param skip_cache: If True, forces a complete rebuild ignoring cache
315
+ :return: BuildInfo containing the template ID and build ID
316
+
317
+ Example
318
+ ```python
319
+ from loopix import AsyncTemplate
320
+
321
+ template = (
322
+ AsyncTemplate()
323
+ .from_python_image('3')
324
+ .run_cmd('echo "test"')
325
+ .set_start_cmd('echo "Hello"', 'sleep 1')
326
+ )
327
+
328
+ # Build with single tag
329
+ build_info = await AsyncTemplate.build_in_background(template, 'my-python-env:v1.0')
330
+
331
+ # Build with multiple tags
332
+ build_info = await AsyncTemplate.build_in_background(template, 'my-python-env', tags=['v1.1.0', 'stable'])
333
+ ```
334
+ """
335
+ name = normalize_build_arguments(name, alias)
336
+
337
+ config = ConnectionConfig(**opts)
338
+ api_client = get_api_client(
339
+ config,
340
+ )
341
+
342
+ return await AsyncTemplate._build(
343
+ api_client,
344
+ template,
345
+ name,
346
+ tags=tags,
347
+ cpu_count=cpu_count,
348
+ memory_mb=memory_mb,
349
+ skip_cache=skip_cache,
350
+ on_build_logs=on_build_logs,
351
+ # Only honor an explicitly set request_timeout for uploads;
352
+ # otherwise upload_file applies its 1-hour default.
353
+ request_timeout=opts.get("request_timeout"),
354
+ )
355
+
356
+ @staticmethod
357
+ async def get_build_status(
358
+ build_info: BuildInfo,
359
+ logs_offset: int = 0,
360
+ **opts: Unpack[ApiParams],
361
+ ):
362
+ """
363
+ Get the status of a build.
364
+
365
+ :param build_info: Build identifiers returned from build_in_background
366
+ :param logs_offset: Offset for fetching logs
367
+ :return: TemplateBuild containing the build status and logs
368
+
369
+ Example
370
+ ```python
371
+ from loopix import AsyncTemplate
372
+
373
+ build_info = await AsyncTemplate.build_in_background(template, alias='my-template')
374
+ status = await AsyncTemplate.get_build_status(build_info, logs_offset=0)
375
+ ```
376
+ """
377
+ config = ConnectionConfig(**opts)
378
+ api_client = get_api_client(
379
+ config,
380
+ )
381
+ return await get_build_status(
382
+ api_client,
383
+ build_info.template_id,
384
+ build_info.build_id,
385
+ logs_offset,
386
+ )
387
+
388
+ @staticmethod
389
+ async def exists(
390
+ name: str,
391
+ **opts: Unpack[ApiParams],
392
+ ) -> bool:
393
+ """
394
+ Check if a template with the given name exists.
395
+
396
+ :param name: Template name to check
397
+ :return: True if the name exists, False otherwise
398
+
399
+ Example
400
+ ```python
401
+ from loopix import AsyncTemplate
402
+
403
+ exists = await AsyncTemplate.exists('my-python-env')
404
+ if exists:
405
+ print('Template exists!')
406
+ ```
407
+ """
408
+
409
+ return await AsyncTemplate.alias_exists(name, **opts)
410
+
411
+ @staticmethod
412
+ async def alias_exists(
413
+ alias: str,
414
+ **opts: Unpack[ApiParams],
415
+ ) -> bool:
416
+ """
417
+ Check if a template with the given alias exists.
418
+
419
+ Deprecated Use `exists` instead.
420
+
421
+ :param alias: Template alias to check
422
+ :return: True if the alias exists, False otherwise
423
+
424
+ Example
425
+ ```python
426
+ from loopix import AsyncTemplate
427
+
428
+ exists = await AsyncTemplate.alias_exists('my-python-env')
429
+ if exists:
430
+ print('Template exists!')
431
+ ```
432
+ """
433
+ config = ConnectionConfig(**opts)
434
+ api_client = get_api_client(
435
+ config,
436
+ )
437
+
438
+ return await check_alias_exists(api_client, alias)
439
+
440
+ @staticmethod
441
+ async def assign_tags(
442
+ target_name: str,
443
+ tags: Union[str, List[str]],
444
+ **opts: Unpack[ApiParams],
445
+ ) -> TemplateTagInfo:
446
+ """
447
+ Assign tag(s) to an existing template build.
448
+
449
+ :param target_name: Template name in 'name:tag' format (the source build to tag from)
450
+ :param tags: Tag or tags to assign
451
+ :return: TemplateTagInfo with build_id and assigned tags
452
+
453
+ Example
454
+ ```python
455
+ from loopix import AsyncTemplate
456
+
457
+ # Assign a single tag
458
+ result = await AsyncTemplate.assign_tags('my-template:v1.0', 'production')
459
+
460
+ # Assign multiple tags
461
+ result = await AsyncTemplate.assign_tags('my-template:v1.0', ['production', 'stable'])
462
+ ```
463
+ """
464
+ config = ConnectionConfig(**opts)
465
+ api_client = get_api_client(
466
+ config,
467
+ )
468
+
469
+ normalized_tags = [tags] if isinstance(tags, str) else tags
470
+ return await assign_tags(api_client, target_name, normalized_tags)
471
+
472
+ @staticmethod
473
+ async def remove_tags(
474
+ name: str,
475
+ tags: Union[str, List[str]],
476
+ **opts: Unpack[ApiParams],
477
+ ) -> None:
478
+ """
479
+ Remove tag(s) from a template.
480
+
481
+ :param name: Template name
482
+ :param tags: Tag or tags to remove
483
+
484
+ Example
485
+ ```python
486
+ from loopix import AsyncTemplate
487
+
488
+ # Remove a single tag
489
+ await AsyncTemplate.remove_tags('my-template', 'production')
490
+
491
+ # Remove multiple tags
492
+ await AsyncTemplate.remove_tags('my-template', ['production', 'stable'])
493
+ ```
494
+ """
495
+ config = ConnectionConfig(**opts)
496
+ api_client = get_api_client(
497
+ config,
498
+ )
499
+
500
+ normalized_tags = [tags] if isinstance(tags, str) else tags
501
+ await remove_tags(api_client, name, normalized_tags)
502
+
503
+ @staticmethod
504
+ async def get_tags(
505
+ template_id: str,
506
+ **opts: Unpack[ApiParams],
507
+ ) -> List[TemplateTag]:
508
+ """
509
+ Get all tags for a template.
510
+
511
+ :param template_id: Template ID or name
512
+ :return: List of TemplateTag with tag name, build_id, and created_at
513
+
514
+ Example
515
+ ```python
516
+ from loopix import AsyncTemplate
517
+
518
+ tags = await AsyncTemplate.get_tags('my-template')
519
+ for tag in tags:
520
+ print(f"Tag: {tag.tag}, Build: {tag.build_id}, Created: {tag.created_at}")
521
+ ```
522
+ """
523
+ config = ConnectionConfig(**opts)
524
+ api_client = get_api_client(
525
+ config,
526
+ )
527
+
528
+ return await get_template_tags(api_client, template_id)