modal 0.73.106__py3-none-any.whl → 0.73.107__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.
@@ -0,0 +1,72 @@
1
+ # Copyright Modal Labs 2025
2
+ import asyncio
3
+ from typing import Optional
4
+
5
+ from modal.config import logger
6
+ from modal_proto import api_pb2
7
+
8
+
9
+ async def run_command_fallible(args: list[str]) -> Optional[str]:
10
+ try:
11
+ process = await asyncio.create_subprocess_exec(
12
+ *args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
13
+ )
14
+ stdout_bytes, _ = await process.communicate()
15
+
16
+ if process.returncode != 0:
17
+ logger.debug(f"Command {args} exited with code {process.returncode}")
18
+ return None
19
+
20
+ return stdout_bytes.decode("utf-8").strip()
21
+
22
+ except Exception as e:
23
+ logger.debug(f"Command {args} failed", exc_info=e)
24
+ return None
25
+
26
+
27
+ async def get_git_commit_info() -> Optional[api_pb2.CommitInfo]:
28
+ """Collect git information about the current repository asynchronously."""
29
+ git_info: api_pb2.CommitInfo = api_pb2.CommitInfo(vcs="git")
30
+
31
+ commands = [
32
+ # Get commit hash, timestamp, author name, and author email
33
+ ["git", "log", "-1", "--format=%H%n%ct%n%an%n%ae", "HEAD"],
34
+ # Get branch name
35
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
36
+ # Check if working directory is dirty
37
+ ["git", "status", "--porcelain"],
38
+ ["git", "remote", "get-url", "origin"],
39
+ ]
40
+
41
+ tasks = (run_command_fallible(cmd) for cmd in commands)
42
+ (log_info, branch, status, origin_url) = await asyncio.gather(*tasks)
43
+
44
+ if not branch:
45
+ return None
46
+
47
+ git_info.branch = branch
48
+
49
+ if not log_info:
50
+ return None
51
+
52
+ info_lines = log_info.split("\n")
53
+ if len(info_lines) < 4:
54
+ # If we didn't get all expected lines, bail
55
+ logger.debug(f"Log info returned only {len(info_lines)} lines")
56
+ return None
57
+
58
+ try:
59
+ git_info.commit_hash = info_lines[0]
60
+ git_info.commit_timestamp = int(info_lines[1])
61
+ git_info.author_name = info_lines[2]
62
+ git_info.author_email = info_lines[3]
63
+ except (ValueError, IndexError):
64
+ logger.debug(f"Failed to parse git log info: {log_info}")
65
+ return None
66
+
67
+ git_info.dirty = bool(status)
68
+
69
+ if origin_url:
70
+ git_info.repo_url = origin_url
71
+
72
+ return git_info
modal/client.pyi CHANGED
@@ -31,7 +31,7 @@ class _Client:
31
31
  server_url: str,
32
32
  client_type: int,
33
33
  credentials: typing.Optional[tuple[str, str]],
34
- version: str = "0.73.106",
34
+ version: str = "0.73.107",
35
35
  ): ...
36
36
  def is_closed(self) -> bool: ...
37
37
  @property
@@ -93,7 +93,7 @@ class Client:
93
93
  server_url: str,
94
94
  client_type: int,
95
95
  credentials: typing.Optional[tuple[str, str]],
96
- version: str = "0.73.106",
96
+ version: str = "0.73.107",
97
97
  ): ...
98
98
  def is_closed(self) -> bool: ...
99
99
  @property
modal/functions.pyi CHANGED
@@ -198,11 +198,11 @@ class Function(
198
198
 
199
199
  _call_generator_nowait: ___call_generator_nowait_spec[typing_extensions.Self]
200
200
 
201
- class __remote_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
201
+ class __remote_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER, SUPERSELF]):
202
202
  def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER: ...
203
203
  async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER: ...
204
204
 
205
- remote: __remote_spec[modal._functions.P, modal._functions.ReturnType, typing_extensions.Self]
205
+ remote: __remote_spec[modal._functions.ReturnType, modal._functions.P, typing_extensions.Self]
206
206
 
207
207
  class __remote_gen_spec(typing_extensions.Protocol[SUPERSELF]):
