e2b 2.2.0__tar.gz → 2.2.2__tar.gz

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 (136) hide show
  1. {e2b-2.2.0 → e2b-2.2.2}/PKG-INFO +1 -1
  2. {e2b-2.2.0 → e2b-2.2.2}/e2b/__init__.py +3 -0
  3. {e2b-2.2.0 → e2b-2.2.2}/e2b/template/consts.py +2 -0
  4. {e2b-2.2.0 → e2b-2.2.2}/e2b/template/dockerfile_parser.py +4 -3
  5. {e2b-2.2.0 → e2b-2.2.2}/e2b/template/main.py +169 -126
  6. {e2b-2.2.0 → e2b-2.2.2}/e2b/template/types.py +8 -5
  7. {e2b-2.2.0 → e2b-2.2.2}/e2b/template/utils.py +35 -7
  8. {e2b-2.2.0 → e2b-2.2.2}/e2b/template_async/build_api.py +13 -8
  9. {e2b-2.2.0 → e2b-2.2.2}/e2b/template_async/main.py +4 -0
  10. {e2b-2.2.0 → e2b-2.2.2}/e2b/template_sync/build_api.py +12 -7
  11. {e2b-2.2.0 → e2b-2.2.2}/e2b/template_sync/main.py +4 -0
  12. {e2b-2.2.0 → e2b-2.2.2}/pyproject.toml +1 -1
  13. {e2b-2.2.0 → e2b-2.2.2}/LICENSE +0 -0
  14. {e2b-2.2.0 → e2b-2.2.2}/README.md +0 -0
  15. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/__init__.py +0 -0
  16. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/__init__.py +0 -0
  17. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/api/__init__.py +0 -0
  18. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/api/sandboxes/__init__.py +0 -0
  19. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/api/sandboxes/delete_sandboxes_sandbox_id.py +0 -0
  20. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/api/sandboxes/get_sandboxes.py +0 -0
  21. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/api/sandboxes/get_sandboxes_metrics.py +0 -0
  22. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/api/sandboxes/get_sandboxes_sandbox_id.py +0 -0
  23. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/api/sandboxes/get_sandboxes_sandbox_id_logs.py +0 -0
  24. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/api/sandboxes/get_sandboxes_sandbox_id_metrics.py +0 -0
  25. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/api/sandboxes/get_v2_sandboxes.py +0 -0
  26. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/api/sandboxes/post_sandboxes.py +0 -0
  27. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_pause.py +0 -0
  28. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_refreshes.py +0 -0
  29. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_resume.py +0 -0
  30. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_timeout.py +0 -0
  31. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/api/templates/__init__.py +0 -0
  32. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/api/templates/delete_templates_template_id.py +0 -0
  33. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/api/templates/get_templates.py +0 -0
  34. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/api/templates/get_templates_template_id_builds_build_id_status.py +0 -0
  35. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/api/templates/get_templates_template_id_files_hash.py +0 -0
  36. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/api/templates/patch_templates_template_id.py +0 -0
  37. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/api/templates/post_templates.py +0 -0
  38. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/api/templates/post_templates_template_id.py +0 -0
  39. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/api/templates/post_templates_template_id_builds_build_id.py +0 -0
  40. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/api/templates/post_v2_templates.py +0 -0
  41. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/api/templates/post_v_2_templates_template_id_builds_build_id.py +0 -0
  42. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/client.py +0 -0
  43. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/errors.py +0 -0
  44. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/__init__.py +0 -0
  45. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/aws_registry.py +0 -0
  46. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/aws_registry_type.py +0 -0
  47. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/build_log_entry.py +0 -0
  48. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/build_status_reason.py +0 -0
  49. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/created_access_token.py +0 -0
  50. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/created_team_api_key.py +0 -0
  51. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/disk_metrics.py +0 -0
  52. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/error.py +0 -0
  53. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/gcp_registry.py +0 -0
  54. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/gcp_registry_type.py +0 -0
  55. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/general_registry.py +0 -0
  56. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/general_registry_type.py +0 -0
  57. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/identifier_masking_details.py +0 -0
  58. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/listed_sandbox.py +0 -0
  59. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/log_level.py +0 -0
  60. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/new_access_token.py +0 -0
  61. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/new_sandbox.py +0 -0
  62. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/new_team_api_key.py +0 -0
  63. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/node.py +0 -0
  64. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/node_detail.py +0 -0
  65. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/node_metrics.py +0 -0
  66. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/node_status.py +0 -0
  67. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/node_status_change.py +0 -0
  68. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/post_sandboxes_sandbox_id_refreshes_body.py +0 -0
  69. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/post_sandboxes_sandbox_id_timeout_body.py +0 -0
  70. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/resumed_sandbox.py +0 -0
  71. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/sandbox.py +0 -0
  72. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/sandbox_detail.py +0 -0
  73. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/sandbox_log.py +0 -0
  74. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/sandbox_log_entry.py +0 -0
  75. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/sandbox_log_entry_fields.py +0 -0
  76. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/sandbox_logs.py +0 -0
  77. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/sandbox_metric.py +0 -0
  78. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/sandbox_state.py +0 -0
  79. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/sandboxes_with_metrics.py +0 -0
  80. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/team.py +0 -0
  81. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/team_api_key.py +0 -0
  82. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/team_metric.py +0 -0
  83. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/team_user.py +0 -0
  84. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/template.py +0 -0
  85. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/template_build.py +0 -0
  86. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/template_build_file_upload.py +0 -0
  87. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/template_build_request.py +0 -0
  88. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/template_build_request_v2.py +0 -0
  89. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/template_build_start_v2.py +0 -0
  90. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/template_build_status.py +0 -0
  91. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/template_step.py +0 -0
  92. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/template_update_request.py +0 -0
  93. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/models/update_team_api_key.py +0 -0
  94. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/py.typed +0 -0
  95. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/client/types.py +0 -0
  96. {e2b-2.2.0 → e2b-2.2.2}/e2b/api/metadata.py +0 -0
  97. {e2b-2.2.0 → e2b-2.2.2}/e2b/connection_config.py +0 -0
  98. {e2b-2.2.0 → e2b-2.2.2}/e2b/envd/api.py +0 -0
  99. {e2b-2.2.0 → e2b-2.2.2}/e2b/envd/filesystem/filesystem_connect.py +0 -0
  100. {e2b-2.2.0 → e2b-2.2.2}/e2b/envd/filesystem/filesystem_pb2.py +0 -0
  101. {e2b-2.2.0 → e2b-2.2.2}/e2b/envd/filesystem/filesystem_pb2.pyi +0 -0
  102. {e2b-2.2.0 → e2b-2.2.2}/e2b/envd/process/process_connect.py +0 -0
  103. {e2b-2.2.0 → e2b-2.2.2}/e2b/envd/process/process_pb2.py +0 -0
  104. {e2b-2.2.0 → e2b-2.2.2}/e2b/envd/process/process_pb2.pyi +0 -0
  105. {e2b-2.2.0 → e2b-2.2.2}/e2b/envd/rpc.py +0 -0
  106. {e2b-2.2.0 → e2b-2.2.2}/e2b/envd/versions.py +0 -0
  107. {e2b-2.2.0 → e2b-2.2.2}/e2b/exceptions.py +0 -0
  108. {e2b-2.2.0 → e2b-2.2.2}/e2b/sandbox/commands/command_handle.py +0 -0
  109. {e2b-2.2.0 → e2b-2.2.2}/e2b/sandbox/commands/main.py +0 -0
  110. {e2b-2.2.0 → e2b-2.2.2}/e2b/sandbox/filesystem/filesystem.py +0 -0
  111. {e2b-2.2.0 → e2b-2.2.2}/e2b/sandbox/filesystem/watch_handle.py +0 -0
  112. {e2b-2.2.0 → e2b-2.2.2}/e2b/sandbox/main.py +0 -0
  113. {e2b-2.2.0 → e2b-2.2.2}/e2b/sandbox/sandbox_api.py +0 -0
  114. {e2b-2.2.0 → e2b-2.2.2}/e2b/sandbox/signature.py +0 -0
  115. {e2b-2.2.0 → e2b-2.2.2}/e2b/sandbox/utils.py +0 -0
  116. {e2b-2.2.0 → e2b-2.2.2}/e2b/sandbox_async/commands/command.py +0 -0
  117. {e2b-2.2.0 → e2b-2.2.2}/e2b/sandbox_async/commands/command_handle.py +0 -0
  118. {e2b-2.2.0 → e2b-2.2.2}/e2b/sandbox_async/commands/pty.py +0 -0
  119. {e2b-2.2.0 → e2b-2.2.2}/e2b/sandbox_async/filesystem/filesystem.py +0 -0
  120. {e2b-2.2.0 → e2b-2.2.2}/e2b/sandbox_async/filesystem/watch_handle.py +0 -0
  121. {e2b-2.2.0 → e2b-2.2.2}/e2b/sandbox_async/main.py +0 -0
  122. {e2b-2.2.0 → e2b-2.2.2}/e2b/sandbox_async/paginator.py +0 -0
  123. {e2b-2.2.0 → e2b-2.2.2}/e2b/sandbox_async/sandbox_api.py +0 -0
  124. {e2b-2.2.0 → e2b-2.2.2}/e2b/sandbox_async/utils.py +0 -0
  125. {e2b-2.2.0 → e2b-2.2.2}/e2b/sandbox_sync/commands/command.py +0 -0
  126. {e2b-2.2.0 → e2b-2.2.2}/e2b/sandbox_sync/commands/command_handle.py +0 -0
  127. {e2b-2.2.0 → e2b-2.2.2}/e2b/sandbox_sync/commands/pty.py +0 -0
  128. {e2b-2.2.0 → e2b-2.2.2}/e2b/sandbox_sync/filesystem/filesystem.py +0 -0
  129. {e2b-2.2.0 → e2b-2.2.2}/e2b/sandbox_sync/filesystem/watch_handle.py +0 -0
  130. {e2b-2.2.0 → e2b-2.2.2}/e2b/sandbox_sync/main.py +0 -0
  131. {e2b-2.2.0 → e2b-2.2.2}/e2b/sandbox_sync/paginator.py +0 -0
  132. {e2b-2.2.0 → e2b-2.2.2}/e2b/sandbox_sync/sandbox_api.py +0 -0
  133. {e2b-2.2.0 → e2b-2.2.2}/e2b/template/exceptions.py +0 -0
  134. {e2b-2.2.0 → e2b-2.2.2}/e2b/template/readycmd.py +0 -0
  135. {e2b-2.2.0 → e2b-2.2.2}/e2b_connect/__init__.py +0 -0
  136. {e2b-2.2.0 → e2b-2.2.2}/e2b_connect/client.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: e2b
