moru 0.1.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 (152) hide show
  1. moru/__init__.py +174 -0
  2. moru/api/__init__.py +164 -0
  3. moru/api/client/__init__.py +8 -0
  4. moru/api/client/api/__init__.py +1 -0
  5. moru/api/client/api/sandboxes/__init__.py +1 -0
  6. moru/api/client/api/sandboxes/delete_sandboxes_sandbox_id.py +161 -0
  7. moru/api/client/api/sandboxes/get_sandboxes.py +176 -0
  8. moru/api/client/api/sandboxes/get_sandboxes_metrics.py +173 -0
  9. moru/api/client/api/sandboxes/get_sandboxes_sandbox_id.py +163 -0
  10. moru/api/client/api/sandboxes/get_sandboxes_sandbox_id_logs.py +199 -0
  11. moru/api/client/api/sandboxes/get_sandboxes_sandbox_id_metrics.py +212 -0
  12. moru/api/client/api/sandboxes/get_v2_sandboxes.py +230 -0
  13. moru/api/client/api/sandboxes/post_sandboxes.py +172 -0
  14. moru/api/client/api/sandboxes/post_sandboxes_sandbox_id_connect.py +193 -0
  15. moru/api/client/api/sandboxes/post_sandboxes_sandbox_id_pause.py +165 -0
  16. moru/api/client/api/sandboxes/post_sandboxes_sandbox_id_refreshes.py +181 -0
  17. moru/api/client/api/sandboxes/post_sandboxes_sandbox_id_resume.py +189 -0
  18. moru/api/client/api/sandboxes/post_sandboxes_sandbox_id_timeout.py +193 -0
  19. moru/api/client/api/templates/__init__.py +1 -0
  20. moru/api/client/api/templates/delete_templates_template_id.py +157 -0
  21. moru/api/client/api/templates/get_templates.py +172 -0
  22. moru/api/client/api/templates/get_templates_template_id.py +195 -0
  23. moru/api/client/api/templates/get_templates_template_id_builds_build_id_status.py +217 -0
  24. moru/api/client/api/templates/get_templates_template_id_files_hash.py +180 -0
  25. moru/api/client/api/templates/patch_templates_template_id.py +183 -0
  26. moru/api/client/api/templates/post_templates.py +172 -0
  27. moru/api/client/api/templates/post_templates_template_id.py +181 -0
  28. moru/api/client/api/templates/post_templates_template_id_builds_build_id.py +170 -0
  29. moru/api/client/api/templates/post_v2_templates.py +172 -0
  30. moru/api/client/api/templates/post_v3_templates.py +172 -0
  31. moru/api/client/api/templates/post_v_2_templates_template_id_builds_build_id.py +192 -0
  32. moru/api/client/client.py +286 -0
  33. moru/api/client/errors.py +16 -0
  34. moru/api/client/models/__init__.py +123 -0
  35. moru/api/client/models/aws_registry.py +85 -0
  36. moru/api/client/models/aws_registry_type.py +8 -0
  37. moru/api/client/models/build_log_entry.py +89 -0
  38. moru/api/client/models/build_status_reason.py +95 -0
  39. moru/api/client/models/connect_sandbox.py +59 -0
  40. moru/api/client/models/created_access_token.py +100 -0
  41. moru/api/client/models/created_team_api_key.py +166 -0
  42. moru/api/client/models/disk_metrics.py +91 -0
  43. moru/api/client/models/error.py +67 -0
  44. moru/api/client/models/gcp_registry.py +69 -0
  45. moru/api/client/models/gcp_registry_type.py +8 -0
  46. moru/api/client/models/general_registry.py +77 -0
  47. moru/api/client/models/general_registry_type.py +8 -0
  48. moru/api/client/models/identifier_masking_details.py +83 -0
  49. moru/api/client/models/listed_sandbox.py +154 -0
  50. moru/api/client/models/log_level.py +11 -0
  51. moru/api/client/models/max_team_metric.py +78 -0
  52. moru/api/client/models/mcp_type_0.py +44 -0
  53. moru/api/client/models/new_access_token.py +59 -0
  54. moru/api/client/models/new_sandbox.py +172 -0
  55. moru/api/client/models/new_team_api_key.py +59 -0
  56. moru/api/client/models/node.py +155 -0
  57. moru/api/client/models/node_detail.py +165 -0
  58. moru/api/client/models/node_metrics.py +122 -0
  59. moru/api/client/models/node_status.py +11 -0
  60. moru/api/client/models/node_status_change.py +79 -0
  61. moru/api/client/models/post_sandboxes_sandbox_id_refreshes_body.py +59 -0
  62. moru/api/client/models/post_sandboxes_sandbox_id_timeout_body.py +59 -0
  63. moru/api/client/models/resumed_sandbox.py +68 -0
  64. moru/api/client/models/sandbox.py +145 -0
  65. moru/api/client/models/sandbox_detail.py +183 -0
  66. moru/api/client/models/sandbox_log.py +70 -0
  67. moru/api/client/models/sandbox_log_entry.py +93 -0
  68. moru/api/client/models/sandbox_log_entry_fields.py +44 -0
  69. moru/api/client/models/sandbox_logs.py +91 -0
  70. moru/api/client/models/sandbox_metric.py +118 -0
  71. moru/api/client/models/sandbox_network_config.py +92 -0
  72. moru/api/client/models/sandbox_state.py +9 -0
  73. moru/api/client/models/sandboxes_with_metrics.py +59 -0
  74. moru/api/client/models/team.py +83 -0
  75. moru/api/client/models/team_api_key.py +158 -0
  76. moru/api/client/models/team_metric.py +86 -0
  77. moru/api/client/models/team_user.py +68 -0
  78. moru/api/client/models/template.py +217 -0
  79. moru/api/client/models/template_build.py +139 -0
  80. moru/api/client/models/template_build_file_upload.py +70 -0
  81. moru/api/client/models/template_build_info.py +126 -0
  82. moru/api/client/models/template_build_request.py +115 -0
  83. moru/api/client/models/template_build_request_v2.py +88 -0
  84. moru/api/client/models/template_build_request_v3.py +88 -0
  85. moru/api/client/models/template_build_start_v2.py +184 -0
  86. moru/api/client/models/template_build_status.py +11 -0
  87. moru/api/client/models/template_legacy.py +207 -0
  88. moru/api/client/models/template_request_response_v3.py +83 -0
  89. moru/api/client/models/template_step.py +91 -0
  90. moru/api/client/models/template_update_request.py +59 -0
  91. moru/api/client/models/template_with_builds.py +148 -0
  92. moru/api/client/models/update_team_api_key.py +59 -0
  93. moru/api/client/py.typed +1 -0
  94. moru/api/client/types.py +54 -0
  95. moru/api/client_async/__init__.py +50 -0
  96. moru/api/client_sync/__init__.py +52 -0
  97. moru/api/metadata.py +14 -0
  98. moru/connection_config.py +217 -0
  99. moru/envd/api.py +59 -0
  100. moru/envd/filesystem/filesystem_connect.py +193 -0
  101. moru/envd/filesystem/filesystem_pb2.py +76 -0
  102. moru/envd/filesystem/filesystem_pb2.pyi +233 -0
  103. moru/envd/process/process_connect.py +155 -0
  104. moru/envd/process/process_pb2.py +92 -0
  105. moru/envd/process/process_pb2.pyi +304 -0
  106. moru/envd/rpc.py +61 -0
  107. moru/envd/versions.py +6 -0
  108. moru/exceptions.py +95 -0
  109. moru/sandbox/commands/command_handle.py +69 -0
  110. moru/sandbox/commands/main.py +39 -0
  111. moru/sandbox/filesystem/filesystem.py +94 -0
  112. moru/sandbox/filesystem/watch_handle.py +60 -0
  113. moru/sandbox/main.py +210 -0
  114. moru/sandbox/mcp.py +1120 -0
  115. moru/sandbox/network.py +8 -0
  116. moru/sandbox/sandbox_api.py +210 -0
  117. moru/sandbox/signature.py +45 -0
  118. moru/sandbox/utils.py +34 -0
  119. moru/sandbox_async/commands/command.py +336 -0
  120. moru/sandbox_async/commands/command_handle.py +196 -0
  121. moru/sandbox_async/commands/pty.py +240 -0
  122. moru/sandbox_async/filesystem/filesystem.py +531 -0
  123. moru/sandbox_async/filesystem/watch_handle.py +62 -0
  124. moru/sandbox_async/main.py +734 -0
  125. moru/sandbox_async/paginator.py +69 -0
  126. moru/sandbox_async/sandbox_api.py +325 -0
  127. moru/sandbox_async/utils.py +7 -0
  128. moru/sandbox_sync/commands/command.py +328 -0
  129. moru/sandbox_sync/commands/command_handle.py +150 -0
  130. moru/sandbox_sync/commands/pty.py +230 -0
  131. moru/sandbox_sync/filesystem/filesystem.py +518 -0
  132. moru/sandbox_sync/filesystem/watch_handle.py +69 -0
  133. moru/sandbox_sync/main.py +726 -0
  134. moru/sandbox_sync/paginator.py +69 -0
  135. moru/sandbox_sync/sandbox_api.py +308 -0
  136. moru/template/consts.py +30 -0
  137. moru/template/dockerfile_parser.py +275 -0
  138. moru/template/logger.py +232 -0
  139. moru/template/main.py +1360 -0
  140. moru/template/readycmd.py +138 -0
  141. moru/template/types.py +105 -0
  142. moru/template/utils.py +320 -0
  143. moru/template_async/build_api.py +202 -0
  144. moru/template_async/main.py +366 -0
  145. moru/template_sync/build_api.py +199 -0
  146. moru/template_sync/main.py +371 -0
  147. moru-0.1.0.dist-info/METADATA +63 -0
  148. moru-0.1.0.dist-info/RECORD +152 -0
  149. moru-0.1.0.dist-info/WHEEL +4 -0
  150. moru-0.1.0.dist-info/licenses/LICENSE +9 -0
  151. moru_connect/__init__.py +1 -0
  152. moru_connect/client.py +493 -0