208
208
  def __call__(self, *args, **kwargs) -> typing.Generator[typing.Any, None, None]: ...
@@ -217,19 +217,19 @@ class Function(
217
217
  self, *args: modal._functions.P.args, **kwargs: modal._functions.P.kwargs
218
218
  ) -> modal._functions.OriginalReturnType: ...
219
219
 
220
- class ___experimental_spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
220
+ class ___experimental_spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER, SUPERSELF]):
221
221
  def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
222
222
  async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
223
223
 
224
224
  _experimental_spawn: ___experimental_spawn_spec[
225
- modal._functions.P, modal._functions.ReturnType, typing_extensions.Self
225
+ modal._functions.ReturnType, modal._functions.P, typing_extensions.Self
226
226
  ]
227
227
 
228
- class __spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
228
+ class __spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER, SUPERSELF]):
229
229
  def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
230
230
  async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
231
231
 
232
- spawn: __spawn_spec[modal._functions.P, modal._functions.ReturnType, typing_extensions.Self]
232
+ spawn: __spawn_spec[modal._functions.ReturnType, modal._functions.P, typing_extensions.Self]
233
233
 
234
234
  def get_raw_f(self) -> collections.abc.Callable[..., typing.Any]: ...
235
235
 
modal/runner.py CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright Modal Labs 2022
1
+ # Copyright Modal Labs 2025
2
2
  import asyncio
3
3
  import dataclasses
4
4
  import os
@@ -23,6 +23,7 @@ from ._runtime.execution_context import is_local
23
23
  from ._traceback import print_server_warnings, traceback_contains_remote_call
24
24
  from ._utils.async_utils import TaskContext, gather_cancel_on_exc, synchronize_api
25
25
  from ._utils.deprecation import deprecation_error
26
+ from ._utils.git_utils import get_git_commit_info
26
27
  from ._utils.grpc_utils import retry_transient_errors
27
28
  from ._utils.name_utils import check_object_name, is_valid_tag
28
29
  from .client import HEARTBEAT_INTERVAL, HEARTBEAT_TIMEOUT, _Client
@@ -182,6 +183,7 @@ async def _publish_app(
182
183
  classes: dict[str, _Cls],
183
184
  name: str = "", # Only relevant for deployments
184
185
  tag: str = "", # Only relevant for deployments
186
+ commit_info: Optional[api_pb2.CommitInfo] = None, # Git commit information
185
187
  ) -> tuple[str, list[api_pb2.Warning]]:
186
188
  """Wrapper for AppPublish RPC."""
187
189
 
@@ -195,7 +197,9 @@ async def _publish_app(
195
197
  function_ids=running_app.function_ids,
196
198
  class_ids=running_app.class_ids,
197
199
  definition_ids=definition_ids,
200
+ commit_info=commit_info,
198
201
  )
202
+
199
203
  try:
200
204
  response = await retry_transient_errors(client.stub.AppPublish, request)
201
205
  except GRPCError as exc:
