modal 0.72.50__py3-none-any.whl → 0.72.52__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.
modal/__init__.py CHANGED
@@ -31,6 +31,7 @@ try:
31
31
  from .schedule import Cron, Period
32
32
  from .scheduler_placement import SchedulerPlacement
33
33
  from .secret import Secret
34
+ from .snapshot import SandboxSnapshot
34
35
  from .volume import Volume
35
36
  except Exception:
36
37
  print()
@@ -60,6 +61,7 @@ __all__ = [
60
61
  "Retries",
61
62
  "CloudBucketMount",
62
63
  "Sandbox",
64
+ "SandboxSnapshot",
63
65
  "SchedulerPlacement",
64
66
  "Secret",
65
67
  "Stub",
modal/client.pyi CHANGED
@@ -27,7 +27,7 @@ class _Client:
27
27
  _snapshotted: bool
28
28
 
29
29
  def __init__(
30
- self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.72.50"
30
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.72.52"
31
31
  ): ...
32
32
  def is_closed(self) -> bool: ...
33
33
  @property
@@ -85,7 +85,7 @@ class Client:
85
85
  _snapshotted: bool
86
86
 
87
87
  def __init__(
88
- self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.72.50"
88
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.72.52"
89
89
  ): ...
90
90
  def is_closed(self) -> bool: ...
91
91
  @property
modal/sandbox.py CHANGED
@@ -36,6 +36,7 @@ from .network_file_system import _NetworkFileSystem, network_file_system_mount_p
36
36
  from .proxy import _Proxy
37
37
  from .scheduler_placement import SchedulerPlacement
38
38
  from .secret import _Secret
39
+ from .snapshot import _SandboxSnapshot
39
40
  from .stream_type import StreamType
40
41
 
41
42
  _default_image: _Image = _Image.debian_slim()
@@ -58,6 +59,7 @@ class _Sandbox(_Object, type_prefix="sb"):
58
59
  _stdin: _StreamWriter
59
60
  _task_id: Optional[str] = None
60
61
  _tunnels: Optional[dict[int, Tunnel]] = None
62
+ _enable_snapshot: bool = False
61
63
 
62
64
  @staticmethod
63
65
  def _new(
@@ -81,6 +83,7 @@ class _Sandbox(_Object, type_prefix="sb"):
81
83
  unencrypted_ports: Sequence[int] = [],
82
84
  proxy: Optional[_Proxy] = None,
83
85
  _experimental_scheduler_placement: Optional[SchedulerPlacement] = None,
86
+ enable_snapshot: bool = False,
84
87
  ) -> "_Sandbox":
85
88
  """mdmd:hidden"""
86
89
 
@@ -177,6 +180,7 @@ class _Sandbox(_Object, type_prefix="sb"):
177
180
  open_ports=api_pb2.PortSpecs(ports=open_ports),
178
181
  network_access=network_access,
179
182
  proxy_id=(proxy.object_id if proxy else None),
183
+ enable_snapshot=enable_snapshot,
180
184
  )
181
185
 
182
186
  # Note - `resolver.app_id` will be `None` for app-less sandboxes
@@ -224,6 +228,8 @@ class _Sandbox(_Object, type_prefix="sb"):
224
228
  unencrypted_ports: Sequence[int] = [],
225
229
  # Reference to a Modal Proxy to use in front of this Sandbox.
226
230
  proxy: Optional[_Proxy] = None,
231
+ # Enable memory snapshots.
232
+ _experimental_enable_snapshot: bool = False,
227
233
  _experimental_scheduler_placement: Optional[
228
234
  SchedulerPlacement
229
235
  ] = None, # Experimental controls over fine-grained scheduling (alpha).
@@ -261,7 +267,9 @@ class _Sandbox(_Object, type_prefix="sb"):
261
267
  unencrypted_ports=unencrypted_ports,
262
268
  proxy=proxy,
263
269
  _experimental_scheduler_placement=_experimental_scheduler_placement,
