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,199 @@
1
+ import time
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
+ def request_build(
29
+ client: AuthenticatedClient, name: str, cpu_count: int, memory_mb: int
30
+ ):
31
+ res = post_v3_templates.sync_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
+ 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 = get_templates_template_id_files_hash.sync_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
+ 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
+ client = api_client.get_httpx_client()
94
+ response = client.put(url, content=tar_buffer.getvalue())
95
+ response.raise_for_status()
96
+ except httpx.HTTPStatusError as e:
97
+ raise FileUploadException(f"Failed to upload file: {e}").with_traceback(
98
+ stack_trace
99
+ )
100
+ except Exception as e:
101
+ raise FileUploadException(f"Failed to upload file: {e}").with_traceback(
102
+ stack_trace
103
+ )
104
+
105
+
106
+ def trigger_build(
107
+ client: AuthenticatedClient,
108
+ template_id: str,
109
+ build_id: str,
110
+ template: TemplateType,
111
+ ) -> None:
112
+ # Convert template dict to TemplateBuildStartV2 model using from_dict
113
+ template_data = TemplateBuildStartV2.from_dict(template)
114
+
115
+ res = post_v_2_templates_template_id_builds_build_id.sync_detailed(
116
+ template_id=template_id,
117
+ build_id=build_id,
118
+ client=client,
119
+ body=template_data,
120
+ )
121
+
122
+ if res.status_code >= 300:
123
+ raise handle_api_exception(res, BuildException)
124
+
125
+
126
+ def get_build_status(
127
+ client: AuthenticatedClient, template_id: str, build_id: str, logs_offset: int
128
+ ) -> TemplateBuild:
129
+ res = get_templates_template_id_builds_build_id_status.sync_detailed(
130
+ template_id=template_id,
131
+ build_id=build_id,
132
+ client=client,
133
+ logs_offset=logs_offset,
134
+ )
135
+
136
+ if res.status_code >= 300:
137
+ raise handle_api_exception(res, BuildException)
138
+
139
+ if isinstance(res.parsed, Error):
140
+ raise BuildException(f"API error: {res.parsed.message}")
141
+
142
+ if res.parsed is None:
143
+ raise BuildException("Failed to get build status")
144
+
145
+ return res.parsed
146
+
147
+
148
+ def wait_for_build_finish(
149
+ client: AuthenticatedClient,
150
+ template_id: str,
151
+ build_id: str,
152
+ on_build_logs: Optional[Callable[[LogEntry], None]] = None,
153
+ logs_refresh_frequency: float = 0.2,
154
+ stack_traces: List[Union[TracebackType, None]] = [],
155
+ ):
156
+ logs_offset = 0
157
+ status: Literal["building", "waiting", "ready", "error"] = "building"
158
+
159
+ while status in ["building", "waiting"]:
160
+ build_status = get_build_status(client, template_id, build_id, logs_offset)
161
+
162
+ logs_offset += len(build_status.log_entries)
163
+
164
+ for log_entry in build_status.log_entries:
165
+ if on_build_logs:
166
+ on_build_logs(
167
+ LogEntry(
168
+ timestamp=log_entry.timestamp,
169
+ level=log_entry.level.value,
170
+ message=log_entry.message,
171
+ )
172
+ )
173
+
174
+ status = build_status.status.value
175
+
176
+ if status == "ready":
177
+ return
178
+
179
+ elif status == "waiting":
180
+ pass
181
+
182
+ elif status == "error":
183
+ traceback = None
184
+ if build_status.reason and build_status.reason.step:
185
+ # Find the corresponding stack trace for the failed step
186
+ step_index = get_build_step_index(
187
+ build_status.reason.step, len(stack_traces)
188
+ )
189
+ if step_index < len(stack_traces):
190
+ traceback = stack_traces[step_index]
191
+
192
+ raise BuildException(
193
+ build_status.reason.message if build_status.reason else "Build failed"
194
+ ).with_traceback(traceback)
195
+
196
+ # Wait for a short period before checking the status again
197
+ time.sleep(logs_refresh_frequency)
198
+
199
+ raise BuildException("Unknown build error occurred.")
@@ -0,0 +1,371 @@
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
+
8
+ from moru.api.client_sync import get_api_client
9
+ from moru.template.consts import RESOLVE_SYMLINKS
10
+ from moru.template.logger import LogEntry, LogEntryEnd, LogEntryStart
11
+ from moru.template.main import TemplateBase, TemplateClass
12
+ from moru.template.types import BuildInfo, InstructionType
13
+ from moru.template_sync.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.template.utils import read_dockerignore
22
+
23
+
24
+ class Template(TemplateBase):
25
+ """
26
+ Synchronous template builder for Moru sandboxes.
27
+ """
28
+
29
+ @staticmethod
30
+ 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 = 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 = 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
+ 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
+ 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
+ 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 Template
197
+
198
+ template = (
199
+ Template()
200
+ .from_python_image('3')
201
+ .copy('requirements.txt', '/home/user/')
202
+ .run_cmd('pip install -r /home/user/requirements.txt')
203
+ )
204
+
205
+ Template.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 = Template._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
+ 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
+ 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 Template
296
+
297
+ template = (
298
+ Template()
299
+ .from_python_image('3')
300
+ .run_cmd('echo "test"')
301
+ .set_start_cmd('echo "Hello"', 'sleep 1')
302
+ )
303
+
304
+ build_info = Template.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 Template._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
+ 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 Template
351
+
352
+ build_info = Template.build_in_background(template, alias='my-template')
353
+ status = Template.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(
361
+ config,
362
+ require_api_key=True,
363
+ require_access_token=False,
364
+ )
365
+
366
+ return get_build_status(
367
+ api_client,
368
+ build_info.template_id,
369
+ build_info.build_id,
370
+ logs_offset,
371
+ )
@@ -0,0 +1,63 @@
1
+ Metadata-Version: 2.4
2
+ Name: moru
3
+ Version: 0.1.0
4
+ Summary: Moru SDK that gives agents cloud environments
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Author: Moru AI
8
+ Author-email: hello@moru.ai
9
+ Requires-Python: >=3.9,<4.0
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Requires-Dist: attrs (>=23.2.0)
19
+ Requires-Dist: dockerfile-parse (>=2.0.1,<3.0.0)
20
+ Requires-Dist: httpcore (>=1.0.5,<2.0.0)
21
+ Requires-Dist: httpx (>=0.27.0,<1.0.0)
22
+ Requires-Dist: packaging (>=24.1)
23
+ Requires-Dist: protobuf (>=4.21.0)
24
+ Requires-Dist: python-dateutil (>=2.8.2)
25
+ Requires-Dist: rich (>=14.0.0)
26
+ Requires-Dist: typing-extensions (>=4.1.0)
27
+ Requires-Dist: wcmatch (>=10.1,<11.0)
28
+ Project-URL: Bug Tracker, https://github.com/moru-ai/sdks/issues
29
+ Project-URL: Homepage, https://github.com/moru-ai/sdks
30
+ Project-URL: Repository, https://github.com/moru-ai/sdks/tree/main/packages/python-sdk
31
+ Description-Content-Type: text/markdown
32
+
33
+ # Moru Python SDK
34
+
35
+ Moru SDK for Python provides cloud environments for AI agents.
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ pip install moru
41
+ ```
42
+
43
+ ## Quick Start
44
+
45
+ ### 1. Set your API key
46
+
47
+ ```bash
48
+ export MORU_API_KEY=your_api_key
49
+ ```
50
+
51
+ ### 2. Create a sandbox
52
+
53
+ ```python
54
+ from moru import Sandbox
55
+
56
+ with Sandbox() as sandbox:
57
+ sandbox.run_code('print("Hello from Moru!")')
58
+ ```
59
+
60
+ ## Acknowledgement
61
+
62
+ This project is a fork of [E2B](https://github.com/e2b-dev/E2B).
63
+