3
- Version: 2.2.0
3
+ Version: 2.2.2
4
4
  Summary: E2B SDK that give agents cloud environments
5
5
  Home-page: https://e2b.dev/
6
6
  License: MIT
@@ -71,6 +71,8 @@ from .sandbox_sync.paginator import SandboxPaginator
71
71
 
72
72
  from .template.main import TemplateBase, TemplateClass
73
73
 
74
+ from .template.types import CopyItem
75
+
74
76
  from .template_sync.main import Template
75
77
  from .template_async.main import AsyncTemplate
76
78
 
@@ -136,6 +138,7 @@ __all__ = [
136
138
  "AsyncTemplate",
137
139
  "TemplateBase",
138
140
  "TemplateClass",
141
+ "CopyItem",
139
142
  "wait_for_file",
140
143
  "wait_for_url",
141
144
  "wait_for_port",
@@ -7,3 +7,5 @@ Depths explained:
7
7
  2. Caller method (eg. copy(), fromImage(), etc.)
8
8
  """
9
9
  STACK_TRACE_DEPTH = 2
10
+
11
+ RESOLVE_SYMLINKS = False
@@ -1,7 +1,7 @@
1
1
  import os
2
2
  import re
3
3
  import tempfile
4
- from typing import Dict, List, Optional, Protocol, Union
4
+ from typing import Dict, List, Optional, Protocol, Union, Literal
5
5
 
6
6
  from dockerfile_parse import DockerfileParser
7
7
  from e2b.template.types import CopyItem
@@ -24,7 +24,8 @@ class DockerfileParserInterface(Protocol):
24
24
  self,
25
25
  src: Union[str, List[CopyItem]],
26
26
  dest: Optional[str] = None,
27
- force_upload: Optional[bool] = None,
27
+ force_upload: Optional[Literal[True]] = None,
28
+ resolve_symlinks: Optional[bool] = None,
28
29
  user: Optional[str] = None,
29
30
  mode: Optional[int] = None,
30
31
  ) -> "DockerfileParserInterface":
@@ -239,7 +240,7 @@ def _handle_env_instruction(
239
240
  def _handle_cmd_entrypoint_instruction(
240
241
  value: str, template_builder: DockerfileParserInterface
241
242
  ) -> None:
242
- """Handle CMD/ENTRYPOINT instruction - convert to setStartCmd with 20s timeout"""
243
+ """Handle CMD/ENTRYPOINT instruction - convert to set_start_cmd with 20s timeout"""
243
244
  if not value.strip():
244
245
  return
245
246
  command = value.strip()
@@ -1,10 +1,12 @@
1
1
  import json
2
- from typing import Dict, List, Optional, Union
3
- from types import TracebackType
2
+ from typing import Dict, List, Optional, Union, Literal
3
+ from pathlib import Path
4
+
4
5
  from httpx import Limits
5
6
 
6
- from e2b.template.consts import STACK_TRACE_DEPTH
7
+ from e2b.template.consts import STACK_TRACE_DEPTH, RESOLVE_SYMLINKS
7
8
  from e2b.template.dockerfile_parser import parse_dockerfile
9
+ from e2b.template.readycmd import ReadyCmd
8
10
  from e2b.template.types import (
9
11
  CopyItem,
10
12
  Instruction,
@@ -20,7 +22,7 @@ from e2b.template.utils import (
20
22
  read_gcp_service_account_json,
21
23
  get_caller_frame,
22
24
  )
23
- from e2b.template.readycmd import ReadyCmd
25
+ from types import TracebackType
24
26
 
25
27
 
26
28
  class TemplateBuilder:
@@ -29,62 +31,74 @@ class TemplateBuilder:
29
31
 
30
32
  def copy(
31
33
  self,
32
- src: Union[str, List[CopyItem]],
33
- dest: Optional[str] = None,
34
- force_upload: Optional[bool] = None,
34
+ src: Union[Union[str, Path], List[Union[str, Path]]],
35
+ dest: Union[str, Path],
36
+ force_upload: Optional[Literal[True]] = None,
35
37
  user: Optional[str] = None,
36
38
  mode: Optional[int] = None,
39
+ resolve_symlinks: Optional[bool] = None,
37
40
  ) -> "TemplateBuilder":
38
- if isinstance(src, str):
39
- # Single copy operation
40
- if dest is None:
41
- raise ValueError("dest parameter is required when src is a string")
42
- copy_items: List[CopyItem] = [
43
- {
44
- "src": src,
45
- "dest": dest,
46
- "forceUpload": force_upload,
47
- "user": user,
48
- "mode": mode,
49
- }
50
- ]
51
- else:
52
- # Multiple copy operations
53
- copy_items = src
41
+ srcs = [src] if isinstance(src, (str, Path)) else src
54
42
 
55
- for copy_item in copy_items:
43
+ for src_item in srcs:
56
44
  args = [
57
- copy_item["src"],
58
- copy_item["dest"],
45
+ str(src_item),
46
+ str(dest),
59
47
  user or "",
60
48
  pad_octal(mode) if mode else "",
61
49
  ]
62
50
 
63
- instruction: Instruction = Instruction(
64
- type=InstructionType.COPY,
65
- args=args,
66
- force=force_upload or self._template._force_next_layer,
67
- forceUpload=force_upload,
68
- )
51
+ instruction: Instruction = {
52
+ "type": InstructionType.COPY,
53
+ "args": args,
54
+ "force": force_upload or self._template._force_next_layer,
55
+ "forceUpload": force_upload,
56
+ "resolveSymlinks": resolve_symlinks,
57
+ }
58
+
69
59
  self._template._instructions.append(instruction)
60
+
70
61
  self._template._collect_stack_trace()
71
62
  return self
72
63
 
64
+ def copy_items(self, items: List[CopyItem]) -> "TemplateBuilder":
65
+ self._template._run_in_new_stack_trace_context(
66
+ lambda: [
67
+ self.copy(
68
+ item["src"],
69
+ item["dest"],
70
+ item.get("forceUpload"),
71
+ item.get("user"),
72
+ item.get("mode"),
73
+ item.get("resolveSymlinks"),
74
+ )
75
+ for item in items
76
+ ]
77
+ )
78
+ return self
79
+
73
80
  def remove(
74
- self, path: str, force: bool = False, recursive: bool = False
81
+ self,
82
+ path: Union[Union[str, Path], List[Union[str, Path]]],
83
+ force: bool = False,
84
+ recursive: bool = False,
75
85
  ) -> "TemplateBuilder":
76
- args = ["rm", path]
86
+ paths = [path] if isinstance(path, (str, Path)) else path
87
+ args = ["rm"]
77
88
  if recursive:
78
89
  args.append("-r")
79
90
  if force:
80
91
  args.append("-f")
92
+ args.extend([str(p) for p in paths])
81
93
 
82
94
  return self._template._run_in_new_stack_trace_context(
83
95
  lambda: self.run_cmd(" ".join(args))
84
96
  )
85
97
 
86
- def rename(self, src: str, dest: str, force: bool = False) -> "TemplateBuilder":
87
- args = ["mv", src, dest]
98
+ def rename(
99
+ self, src: Union[str, Path], dest: Union[str, Path], force: bool = False
100
+ ) -> "TemplateBuilder":
101
+ args = ["mv", str(src), str(dest)]
88
102
  if force:
89
103
  args.append("-f")
90
104
 
@@ -93,21 +107,24 @@ class TemplateBuilder:
93
107
  )
94
108
 
95
109
  def make_dir(
96
- self, paths: Union[str, List[str]], mode: Optional[int] = None
110
+ self,
111
+ path: Union[Union[str, Path], List[Union[str, Path]]],
112
+ mode: Optional[int] = None,
97
113
  ) -> "TemplateBuilder":
98
- if isinstance(paths, str):
99
- paths = [paths]
100
-
101
- args = ["mkdir", "-p", *paths]
114
+ path_list = [path] if isinstance(path, (str, Path)) else path
115
+ args = ["mkdir", "-p"]
102
116
  if mode:
103
117
  args.append(f"-m {pad_octal(mode)}")
118
+ args.extend([str(p) for p in path_list])
104
119
 
105
120
  return self._template._run_in_new_stack_trace_context(
106
121
  lambda: self.run_cmd(" ".join(args))
107
122
  )
108
123
 
109
- def make_symlink(self, src: str, dest: str) -> "TemplateBuilder":
110
- args = ["ln", "-s", src, dest]
124
+ def make_symlink(
125
+ self, src: Union[str, Path], dest: Union[str, Path]
126
+ ) -> "TemplateBuilder":
127
+ args = ["ln", "-s", str(src), str(dest)]
111
128
  return self._template._run_in_new_stack_trace_context(
112
129
  lambda: self.run_cmd(" ".join(args))
113
130
  )
@@ -121,34 +138,34 @@ class TemplateBuilder:
121
138
  if user:
122
139
  args.append(user)
123
140
 
124
- instruction: Instruction = Instruction(
125
- type=InstructionType.RUN,
126
- args=args,
127
- force=self._template._force_next_layer,
128
- forceUpload=None,
129
- )
141
+ instruction: Instruction = {
142
+ "type": InstructionType.RUN,
143
+ "args": args,
144
+ "force": self._template._force_next_layer,
145
+ "forceUpload": None,
146
+ }
130
147
  self._template._instructions.append(instruction)
131
148
  self._template._collect_stack_trace()
132
149
  return self
133
150
 
134
- def set_workdir(self, workdir: str) -> "TemplateBuilder":
135
- instruction: Instruction = Instruction(
136
- type=InstructionType.WORKDIR,
137
- args=[workdir],
138
- force=self._template._force_next_layer,
139
- forceUpload=None,
140
- )
151
+ def set_workdir(self, workdir: Union[str, Path]) -> "TemplateBuilder":
152
+ instruction: Instruction = {
153
+ "type": InstructionType.WORKDIR,
154
+ "args": [str(workdir)],
155
+ "force": self._template._force_next_layer,
156
+ "forceUpload": None,
157
+ }
141
158
  self._template._instructions.append(instruction)
142
159
  self._template._collect_stack_trace()
143
160
  return self
144
161
 
145
162
  def set_user(self, user: str) -> "TemplateBuilder":
146
- instruction: Instruction = Instruction(
147
- type=InstructionType.USER,
148
- args=[user],
149
- force=self._template._force_next_layer,
150
- forceUpload=None,
151
- )
163
+ instruction: Instruction = {
164
+ "type": InstructionType.USER,
165
+ "args": [user],
166
+ "force": self._template._force_next_layer,
167
+ "forceUpload": None,
168
+ }
152
169
  self._template._instructions.append(instruction)
153
170
  self._template._collect_stack_trace()
154
171
  return self
@@ -204,18 +221,18 @@ class TemplateBuilder:
204
221
  def git_clone(
205
222
  self,
206
223
  url: str,
207
- path: Optional[str] = None,
224
+ path: Optional[Union[str, Path]] = None,
208
225
  branch: Optional[str] = None,
209
226
  depth: Optional[int] = None,
210
227
  ) -> "TemplateBuilder":
211
228
  args = ["git", "clone", url]
212
- if path:
213
- args.append(path)
214
229
  if branch:
215
230
  args.append(f"--branch {branch}")
216
231
  args.append("--single-branch")
217
232
  if depth:
218
233
  args.append(f"--depth {depth}")
234
+ if path:
235
+ args.append(str(path))
219
236
  return self._template._run_in_new_stack_trace_context(
220
237
  lambda: self.run_cmd(" ".join(args))
221
238
  )
@@ -224,12 +241,12 @@ class TemplateBuilder:
224
241
  if len(envs) == 0:
225
242
  return self
226
243
 
227
- instruction: Instruction = Instruction(
228
- type=InstructionType.ENV,
229
- args=[item for key, value in envs.items() for item in [key, value]],
230
- force=self._template._force_next_layer,
231
- forceUpload=None,
232
- )
244
+ instruction: Instruction = {
245
+ "type": InstructionType.ENV,
246
+ "args": [item for key, value in envs.items() for item in [key, value]],
247
+ "force": self._template._force_next_layer,
248
+ "forceUpload": None,
249
+ }
233
250
  self._template._instructions.append(instruction)
234
251
  self._template._collect_stack_trace()
235
252
  return self
@@ -274,8 +291,8 @@ class TemplateBase:
274
291
 
275
292
  def __init__(
276
293
  self,
277
- file_context_path: Optional[str] = None,
278
- ignore_file_paths: Optional[List[str]] = None,
294
+ file_context_path: Optional[Union[str, Path]] = None,
295
+ file_ignore_patterns: Optional[List[str]] = None,
279
296
  ):
280
297
  self._default_base_image: str = "e2bdev/base"
281
298
  self._base_image: Optional[str] = self._default_base_image
@@ -289,11 +306,13 @@ class TemplateBase:
289
306
  self._force_next_layer: bool = False
290
307
  self._instructions: List[Instruction] = []
291
308
  # If no file_context_path is provided, use the caller's directory
292
- self._file_context_path: str = (
293
- file_context_path or get_caller_directory(STACK_TRACE_DEPTH) or "."
309
+ self._file_context_path = (
310
+ file_context_path.as_posix()
311
+ if isinstance(file_context_path, Path)
312
+ else (file_context_path or get_caller_directory(STACK_TRACE_DEPTH) or ".")
294
313
  )
295
- self._ignore_file_paths: List[str] = ignore_file_paths or []
296
- self._stack_traces: List[TracebackType] = []
314
+ self._file_ignore_patterns: List[str] = file_ignore_patterns or []
315
+ self._stack_traces: List[Union[TracebackType, None]] = []
297
316
  self._stack_traces_enabled: bool = True
298
317
 
299
318
  def skip_cache(self) -> "TemplateBase":
@@ -302,7 +321,7 @@ class TemplateBase:
302
321
  return self
303
322
 
304
323
  def _collect_stack_trace(
305
- self, stack_traces_depth: Optional[int] = STACK_TRACE_DEPTH
324
+ self, stack_traces_depth: int = STACK_TRACE_DEPTH
306
325
  ) -> "TemplateBase":
307
326
  """Collect stack trace if enabled"""
308
327
  if not self._stack_traces_enabled:
@@ -369,15 +388,22 @@ class TemplateBase:
369
388
  )
370
389
 
371
390
  def from_image(
372
- self, base_image: str, registry_config: Optional[RegistryConfig] = None
391
+ self,
392
+ image: str,
393
+ username: Optional[str] = None,
394
+ password: Optional[str] = None,
373
395
  ) -> TemplateBuilder:
374
396
  """Private method to set base image without adding stack trace"""
375
- self._base_image = base_image
397
+ self._base_image = image
376
398
  self._base_template = None
377
399
 
378
400
  # Set the registry config if provided
379
- if registry_config is not None:
380
- self._registry_config = registry_config
401
+ if username and password:
402
+ self._registry_config = {
403
+ "type": "registry",
404
+ "username": username,
405
+ "password": password,
406
+ }
381
407
 
382
408
  # If we should force the next layer and it's a FROM command, invalidate whole template
383
409
  if self._force_next_layer:
@@ -418,20 +444,6 @@ class TemplateBase:
418
444
  self._collect_stack_trace()
419
445
  return builder
420
446
 
421
- def from_registry(
422
- self, image: str, username: str, password: str
423
- ) -> TemplateBuilder:
424
- return self._run_in_new_stack_trace_context(
425
- lambda: self.from_image(
426
- image,
427
- registry_config={
428
- "type": "registry",
429
- "username": username,
430
- "password": password,
431
- },
432
- )
433
- )
434
-
435
447
  def from_aws_registry(
436
448
  self,
437
449
  image: str,
@@ -439,32 +451,44 @@ class TemplateBase:
439
451
  secret_access_key: str,
440
452
  region: str,
441
453
  ) -> TemplateBuilder:
442
- return self._run_in_new_stack_trace_context(
443
- lambda: self.from_image(
444
- image,
445
- registry_config={
446
- "type": "aws",
447
- "awsAccessKeyId": access_key_id,
448
- "awsSecretAccessKey": secret_access_key,
449
- "awsRegion": region,
450
- },
451
- )
452
- )
454
+ self._base_image = image
455
+ self._base_template = None
456
+
457
+ # Set the registry config if provided
458
+ self._registry_config = {
459
+ "type": "aws",
460
+ "awsAccessKeyId": access_key_id,
461
+ "awsSecretAccessKey": secret_access_key,
462
+ "awsRegion": region,
463
+ }
464
+
465
+ # If we should force the next layer and it's a FROM command, invalidate whole template
466
+ if self._force_next_layer:
467
+ self._force = True
468
+
469
+ self._collect_stack_trace()
470
+ return TemplateBuilder(self)
453
471
 
454
472
  def from_gcp_registry(
455
473
  self, image: str, service_account_json: Union[str, dict]
456
474
  ) -> TemplateBuilder:
457
- return self._run_in_new_stack_trace_context(
458
- lambda: self.from_image(
459
- image,
460
- registry_config={
461
- "type": "gcp",
462
- "serviceAccountJson": read_gcp_service_account_json(
463
- self._file_context_path, service_account_json
464
- ),
465
- },
466
- )
467
- )
475
+ self._base_image = image
476
+ self._base_template = None
477
+
478
+ # Set the registry config if provided
479
+ self._registry_config = {
480
+ "type": "gcp",
481
+ "serviceAccountJson": read_gcp_service_account_json(
482
+ self._file_context_path, service_account_json
483
+ ),
484
+ }
485
+
486
+ # If we should force the next layer and it's a FROM command, invalidate whole template
487
+ if self._force_next_layer:
488
+ self._force = True
489
+
490
+ self._collect_stack_trace()
491
+ return TemplateBuilder(self)
468
492
 
469
493
  @staticmethod
470
494
  def to_json(template: "TemplateClass") -> str:
@@ -504,12 +528,13 @@ class TemplateBase:
504
528
  steps: List[Instruction] = []
505
529
 
506
530
  for index, instruction in enumerate(self._instructions):
507
- step: Instruction = Instruction(
508
- type=instruction["type"].value,
509
- args=instruction["args"],
510
- force=instruction["force"],
511
- forceUpload=instruction.get("forceUpload"),
512
- )
531
+ step: Instruction = {
532
+ "type": instruction["type"],
533
+ "args": instruction["args"],
534
+ "force": instruction["force"],
535
+ "forceUpload": instruction.get("forceUpload"),
536
+ "resolveSymlinks": instruction.get("resolveSymlinks"),
537
+ }
513
538
 
514
539
  if instruction["type"] == InstructionType.COPY:
515
540
  stack_trace = None
@@ -527,9 +552,10 @@ class TemplateBase:
527
552
  dest,
528
553
  self._file_context_path,
529
554
  [
530
- *self._ignore_file_paths,
555
+ *self._file_ignore_patterns,
531
556
  *read_dockerignore(self._file_context_path),
532
557
  ],
558
+ instruction.get("resolveSymlinks", RESOLVE_SYMLINKS),
533
559
  stack_trace,
534
560
  )
535
561
 
@@ -538,8 +564,25 @@ class TemplateBase:
538
564
  return steps
539
565
 
540
566
  def _serialize(self, steps: List[Instruction]) -> TemplateType:
567
+ _steps: List[Instruction] = []
568
+
569
+ for _, instruction in enumerate(steps):
570
+ step: Instruction = {
571
+ "type": instruction.get("type"),
572
+ "args": instruction.get("args"),
573
+ "force": instruction.get("force"),
574
+ }
575
+
576
+ if instruction.get("filesHash") is not None:
577
+ step["filesHash"] = instruction["filesHash"]
578
+
579
+ if instruction.get("forceUpload") is not None:
580
+ step["forceUpload"] = instruction["forceUpload"]
581
+
582
+ _steps.append(step)
583
+
541
584
  template_data: TemplateType = {
542
- "steps": steps,
585
+ "steps": _steps,
543
586
  "force": self._force,
544
587
  }
545
588
 
@@ -4,10 +4,11 @@ from dataclasses import dataclass
4
4
  from datetime import datetime
5
5
  from typing import Literal
6
6
  from enum import Enum
7
+ from pathlib import Path
7
8
  from e2b.template.utils import strip_ansi_escape_codes
8
9
 
9
10
 
10
- class InstructionType(Enum):
11
+ class InstructionType(str, Enum):
11
12
  COPY = "COPY"
12
13
  ENV = "ENV"
13
14
  RUN = "RUN"
@@ -16,19 +17,21 @@ class InstructionType(Enum):
16
17
 
17
18
 
18
19
  class CopyItem(TypedDict):
19
- src: str
20
- dest: str
21
- forceUpload: NotRequired[Optional[bool]]
20
+ src: Union[Union[str, Path], List[Union[str, Path]]]
21
+ dest: Union[str, Path]
22
+ forceUpload: NotRequired[Optional[Literal[True]]]
22
23
  user: NotRequired[Optional[str]]
23
24
  mode: NotRequired[Optional[int]]
25
+ resolveSymlinks: NotRequired[Optional[bool]]
24
26
 
25
27
 
26
28
  class Instruction(TypedDict):
27
29
  type: InstructionType
28
30
  args: List[str]
29
31
  force: bool
30
- forceUpload: NotRequired[Optional[bool]]
32
+ forceUpload: NotRequired[Optional[Literal[True]]]
31
33
  filesHash: NotRequired[Optional[str]]
34
+ resolveSymlinks: NotRequired[Optional[bool]]
32
35
 
33
36
 
34
37
  @dataclass
@@ -1,6 +1,7 @@
1
1
  import hashlib
2
2
  import os
3
3
  import json
4
+ import stat
4
5
  from glob import glob
5
6
  import fnmatch
6
7
  import re
@@ -30,8 +31,9 @@ def calculate_files_hash(
30
31
  src: str,
31
32
  dest: str,
32
33
  context_path: str,
33
- ignore_patterns: Optional[List[str]] = None,
34
- stack_trace: Optional[TracebackType] = None,
34
+ ignore_patterns: List[str],
35
+ resolve_symlinks: bool,
36
+ stack_trace: Optional[TracebackType],
35
37
  ) -> str:
36
38
  src_path = os.path.join(context_path, src)
37
39
  hash_obj = hashlib.sha256()
@@ -52,12 +54,38 @@ def calculate_files_hash(
52
54
  if len(files) == 0:
53
55
  raise ValueError(f"No files found in {src_path}").with_traceback(stack_trace)
54
56
 
57
+ def hash_stats(stat_info: os.stat_result) -> None:
58
+ hash_obj.update(str(stat_info.st_mode).encode())
59
+ hash_obj.update(str(stat_info.st_uid).encode())
60
+ hash_obj.update(str(stat_info.st_gid).encode())
61
+ hash_obj.update(str(stat_info.st_size).encode())
62
+ hash_obj.update(str(stat_info.st_mtime).encode())
63
+
55
64
  for file in files:
56
- if not os.path.isfile(file):
57
- # skip folders
58
- continue
59
- with open(file, "rb") as f:
60
- hash_obj.update(f.read())
65
+ # Add a relative path to hash calculation
66
+ relative_path = os.path.relpath(file, context_path)
67
+ hash_obj.update(relative_path.encode())
68
+
69
+ # Add stat information to hash calculation
70
+ if os.path.islink(file):
71
+ stats = os.lstat(file)
72
+ should_follow = resolve_symlinks and (
73
+ os.path.isfile(file) or os.path.isdir(file)
74
+ )
75
+
76
+ if not should_follow:
77
+ hash_stats(stats)
78
+
79
+ content = os.readlink(file)
80
+ hash_obj.update(content.encode())
81
+ continue
82
+
83
+ stats = os.stat(file)
84
+ hash_stats(stats)
85
+
86
+ if stat.S_ISREG(stats.st_mode):
87
+ with open(file, "rb") as f:
88
+ hash_obj.update(f.read())
61
89
 
62
90
  return hash_obj.hexdigest()
63
91