270
+ enable_snapshot=_experimental_enable_snapshot,
264
271
  )
272
+ obj._enable_snapshot = _experimental_enable_snapshot
265
273
 
266
274
  app_id: Optional[str] = None
267
275
  app_client: Optional[_Client] = None
@@ -534,6 +542,54 @@ class _Sandbox(_Object, type_prefix="sb"):
534
542
  by_line = bufsize == 1
535
543
  return _ContainerProcess(resp.exec_id, self._client, stdout=stdout, stderr=stderr, text=text, by_line=by_line)
536
544
 
545
+ async def _experimental_snapshot(self) -> _SandboxSnapshot:
546
+ if not self._enable_snapshot:
547
+ raise ValueError(
548
+ "Memory snapshots are not supported for this sandbox. To enable memory snapshots, "
549
+ "set `_experimental_enable_snapshot=True` when creating the sandbox."
550
+ )
551
+ await self._get_task_id()
552
+ snap_req = api_pb2.SandboxSnapshotRequest(sandbox_id=self.object_id)
553
+ snap_resp = await retry_transient_errors(self._client.stub.SandboxSnapshot, snap_req)
554
+
555
+ snapshot_id = snap_resp.snapshot_id
556
+
557
+ # wait for the snapshot to succeed. this is implemented as a second idempotent rpc
558
+ # because the snapshot itself may take a while to complete.
559
+ wait_req = api_pb2.SandboxSnapshotWaitRequest(snapshot_id=snapshot_id, timeout=55.0)
560
+ wait_resp = await retry_transient_errors(self._client.stub.SandboxSnapshotWait, wait_req)
561
+ if wait_resp.result.status != api_pb2.GenericResult.GENERIC_STATUS_SUCCESS:
562
+ raise ExecutionError(wait_resp.result.exception)
563
+
564
+ async def _load(self: _SandboxSnapshot, resolver: Resolver, existing_object_id: Optional[str]):
565
+ # we eagerly hydrate the sandbox snapshot below
566
+ pass
567
+
568
+ rep = "SandboxSnapshot()"
569
+ obj = _SandboxSnapshot._from_loader(_load, rep, hydrate_lazily=True)
570
+ obj._hydrate(snapshot_id, self._client, None)
571
+
572
+ return obj
573
+
574
+ @staticmethod
575
+ async def _experimental_from_snapshot(snapshot: _SandboxSnapshot, client: Optional[_Client] = None):
576
+ client = client or await _Client.from_env()
577
+
578
+ restore_req = api_pb2.SandboxRestoreRequest(snapshot_id=snapshot.object_id)
579
+ restore_resp: api_pb2.SandboxRestoreResponse = await retry_transient_errors(
580
+ client.stub.SandboxRestore, restore_req
581
+ )
582
+ sandbox = await _Sandbox.from_id(restore_resp.sandbox_id, client)
583
+
584
+ task_id_req = api_pb2.SandboxGetTaskIdRequest(sandbox_id=restore_resp.sandbox_id)
585
+ resp = await retry_transient_errors(client.stub.SandboxGetTaskId, task_id_req)
586
+ if resp.task_result.status not in [
587
+ api_pb2.GenericResult.GENERIC_STATUS_UNSPECIFIED,
588
+ api_pb2.GenericResult.GENERIC_STATUS_SUCCESS,
589
+ ]:
590
+ raise ExecutionError(resp.task_result.exception)
591
+ return sandbox
592
+
537
593
  @overload