@@ -521,6 +525,9 @@ async def _deploy_app(
521
525
 
522
526
  t0 = time.time()
523
527
 
528
+ # Get git information to track deployment history
529
+ commit_info_task = asyncio.create_task(get_git_commit_info())
530
+
524
531
  running_app: RunningApp = await _init_local_app_from_name(
525
532
  client, name, namespace, environment_name=environment_name
526
533
  )
@@ -542,8 +549,21 @@ async def _deploy_app(
542
549
  environment_name=environment_name,
543
550
  )
544
551
 
552
+ commit_info = None
553
+ try:
554
+ commit_info = await commit_info_task
555
+ except Exception as e:
556
+ logger.debug("Failed to get git commit info", exc_info=e)
557
+
545
558
  app_url, warnings = await _publish_app(
546
- client, running_app, api_pb2.APP_STATE_DEPLOYED, app._functions, app._classes, name, tag
559
+ client,
560
+ running_app,
561
+ api_pb2.APP_STATE_DEPLOYED,
562
+ app._functions,
563
+ app._classes,
564
+ name,
565
+ tag,
566
+ commit_info,
547
567
  )
548
568
  except Exception as e:
549
569
  # Note that AppClientDisconnect only stops the app if it's still initializing, and is a no-op otherwise.
modal/runner.pyi CHANGED
@@ -41,6 +41,7 @@ async def _publish_app(
41
41
  classes: dict[str, modal.cls._Cls],
42
42
  name: str = "",
43
43
  tag: str = "",
44
+ commit_info: typing.Optional[modal_proto.api_pb2.CommitInfo] = None,
44
45
  ) -> tuple[str, list[modal_proto.api_pb2.Warning]]: ...
45
46
  async def _disconnect(client: modal.client._Client, app_id: str, reason: int, exc_str: str = "") -> None: ...
46
47
  async def _status_based_disconnect(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: modal
3
- Version: 0.73.106
3
+ Version: 0.73.107
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -22,7 +22,7 @@ modal/app.py,sha256=ojhuLZuNZAQ1OsbDH0k6G4pm1W7bOIvZfXbaKlvQ-Ao,45622
22
22
  modal/app.pyi,sha256=tZFbcsu20SuvfB2puxCyuXLFNJ9bQulzag55rVpgZmc,26827
23
23
  modal/call_graph.py,sha256=1g2DGcMIJvRy-xKicuf63IVE98gJSnQsr8R_NVMptNc,2581
24
24
  modal/client.py,sha256=j9D3hNis1lfhnz9lVFGgJgowbH3PaGUzNKgHPWYG778,15372
25
- modal/client.pyi,sha256=kTCqdHcf_pCO2ELg7v7uxaCnGJjAxH5-EJXA90yimbI,7661
25
+ modal/client.pyi,sha256=zk_fqo5hUeiYvbtxSBT5tnvf9bjiFZVBtNGH7Rltxzc,7661
26
26
  modal/cloud_bucket_mount.py,sha256=YOe9nnvSr4ZbeCn587d7_VhE9IioZYRvF9VYQTQux08,5914
27
27
  modal/cloud_bucket_mount.pyi,sha256=30T3K1a89l6wzmEJ_J9iWv9SknoGqaZDx59Xs-ZQcmk,1607
28
28
  modal/cls.py,sha256=JhDbaZZHN52lqA_roY1BCbcN9BvbkUcdXiM2Kg9lIc0,31717
@@ -41,7 +41,7 @@ modal/file_io.py,sha256=lcMs_E9Xfm0YX1t9U2wNIBPnqHRxmImqjLW1GHqVmyg,20945
41
41
  modal/file_io.pyi,sha256=NTRft1tbPSWf9TlWVeZmTlgB5AZ_Zhu2srWIrWr7brk,9445
42
42
  modal/file_pattern_matcher.py,sha256=trosX-Bp7dOubudN1bLLhRAoidWy1TcoaR4Pv8CedWw,6497
43
43
  modal/functions.py,sha256=kcNHvqeGBxPI7Cgd57NIBBghkfbeFJzXO44WW0jSmao,325
44
- modal/functions.pyi,sha256=ujc6eIYyNmMn__4dpxEy85-vZmAniZv56D2A4uBgs6U,14377
44
+ modal/functions.pyi,sha256=D-PDJfSbwqMDXdq7Bxu2ErZRENo-tRgu_zPoB-jl0OU,14377
45
45
  modal/gpu.py,sha256=Kbhs_u49FaC2Zi0TjCdrpstpRtT5eZgecynmQi5IZVE,6752
46
46
  modal/image.py,sha256=o9wnpB03agSoErzh-odrKU2BprROxRZOWGJidjZeeds,91811
47
47
  modal/image.pyi,sha256=DQ4DLOCPr6_yV7z4LS0bTY0rOyvQP9-dQOrzaW7pPG8,25260
@@ -64,8 +64,8 @@ modal/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
64
64
  modal/queue.py,sha256=OIYmve1a4GTP54Vj2CcLatLPIAWToU7hWBNeu7IJiBY,18985
65
65
  modal/queue.pyi,sha256=sgvELCK4bJXMZIZw7gllooGFZNipGjI3BT4rmUuyD9M,10282
66
66
  modal/retries.py,sha256=HKR2Q9aNPWkMjQ5nwobqYTuZaSuw0a8lI2zrtY5IW98,5230
67
- modal/runner.py,sha256=fdUyDGN-bWu_aZBvxBO_MIgEuucsA0PgDKDHBn5k8J0,24451
68
- modal/runner.pyi,sha256=RYEYsnofrvVroYefWLhWAy8I_uwXV9fRNuJaVgcNzrg,5278
67
+ modal/runner.py,sha256=2VoA5jR9JwgPlmaGFWWOsSCGivS9Pv4-e-YFP9l92iE,25073
68
+ modal/runner.pyi,sha256=HW2pvC_PLwg1Es_EkrfQgMZsktIr9zzVEtmjOVFG6Dw,5351
69
69
  modal/running_app.py,sha256=v61mapYNV1-O-Uaho5EfJlryMLvIT9We0amUOSvSGx8,1188
70
70
  modal/sandbox.py,sha256=bE7nLMge7TEjNFWo_7W4dRft-jYW9AqWHzCjiSi-Afs,32427
71
71
  modal/sandbox.pyi,sha256=cLmSwI1ab-2DgEuXNf6S1PiK63wfUR9dHtxlZtSOuX8,22719
@@ -99,6 +99,7 @@ modal/_utils/bytes_io_segment_payload.py,sha256=uunxVJS4PE1LojF_UpURMzVK9GuvmYWR
99
99
  modal/_utils/deprecation.py,sha256=EXP1beU4pmEqEzWMLw6E3kUfNfpmNA_VOp6i0EHi93g,4856
100
100
  modal/_utils/docker_utils.py,sha256=h1uETghR40mp_y3fSWuZAfbIASH1HMzuphJHghAL6DU,3722
101
101
  modal/_utils/function_utils.py,sha256=OqsmLKOqSenxkXiNSPUWpKqD5KotySa_Omw1tmt97K0,27380
102
+ modal/_utils/git_utils.py,sha256=-bTeR3RCZydqduVwmraBlQG11KHvzsY5H48VKS8coGQ,2202
102
103
  modal/_utils/grpc_testing.py,sha256=H1zHqthv19eGPJz2HKXDyWXWGSqO4BRsxah3L5Xaa8A,8619
103
104
  modal/_utils/grpc_utils.py,sha256=wmMydVKN9YbugTwUXuOuzxbpzYvxkTDaFRxlBtIDE_0,8526
104
105
  modal/_utils/hash_utils.py,sha256=zg3J6OGxTFGSFri1qQ12giDz90lWk8bzaxCTUCRtiX4,3034
@@ -152,10 +153,10 @@ modal_docs/mdmd/__init__.py,sha256=svYKtV8HDwDCN86zbdWqyq5T8sMdGDj0PVlzc2tIxDM,2
152
153
  modal_docs/mdmd/mdmd.py,sha256=Irx49MCCTlBOP4FBdLR--JrpA3-WhsVeriq0LGgsRic,6232
153
154
  modal_docs/mdmd/signatures.py,sha256=XJaZrK7Mdepk5fdX51A8uENiLFNil85Ud0d4MH8H5f0,3218
154
155
  modal_proto/__init__.py,sha256=MIEP8jhXUeGq_eCjYFcqN5b1bxBM4fdk0VESpjWR0fc,28
155
- modal_proto/api.proto,sha256=0koajsaKlUO0ALWiqEv7dXWuioWZEhZWdDdAcBn9edk,86993
156
+ modal_proto/api.proto,sha256=mFNn1_wjeGCXhuact35aPBT8csBfEebQv4q0c9BTVB8,87354
156
157
  modal_proto/api_grpc.py,sha256=FYGqDegM_w_qxdtlxum8k31mDibKoMvmNxv_p9cKdKs,109056
157
- modal_proto/api_pb2.py,sha256=KFU1G0t9pgv9D1Ge-Gc5QxBXPfF_ABf2gyfD0lgyDcM,312874
158
- modal_proto/api_pb2.pyi,sha256=iWlQJkaR0fS5SZJQ8_e-dc6Hq_GgyNuP1_YZMMvgFXg,422349
158
+ modal_proto/api_pb2.py,sha256=dCXCNx6x7PJogjWkpC4U2i3Xx-itbyY4jNkVKOmsQbU,313828
159
+ modal_proto/api_pb2.pyi,sha256=L4BRZZi50OFroqDBpQz6jYoQTT6zinY42acCxjmHpKw,424645
159
160
  modal_proto/api_pb2_grpc.py,sha256=DNp0Et5i_Ey4dKx_1o1LRtYhyWYyT0NzTcAY4EcHn-c,235765
160
161
  modal_proto/api_pb2_grpc.pyi,sha256=RI6tWC3L8EIN4-izFSEGPPJl5Ta0lXPNuHUJaWAr35s,54892
161
162
  modal_proto/modal_api_grpc.py,sha256=UG8WJU81afrWPwItWB4Ag64E9EpyREMpBbAVGVEYJiM,14550
@@ -169,10 +170,10 @@ modal_proto/options_pb2_grpc.pyi,sha256=CImmhxHsYnF09iENPoe8S4J-n93jtgUYD2JPAc0y
169
170
  modal_proto/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
170
171
  modal_version/__init__.py,sha256=wiJQ53c-OMs0Xf1UeXOxQ7FwlV1VzIjnX6o-pRYZ_Pk,470
171
172
  modal_version/__main__.py,sha256=2FO0yYQQwDTh6udt1h-cBnGd1c4ZyHnHSI4BksxzVac,105
172
- modal_version/_version_generated.py,sha256=6gmddUoETCE2rVPQH83wYghpFuNpnKSHUheKCIXM2Vk,150
173
- modal-0.73.106.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
174
- modal-0.73.106.dist-info/METADATA,sha256=0vj17m8hM1gmNyGB3y2hmzXRR0I-qKncrbxRB0dsRBE,2453
175
- modal-0.73.106.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
176
- modal-0.73.106.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
177
- modal-0.73.106.dist-info/top_level.txt,sha256=4BWzoKYREKUZ5iyPzZpjqx4G8uB5TWxXPDwibLcVa7k,43
178
- modal-0.73.106.dist-info/RECORD,,
173
+ modal_version/_version_generated.py,sha256=nfjrv63ua8knwBgcK91Et-6cu5foto7KNinAK4c83q4,150
174
+ modal-0.73.107.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
175
+ modal-0.73.107.dist-info/METADATA,sha256=9XBg9dtDboOAYuWQOiDTTTLi-R4KZc_3lWKjB_o-2-c,2453
176
+ modal-0.73.107.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
177
+ modal-0.73.107.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
178
+ modal-0.73.107.dist-info/top_level.txt,sha256=4BWzoKYREKUZ5iyPzZpjqx4G8uB5TWxXPDwibLcVa7k,43
179
+ modal-0.73.107.dist-info/RECORD,,
modal_proto/api.proto CHANGED
@@ -309,6 +309,7 @@ message AppDeploymentHistory {
309
309
  string tag = 6;
310
310
  uint32 rollback_version = 7;
311
311
  bool rollback_allowed = 8;
312
+ optional CommitInfo commit_info = 10;
312
313
  }
313
314
 
314
315
  message AppDeploymentHistoryRequest {
@@ -419,6 +420,7 @@ message AppPublishRequest {
419
420
  map<string, string> definition_ids = 7; // function_id -> definition_id
420
421
  uint32 rollback_version = 8; // Unused by client, but used internally
421
422
  string client_version = 9; // Unused by client, but used internally
423
+ CommitInfo commit_info = 10; // Git information for deployment tracking
422
424
  }
423
425
 
424
426
  message AppPublishResponse {
@@ -745,6 +747,17 @@ message CloudBucketMount {
745
747
  optional string oidc_auth_role_arn = 9;
746
748
  }
747
749
 
750
+ message CommitInfo {
751
+ string vcs = 1; // Only git is supported for now
752
+ string branch = 2;
753
+ string commit_hash = 3;
754
+ int64 commit_timestamp = 4;
755
+ bool dirty = 5;
756
+ string author_name = 6;
757
+ string author_email = 7;
758
+ string repo_url = 8;
759
+ }
760
+
748
761
  message ContainerArguments { // This is used to pass data from the worker to the container
749
762
  string task_id = 1;
750
763
  string function_id = 2;