@@ -0,0 +1,202 @@
1
+ import asyncio
2
+ from types import TracebackType
3
+ from typing import Callable, Literal, Optional, List, Union
4
+
5
+ import httpx
6
+
7
+ from moru.api import handle_api_exception
8
+ from moru.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
+ )
14
+ from moru.api.client.client import AuthenticatedClient
15
+ from moru.api.client.models import (
16
+ TemplateBuildRequestV3,
17
+ TemplateBuildStartV2,
18
+ TemplateBuildFileUpload,
19
+ TemplateBuild,
20
+ Error,
21
+ )
22
+ from moru.exceptions import BuildException, FileUploadException
23
+ from moru.template.logger import LogEntry
24
+ from moru.template.types import TemplateType
25
+ from moru.template.utils import get_build_step_index, tar_file_stream
26
+
27
+
28
+ async def request_build(
29
+ client: AuthenticatedClient, name: str, cpu_count: int, memory_mb: int
30
+ ):
31
+ res = await post_v3_templates.asyncio_detailed(
32
+ client=client,
33
+ body=TemplateBuildRequestV3(
34
+ alias=name,
35
+ cpu_count=cpu_count,
36
+ memory_mb=memory_mb,
37
+ ),
38
+ )
39
+
40
+ if res.status_code >= 300:
41
+ raise handle_api_exception(res, BuildException)
42
+
43
+ if isinstance(res.parsed, Error):
44
+ raise BuildException(f"API error: {res.parsed.message}")
45
+
46
+ if res.parsed is None:
47
+ raise BuildException("Failed to request build")
48
+
49
+ return res.parsed
50
+
51
+
52
+ async def get_file_upload_link(
53
+ client: AuthenticatedClient,
54
+ template_id: str,
55
+ files_hash: str,
56
+ stack_trace: Optional[TracebackType] = None,
57
+ ) -> TemplateBuildFileUpload:
58
+ res = await get_templates_template_id_files_hash.asyncio_detailed(
59
+ template_id=template_id,
60
+ hash_=files_hash,
61
+ client=client,
62
+ )
63
+
64
+ if res.status_code >= 300:
65
+ raise handle_api_exception(res, FileUploadException, stack_trace)
66
+
67
+ if isinstance(res.parsed, Error):
68
+ raise FileUploadException(f"API error: {res.parsed.message}").with_traceback(
69
+ stack_trace
70
+ )
71
+
72
+ if res.parsed is None:
73
+ raise FileUploadException("Failed to get file upload link").with_traceback(
74
+ stack_trace
75
+ )
76
+
77
+ return res.parsed
78
+
79
+
80
+ async def upload_file(
81
+ api_client: AuthenticatedClient,
82
+ file_name: str,
83
+ context_path: str,
84
+ url: str,
85
+ ignore_patterns: List[str],
86
+ resolve_symlinks: bool,
87
+ stack_trace: Optional[TracebackType],
88
+ ):
89
+ try:
90
+ tar_buffer = tar_file_stream(
91
+ file_name, context_path, ignore_patterns, resolve_symlinks
92
+ )
93
+
94
+ client = api_client.get_async_httpx_client()
95
+ response = await client.put(url, content=tar_buffer.getvalue())
96
+ response.raise_for_status()
97
+ except httpx.HTTPStatusError as e:
98
+ raise FileUploadException(f"Failed to upload file: {e}").with_traceback(
99
+ stack_trace
100
+ )
101
+ except Exception as e:
102
+ raise FileUploadException(f"Failed to upload file: {e}").with_traceback(
103
+ stack_trace
104
+ )
105
+
106
+
107
+ async def trigger_build(
108
+ client: AuthenticatedClient,
109
+ template_id: str,
110
+ build_id: str,
111
+ template: TemplateType,
112
+ ) -> None:
113
+ # Convert template dict to TemplateBuildStartV2 model using from_dict
114
+ template_data = TemplateBuildStartV2.from_dict(template)
115
+
116
+ res = await post_v_2_templates_template_id_builds_build_id.asyncio_detailed(
117
+ template_id=template_id,
118
+ build_id=build_id,
119
+ client=client,
120
+ body=template_data,
121
+ )
122
+
123
+ if res.status_code >= 300:
124
+ raise handle_api_exception(res, BuildException)
125
+
126
+
127
+ async def get_build_status(
128
+ client: AuthenticatedClient, template_id: str, build_id: str, logs_offset: int
129
+ ) -> TemplateBuild:
130
+ res = await get_templates_template_id_builds_build_id_status.asyncio_detailed(
131
+ template_id=template_id,
132
+ build_id=build_id,
133
+ client=client,
134
+ logs_offset=logs_offset,
135
+ )
136
+
137
+ if res.status_code >= 300:
138
+ raise handle_api_exception(res, BuildException)
139
+
140
+ if isinstance(res.parsed, Error):
141
+ raise BuildException(f"API error: {res.parsed.message}")
142
+
143
+ if res.parsed is None:
144
+ raise BuildException("Failed to get build status")
145
+
146
+ return res.parsed
147
+
148
+
149
+ async def wait_for_build_finish(
150
+ client: AuthenticatedClient,
151
+ template_id: str,
152
+ build_id: str,
153
+ on_build_logs: Optional[Callable[[LogEntry], None]] = None,
154
+ logs_refresh_frequency: float = 0.2,
155
+ stack_traces: List[Union[TracebackType, None]] = [],
156
+ ):
157
+ logs_offset = 0
158
+ status: Literal["building", "waiting", "ready", "error"] = "building"
159
+
160
+ while status in ["building", "waiting"]:
161
+ build_status = await get_build_status(
162
+ client, template_id, build_id, logs_offset
163
+ )
164
+
165
+ logs_offset += len(build_status.log_entries)
166
+
167
+ for log_entry in build_status.log_entries:
168
+ if on_build_logs:
169
+ on_build_logs(
170
+ LogEntry(
171
+ timestamp=log_entry.timestamp,
172
+ level=log_entry.level.value,
173
+ message=log_entry.message,
174
+ )
175
+ )
176
+
177
+ status = build_status.status.value
178
+
179
+ if status == "ready":
180
+ return
181
+
182
+ elif status == "waiting":
183
+ pass
184
+
185
+ elif status == "error":
186
+ traceback = None
187
+ if build_status.reason and build_status.reason.step:
188
+ # Find the corresponding stack trace for the failed step
189
+ step_index = get_build_step_index(
190
+ build_status.reason.step, len(stack_traces)
191
+ )
192
+ if step_index < len(stack_traces):
193
+ traceback = stack_traces[step_index]
194
+
195
+ raise BuildException(
196
+ build_status.reason.message if build_status.reason else "Build failed"
197
+ ).with_traceback(traceback)
198
+
199
+ # Wait for a short period before checking the status again
200
+ await asyncio.sleep(logs_refresh_frequency)
201
+
202
+ raise BuildException("Unknown build error occurred.")
@@ -0,0 +1,366 @@
1
+ import os
2
+ from datetime import datetime
3
+ from typing import Callable, Optional
4
+
5
+ from moru.api.client.client import AuthenticatedClient
6
+ from moru.connection_config import ConnectionConfig
7
+ from moru.template.consts import RESOLVE_SYMLINKS
8
+ from moru.template.logger import LogEntry, LogEntryEnd, LogEntryStart
9
+ from moru.template.main import TemplateBase, TemplateClass
10
+ from moru.template.types import BuildInfo, InstructionType
11
+ from moru.template.utils import read_dockerignore
12
+
13
+ from .build_api import (
14
+ get_build_status,
15
+ get_file_upload_link,
16
+ request_build,
17
+ trigger_build,
18
+ upload_file,
19
+ wait_for_build_finish,
20
+ )
21
+ from moru.api.client_async import get_api_client
22
+
23
+
24
+ class AsyncTemplate(TemplateBase):
25
+ """
26
+ Asynchronous template builder for Moru sandboxes.
27
+ """
28
+
29
+ @staticmethod
30
+ async def _build(
31
+ template: TemplateClass,
32
+ api_client: AuthenticatedClient,
33
+ alias: str,
34
+ cpu_count: int = 2,
35
+ memory_mb: int = 1024,
36
+ skip_cache: bool = False,
37
+ on_build_logs: Optional[Callable[[LogEntry], None]] = None,
38
+ ) -> BuildInfo:
39
+ """
40
+ Internal implementation of the template build process
41
+
42
+ :param template: The template to build
43
+ :param api_client: Authenticated API client
44
+ :param alias: Alias name for the template
45
+ :param cpu_count: Number of CPUs allocated to the sandbox
46
+ :param memory_mb: Amount of memory in MB allocated to the sandbox
47
+ :param skip_cache: If True, forces a complete rebuild ignoring cache
48
+ :param on_build_logs: Callback function to receive build logs during the build process
49
+ """
50
+ if skip_cache:
51
+ template._template._force = True
52
+
53
+ # Create template
54
+ if on_build_logs:
55
+ on_build_logs(
56
+ LogEntry(
57
+ timestamp=datetime.now(),
58
+ level="info",
59
+ message=f"Requesting build for template: {alias}",
60
+ )
61
+ )
62
+
63
+ response = await request_build(
64
+ api_client,
65
+ name=alias,
66
+ cpu_count=cpu_count,
67
+ memory_mb=memory_mb,
68
+ )
69
+
70
+ template_id = response.template_id
71
+ build_id = response.build_id
72
+
73
+ if on_build_logs:
74
+ on_build_logs(
75
+ LogEntry(
76
+ timestamp=datetime.now(),
77
+ level="info",
78
+ message=f"Template created with ID: {template_id}, Build ID: {build_id}",
79
+ )
80
+ )
81
+
82
+ instructions_with_hashes = template._template._instructions_with_hashes()
83
+
84
+ # Upload files
85
+ for index, file_upload in enumerate(instructions_with_hashes):
86
+ if file_upload["type"] != InstructionType.COPY:
87
+ continue
88
+
89
+ args = file_upload.get("args", [])
90
+ src = args[0] if len(args) > 0 else None
91
+ force_upload = file_upload.get("forceUpload")
92
+ files_hash = file_upload.get("filesHash", None)
93
+ resolve_symlinks = file_upload.get("resolveSymlinks", RESOLVE_SYMLINKS)
94
+
95
+ if src is None or files_hash is None:
96
+ raise ValueError("Source path and files hash are required")
97
+
98
+ stack_trace = None
99
+ if index + 1 < len(template._template._stack_traces):
100
+ stack_trace = template._template._stack_traces[index + 1]
101
+
102
+ file_info = await get_file_upload_link(
103
+ api_client, template_id, files_hash, stack_trace
104
+ )
105
+
106
+ if (force_upload and file_info.url) or (
107
+ file_info.present is False and file_info.url
108
+ ):
109
+ await upload_file(
110
+ api_client,
111
+ src,
112
+ template._template._file_context_path,
113
+ file_info.url,
114
+ [
115
+ *template._template._file_ignore_patterns,
116
+ *read_dockerignore(template._template._file_context_path),
117
+ ],
118
+ resolve_symlinks,
119
+ stack_trace,
120
+ )
121
+ if on_build_logs:
122
+ on_build_logs(
123
+ LogEntry(
124
+ timestamp=datetime.now(),
125
+ level="info",
126
+ message=f"Uploaded '{src}'",
127
+ )
128
+ )
129
+ else:
130
+ if on_build_logs:
131
+ on_build_logs(
132
+ LogEntry(
133
+ timestamp=datetime.now(),
134
+ level="info",
135
+ message=f"Skipping upload of '{src}', already cached",
136
+ )
137
+ )
138
+
139
+ if on_build_logs:
140
+ on_build_logs(
141
+ LogEntry(
142
+ timestamp=datetime.now(),
143
+ level="info",
144
+ message="All file uploads completed",
145
+ )
146
+ )
147
+
148
+ # Start build
149
+ if on_build_logs:
150
+ on_build_logs(
151
+ LogEntry(
152
+ timestamp=datetime.now(),
153
+ level="info",
154
+ message="Starting building...",
155
+ )
156
+ )
157
+
158
+ await trigger_build(
159
+ api_client,
160
+ template_id,
161
+ build_id,
162
+ template._template._serialize(instructions_with_hashes),
163
+ )
164
+
165
+ return BuildInfo(
166
+ alias=alias,
167
+ template_id=template_id,
168
+ build_id=build_id,
169
+ )
170
+
171
+ @staticmethod
172
+ async def build(
173
+ template: TemplateClass,
174
+ alias: str,
175
+ cpu_count: int = 2,
176
+ memory_mb: int = 1024,
177
+ skip_cache: bool = False,
178
+ on_build_logs: Optional[Callable[[LogEntry], None]] = None,
179
+ api_key: Optional[str] = None,
180
+ domain: Optional[str] = None,
181
+ ) -> BuildInfo:
182
+ """
183
+ Build and deploy a template to Moru infrastructure.
184
+
185
+ :param template: The template to build
186
+ :param alias: Alias name for the template
187
+ :param cpu_count: Number of CPUs allocated to the sandbox
188
+ :param memory_mb: Amount of memory in MB allocated to the sandbox
189
+ :param skip_cache: If True, forces a complete rebuild ignoring cache
190
+ :param on_build_logs: Callback function to receive build logs during the build process
191
+ :param api_key: Moru API key for authentication
192
+ :param domain: Domain of the Moru API
193
+
194
+ Example
195
+ ```python
196
+ from moru import AsyncTemplate
197
+
198
+ template = (
199
+ AsyncTemplate()
200
+ .from_python_image('3')
201
+ .copy('requirements.txt', '/home/user/')
202
+ .run_cmd('pip install -r /home/user/requirements.txt')
203
+ )
204
+
205
+ await AsyncTemplate.build(
206
+ template,
207
+ alias='my-python-env',
208
+ cpu_count=2,
209
+ memory_mb=1024
210
+ )
211
+ ```
212
+ """
213
+ try:
214
+ if on_build_logs:
215
+ on_build_logs(
216
+ LogEntryStart(
217
+ timestamp=datetime.now(),
218
+ message="Build started",
219
+ )
220
+ )
221
+
222
+ domain = domain or os.environ.get("MORU_DOMAIN", "moru.io")
223
+ config = ConnectionConfig(
224
+ domain=domain, api_key=api_key or os.environ.get("MORU_API_KEY")
225
+ )
226
+ api_client = get_api_client(
227
+ config,
228
+ require_api_key=True,
229
+ require_access_token=False,
230
+ )
231
+
232
+ data = await AsyncTemplate._build(
233
+ template,
234
+ api_client,
235
+ alias,
236
+ cpu_count,
237
+ memory_mb,
238
+ skip_cache,
239
+ on_build_logs,
240
+ )
241
+
242
+ if on_build_logs:
243
+ on_build_logs(
244
+ LogEntry(
245
+ timestamp=datetime.now(),
246
+ level="info",
247
+ message="Waiting for logs...",
248
+ )
249
+ )
250
+
251
+ await wait_for_build_finish(
252
+ api_client,
253
+ data.template_id,
254
+ data.build_id,
255
+ on_build_logs,
256
+ logs_refresh_frequency=TemplateBase._logs_refresh_frequency,
257
+ stack_traces=template._template._stack_traces,
258
+ )
259
+
260
+ return data
261
+ finally:
262
+ if on_build_logs:
263
+ on_build_logs(
264
+ LogEntryEnd(
265
+ timestamp=datetime.now(),
266
+ message="Build finished",
267
+ )
268
+ )
269
+
270
+ @staticmethod
271
+ async def build_in_background(
272
+ template: TemplateClass,
273
+ alias: str,
274
+ cpu_count: int = 2,
275
+ memory_mb: int = 1024,
276
+ skip_cache: bool = False,
277
+ on_build_logs: Optional[Callable[[LogEntry], None]] = None,
278
+ api_key: Optional[str] = None,
279
+ domain: Optional[str] = None,
280
+ ) -> BuildInfo:
281
+ """
282
+ Build and deploy a template to Moru infrastructure without waiting for completion.
283
+
284
+ :param template: The template to build
285
+ :param alias: Alias name for the template
286
+ :param cpu_count: Number of CPUs allocated to the sandbox
287
+ :param memory_mb: Amount of memory in MB allocated to the sandbox
288
+ :param skip_cache: If True, forces a complete rebuild ignoring cache
289
+ :param api_key: Moru API key for authentication
290
+ :param domain: Domain of the Moru API
291
+ :return: BuildInfo containing the template ID and build ID
292
+
293
+ Example
294
+ ```python
295
+ from moru import AsyncTemplate
296
+
297
+ template = (
298
+ AsyncTemplate()
299
+ .from_python_image('3')
300
+ .run_cmd('echo "test"')
301
+ .set_start_cmd('echo "Hello"', 'sleep 1')
302
+ )
303
+
304
+ build_info = await AsyncTemplate.build_in_background(
305
+ template,
306
+ alias='my-python-env',
307
+ cpu_count=2,
308
+ memory_mb=1024
309
+ )
310
+ ```
311
+ """
312
+ domain = domain or os.environ.get("MORU_DOMAIN", "moru.io")
313
+ config = ConnectionConfig(
314
+ domain=domain, api_key=api_key or os.environ.get("MORU_API_KEY")
315
+ )
316
+ api_client = get_api_client(
317
+ config,
318
+ require_api_key=True,
319
+ require_access_token=False,
320
+ )
321
+
322
+ return await AsyncTemplate._build(
323
+ template,
324
+ api_client,
325
+ alias,
326
+ cpu_count,
327
+ memory_mb,
328
+ skip_cache,
329
+ on_build_logs,
330
+ )
331
+
332
+ @staticmethod
333
+ async def get_build_status(
334
+ build_info: BuildInfo,
335
+ logs_offset: int = 0,
336
+ api_key: Optional[str] = None,
337
+ domain: Optional[str] = None,
338
+ ):
339
+ """
340
+ Get the status of a build.
341
+
342
+ :param build_info: Build identifiers returned from build_in_background
343
+ :param logs_offset: Offset for fetching logs
344
+ :param api_key: Moru API key for authentication
345
+ :param domain: Domain of the Moru API
346
+ :return: TemplateBuild containing the build status and logs
347
+
348
+ Example
349
+ ```python
350
+ from moru import AsyncTemplate
351
+
352
+ build_info = await AsyncTemplate.build_in_background(template, alias='my-template')
353
+ status = await AsyncTemplate.get_build_status(build_info, logs_offset=0)
354
+ ```
355
+ """
356
+ domain = domain or os.environ.get("MORU_DOMAIN", "moru.io")
357
+ config = ConnectionConfig(
358
+ domain=domain, api_key=api_key or os.environ.get("MORU_API_KEY")
359
+ )
360
+ api_client = get_api_client(config)
361
+ return await get_build_status(
362
+ api_client,
363
+ build_info.template_id,
364
+ build_info.build_id,
365
+ logs_offset,
366
+ )