538
594
  async def open(
539
595
  self,
modal/sandbox.pyi CHANGED
@@ -17,6 +17,7 @@ import modal.object
17
17
  import modal.proxy
18
18
  import modal.scheduler_placement
19
19
  import modal.secret
20
+ import modal.snapshot
20
21
  import modal.stream_type
21
22
  import modal.volume
22
23
  import modal_proto.api_pb2
@@ -31,6 +32,7 @@ class _Sandbox(modal._object._Object):
31
32
  _stdin: modal.io_streams._StreamWriter
32
33
  _task_id: typing.Optional[str]
33
34
  _tunnels: typing.Optional[dict[int, modal._tunnel.Tunnel]]
35
+ _enable_snapshot: bool
34
36
 
35
37
  @staticmethod
36
38
  def _new(
@@ -57,6 +59,7 @@ class _Sandbox(modal._object._Object):
57
59
  unencrypted_ports: collections.abc.Sequence[int] = [],
58
60
  proxy: typing.Optional[modal.proxy._Proxy] = None,
59
61
  _experimental_scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement] = None,
62
+ enable_snapshot: bool = False,
60
63
  ) -> _Sandbox: ...
61
64
  @staticmethod
62
65
  async def create(
@@ -84,6 +87,7 @@ class _Sandbox(modal._object._Object):
84
87
  encrypted_ports: collections.abc.Sequence[int] = [],
85
88
  unencrypted_ports: collections.abc.Sequence[int] = [],
86
89
  proxy: typing.Optional[modal.proxy._Proxy] = None,
90
+ _experimental_enable_snapshot: bool = False,
87
91
  _experimental_scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement] = None,
88
92
  client: typing.Optional[modal.client._Client] = None,
89
93
  ) -> _Sandbox: ...
@@ -125,6 +129,11 @@ class _Sandbox(modal._object._Object):
125
129
  bufsize: typing.Literal[-1, 1] = -1,
126
130
  _pty_info: typing.Optional[modal_proto.api_pb2.PTYInfo] = None,
127
131
  ) -> modal.container_process._ContainerProcess[bytes]: ...
132
+ async def _experimental_snapshot(self) -> modal.snapshot._SandboxSnapshot: ...
133
+ @staticmethod
134
+ async def _experimental_from_snapshot(
135
+ snapshot: modal.snapshot._SandboxSnapshot, client: typing.Optional[modal.client._Client] = None
136
+ ): ...
128
137
  @typing.overload
129
138
  async def open(self, path: str, mode: _typeshed.OpenTextMode) -> modal.file_io._FileIO[str]: ...
130
139
  @typing.overload
@@ -164,6 +173,7 @@ class Sandbox(modal.object.Object):
164
173
  _stdin: modal.io_streams.StreamWriter
165
174
  _task_id: typing.Optional[str]
166
175
  _tunnels: typing.Optional[dict[int, modal._tunnel.Tunnel]]
176
+ _enable_snapshot: bool
167
177
 
168
178
  def __init__(self, *args, **kwargs): ...
169
179
  @staticmethod
@@ -190,6 +200,7 @@ class Sandbox(modal.object.Object):
190
200
  unencrypted_ports: collections.abc.Sequence[int] = [],
191
201
  proxy: typing.Optional[modal.proxy.Proxy] = None,
192
202
  _experimental_scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement] = None,
203
+ enable_snapshot: bool = False,
193
204
  ) -> Sandbox: ...
194
205
 
195
206
  class __create_spec(typing_extensions.Protocol):
@@ -221,6 +232,7 @@ class Sandbox(modal.object.Object):
221
232
  encrypted_ports: collections.abc.Sequence[int] = [],
222
233
  unencrypted_ports: collections.abc.Sequence[int] = [],
223
234
  proxy: typing.Optional[modal.proxy.Proxy] = None,
235
+ _experimental_enable_snapshot: bool = False,
224
236
  _experimental_scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement] = None,
225
237
  client: typing.Optional[modal.client.Client] = None,
226
238
  ) -> Sandbox: ...
@@ -252,6 +264,7 @@ class Sandbox(modal.object.Object):
252
264
  encrypted_ports: collections.abc.Sequence[int] = [],
253
265
  unencrypted_ports: collections.abc.Sequence[int] = [],
254
266
  proxy: typing.Optional[modal.proxy.Proxy] = None,
267
+ _experimental_enable_snapshot: bool = False,
255
268
  _experimental_scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement] = None,
256
269
  client: typing.Optional[modal.client.Client] = None,
257
270
  ) -> Sandbox: ...
@@ -368,6 +381,22 @@ class Sandbox(modal.object.Object):
368
381
 
369
382
  exec: __exec_spec[typing_extensions.Self]
370
383
 
384
+ class ___experimental_snapshot_spec(typing_extensions.Protocol[SUPERSELF]):
385
+ def __call__(self) -> modal.snapshot.SandboxSnapshot: ...
386
+ async def aio(self) -> modal.snapshot.SandboxSnapshot: ...
387
+
388
+ _experimental_snapshot: ___experimental_snapshot_spec[typing_extensions.Self]
389
+
390
+ class ___experimental_from_snapshot_spec(typing_extensions.Protocol):
391
+ def __call__(
392
+ self, snapshot: modal.snapshot.SandboxSnapshot, client: typing.Optional[modal.client.Client] = None
393
+ ): ...
394
+ async def aio(
395
+ self, snapshot: modal.snapshot.SandboxSnapshot, client: typing.Optional[modal.client.Client] = None
396
+ ): ...
397
+
398
+ _experimental_from_snapshot: ___experimental_from_snapshot_spec
399
+
371
400
  class __open_spec(typing_extensions.Protocol[SUPERSELF]):
372
401
  @typing.overload
373
402
  def __call__(self, path: str, mode: _typeshed.OpenTextMode) -> modal.file_io.FileIO[str]: ...
modal/snapshot.py ADDED
@@ -0,0 +1,36 @@
1
+ # Copyright Modal Labs 2024
2
+ from typing import Optional
3
+
4
+ from modal_proto import api_pb2
5
+
6
+ from ._object import _Object
7
+ from ._resolver import Resolver
8
+ from ._utils.async_utils import synchronize_api
9
+ from ._utils.grpc_utils import retry_transient_errors
10
+ from .client import _Client
11
+
12
+
13
+ class _SandboxSnapshot(_Object, type_prefix="sn"):
14
+ """A `SandboxSnapshot` object lets you interact with a stored Sandbox snapshot that was created by calling
15
+ .snapshot() on a Sandbox instance. This includes both the filesystem and memory state of the original Sandbox at the
16
+ time the snapshot was taken.
17
+ """
18
+
19
+ @staticmethod
20
+ async def from_id(sandbox_snapshot_id: str, client: Optional[_Client] = None):
21
+ if client is None:
22
+ client = await _Client.from_env()
23
+
24
+ async def _load(self: _SandboxSnapshot, resolver: Resolver, existing_object_id: Optional[str]):
25
+ await retry_transient_errors(
26
+ client.stub.SandboxSnapshotGet, api_pb2.SandboxSnapshotGetRequest(snapshot_id=sandbox_snapshot_id)
27
+ )
28
+
29
+ rep = "SandboxSnapshot()"
30
+ obj = _SandboxSnapshot._from_loader(_load, rep)
31
+ obj._hydrate(sandbox_snapshot_id, client, None)
32
+
33
+ return obj
34
+
35
+
36
+ SandboxSnapshot = synchronize_api(_SandboxSnapshot)
modal/snapshot.pyi ADDED
@@ -0,0 +1,18 @@
1
+ import modal._object
2
+ import modal.client
3
+ import modal.object
4
+ import typing
5
+ import typing_extensions
6
+
7
+ class _SandboxSnapshot(modal._object._Object):
8
+ @staticmethod
9
+ async def from_id(sandbox_snapshot_id: str, client: typing.Optional[modal.client._Client] = None): ...
10
+
11
+ class SandboxSnapshot(modal.object.Object):
12
+ def __init__(self, *args, **kwargs): ...
13
+
14
+ class __from_id_spec(typing_extensions.Protocol):
15
+ def __call__(self, sandbox_snapshot_id: str, client: typing.Optional[modal.client.Client] = None): ...
16
+ async def aio(self, sandbox_snapshot_id: str, client: typing.Optional[modal.client.Client] = None): ...
17
+
18
+ from_id: __from_id_spec
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: modal
3
- Version: 0.72.50
3
+ Version: 0.72.52
4
4
  Summary: Python client library for Modal
5
5
  Author: Modal Labs
6
6
  Author-email: support@modal.com
@@ -1,4 +1,4 @@
1
- modal/__init__.py,sha256=3NJLLHb0TRc2tc68kf8NHzORx38GbtbZvPEWDWrQ6N4,2234
1
+ modal/__init__.py,sha256=df6aKAigSPFXnmIohWySf_1zZ9Gzgrb7-oprSbopD4w,2299
2
2
  modal/__main__.py,sha256=scYhGFqh8OJcVDo-VOxIT6CCwxOgzgflYWMnIZiMRqE,2871
3
3
  modal/_clustered_functions.py,sha256=kTf-9YBXY88NutC1akI-gCbvf01RhMPCw-zoOI_YIUE,2700
4
4
  modal/_clustered_functions.pyi,sha256=vllkegc99A0jrUOWa8mdlSbdp6uz36TsHhGxysAOpaQ,771
@@ -20,7 +20,7 @@ modal/app.py,sha256=KNfzLlkI2dJPl9LY8AgW76whZpwIvYKi2E2p9u4F3N4,43659
20
20
  modal/app.pyi,sha256=vnQhENaQBhJO6el-ieOcw3NEeYQ314SFXRDtjij4DM8,25324
21
21
  modal/call_graph.py,sha256=1g2DGcMIJvRy-xKicuf63IVE98gJSnQsr8R_NVMptNc,2581
22
22
  modal/client.py,sha256=8SQawr7P1PNUCq1UmJMUQXG2jIo4Nmdcs311XqrNLRE,15276
23
- modal/client.pyi,sha256=hI5zTvONMCzXLOTBTEGfXaj1_47IqxNkc-Xh0DzoM2Q,7593
23
+ modal/client.pyi,sha256=HDGFNW_BAgaraMYdEEDJS-_bDgPhZJ6sSoiyqR39kiQ,7593
24
24
  modal/cloud_bucket_mount.py,sha256=YOe9nnvSr4ZbeCn587d7_VhE9IioZYRvF9VYQTQux08,5914
25
25
  modal/cloud_bucket_mount.pyi,sha256=30T3K1a89l6wzmEJ_J9iWv9SknoGqaZDx59Xs-ZQcmk,1607
26
26
  modal/cls.py,sha256=7Czu5ff8Sezzl4ayHyI9nMgB0el3401_uHvyZU4vbhc,32206
@@ -65,14 +65,16 @@ modal/retries.py,sha256=HKR2Q9aNPWkMjQ5nwobqYTuZaSuw0a8lI2zrtY5IW98,5230
65
65
  modal/runner.py,sha256=0SCMgKO8lZ9W1C7le1EcgViKERMXpi_-QBd6PF_MH0Q,24450
66
66
  modal/runner.pyi,sha256=YmP4EOCNjjkwSIPi2Gl6hF_ji_ytkxz9dw3iB9KXaOI,5275
67
67
  modal/running_app.py,sha256=v61mapYNV1-O-Uaho5EfJlryMLvIT9We0amUOSvSGx8,1188
68
- modal/sandbox.py,sha256=h5bgvt98iFmQs4aBu_d2aR6qP5UM5QVdP3wgm_P7RgE,29014
69
- modal/sandbox.pyi,sha256=XtiDHYa4HOooyuEPmKFwEqO58AJBJVTceO_1HQyDwII,21343
68
+ modal/sandbox.py,sha256=D5Qs3FkatZ7Gbny22edGA2Rty7scPmKyLV_bmOFJISY,31750
69
+ modal/sandbox.pyi,sha256=qncEvzK76h_ehrs03vlroQyLThWiMsjKhD0DnCNc6zI,22663
70
70
  modal/schedule.py,sha256=0ZFpKs1bOxeo5n3HZjoL7OE2ktsb-_oGtq-WJEPO4tY,2615
71
71
  modal/scheduler_placement.py,sha256=BAREdOY5HzHpzSBqt6jDVR6YC_jYfHMVqOzkyqQfngU,1235
72
72
  modal/secret.py,sha256=XTqrpR4rnkuUb-bth3qM_xxPqqEWorSoN9qDwjWRcXo,10118
73
73
  modal/secret.pyi,sha256=W4g_BOSxafYm-K9PvFc7-li3a-rsCFNkYgHTZXr1AFA,2974
74
74
  modal/serving.py,sha256=MnVuTsimN05LfNPxuJZ4sr5s1_BPUkIsOP_VC-bkp78,4464
75
75
  modal/serving.pyi,sha256=ncV-9jY_vZYFnGs5ZnMb3ffrX8LmcLdIMHBC56xRbtE,1711
76
+ modal/snapshot.py,sha256=6rQvDP3iX9hdiAudKTy0-m0JESt4kk0q2gusXbaRA-8,1279
77
+ modal/snapshot.pyi,sha256=Ypd4NKsjOTnnnqXyTGGLKq5lkocRrUURYjY5Pi67_qA,670
76
78
  modal/stream_type.py,sha256=A6320qoAAWhEfwOCZfGtymQTu5AfLfJXXgARqooTPvY,417
77
79
  modal/token_flow.py,sha256=LcgSce_MSQ2p7j55DPwpVRpiAtCDe8GRSEwzO7muNR8,6774
78
80
  modal/token_flow.pyi,sha256=0XV3d-9CGQL3qjPdw3RgwIFVqqxo8Z-u044_mkgAM3o,2064
@@ -167,10 +169,10 @@ modal_proto/options_pb2_grpc.pyi,sha256=CImmhxHsYnF09iENPoe8S4J-n93jtgUYD2JPAc0y
167
169
  modal_proto/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
168
170
  modal_version/__init__.py,sha256=kGya2ZlItX2zB7oHORs-wvP4PG8lg_mtbi1QIK3G6SQ,470
169
171
  modal_version/__main__.py,sha256=2FO0yYQQwDTh6udt1h-cBnGd1c4ZyHnHSI4BksxzVac,105
170
- modal_version/_version_generated.py,sha256=m3uPcEhi0LYAEy2o0qMhVj7x8_1ffVAO3G7_vc6jNYg,149
171
- modal-0.72.50.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
172
- modal-0.72.50.dist-info/METADATA,sha256=IBmHUniLrQ90IbIsSaAiNKKfrHh38r4H6xhuG48lGzY,2329
173
- modal-0.72.50.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
174
- modal-0.72.50.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
175
- modal-0.72.50.dist-info/top_level.txt,sha256=1nvYbOSIKcmU50fNrpnQnrrOpj269ei3LzgB6j9xGqg,64
176
- modal-0.72.50.dist-info/RECORD,,
172
+ modal_version/_version_generated.py,sha256=XmFBa8biwTGGximrbyv2QEzak1dJNJxC1EXdL5FMTcQ,149
173
+ modal-0.72.52.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
174
+ modal-0.72.52.dist-info/METADATA,sha256=3NhsiuuaszkQV6xnPMO3aqYEMxCCMiEFL04guni-2d0,2329
175
+ modal-0.72.52.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
176
+ modal-0.72.52.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
177
+ modal-0.72.52.dist-info/top_level.txt,sha256=1nvYbOSIKcmU50fNrpnQnrrOpj269ei3LzgB6j9xGqg,64
178
+ modal-0.72.52.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  # Copyright Modal Labs 2025
2
2
 
3
3
  # Note: Reset this value to -1 whenever you make a minor `0.X` release of the client.
4
- build_number = 50 # git: 02c0966
4
+ build_number = 52 # git: fc4738b