modal 0.66.25__py3-none-any.whl → 0.66.35__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.
@@ -2,6 +2,8 @@
2
2
  # ruff: noqa: E402
3
3
  import os
4
4
 
5
+ from modal._runtime.user_code_imports import Service, import_class_service, import_single_function_service
6
+
5
7
  telemetry_socket = os.environ.get("MODAL_TELEMETRY_SOCKET")
6
8
  if telemetry_socket:
7
9
  from runtime._telemetry import instrument_imports
@@ -11,17 +13,13 @@ if telemetry_socket:
11
13
  import asyncio
12
14
  import base64
13
15
  import concurrent.futures
14
- import importlib
15
16
  import inspect
16
17
  import queue
17
18
  import signal
18
19
  import sys
19
20
  import threading
20
21
  import time
21
- import typing
22
- from abc import ABCMeta, abstractmethod
23
- from dataclasses import dataclass
24
- from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Sequence, Tuple
22
+ from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Sequence
25
23
 
26
24
  from google.protobuf.message import Message
27
25
 
@@ -30,37 +28,21 @@ from modal._proxy_tunnel import proxy_tunnel
30
28
  from modal._serialization import deserialize, deserialize_proto_params
31
29
  from modal._utils.async_utils import TaskContext, synchronizer
32
30
  from modal._utils.function_utils import (
33
- LocalFunctionError,
34
31
  callable_has_non_self_params,
35
- is_async as get_is_async,
36
- is_global_object,
37
32
  )
38
33
  from modal.app import App, _App
39
34
  from modal.client import Client, _Client
40
- from modal.cls import Cls, Obj
41
35
  from modal.config import logger
42
36
  from modal.exception import ExecutionError, InputCancellation, InvalidError
43
- from modal.functions import Function, _Function
44
37
  from modal.partial_function import (
45
38
  _find_callables_for_obj,
46
- _find_partial_methods_for_user_cls,
47
- _PartialFunction,
48
39
  _PartialFunctionFlags,
49
40
  )
50
41
  from modal.running_app import RunningApp
51
42
  from modal_proto import api_pb2
52
43
 
53
- from ._runtime.asgi import (
54
- asgi_app_wrapper,
55
- get_ip_address,
56
- wait_for_web_server,
57
- web_server_proxy,
58
- webhook_asgi_app,
59
- wsgi_app_wrapper,
60
- )
61
44
  from ._runtime.container_io_manager import (
62
45
  ContainerIOManager,
63
- FinalizedFunction,
64
46
  IOContext,
65
47
  UserException,
66
48
  _ContainerIOManager,
@@ -72,151 +54,6 @@ if TYPE_CHECKING:
72
54
  import modal.object
73
55
 
74
56
 
75
- def construct_webhook_callable(
76
- user_defined_callable: Callable,
77
- webhook_config: api_pb2.WebhookConfig,
78
- container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager",
79
- ):
80
- # For webhooks, the user function is used to construct an asgi app:
81
- if webhook_config.type == api_pb2.WEBHOOK_TYPE_ASGI_APP:
82
- # Function returns an asgi_app, which we can use as a callable.
83
- return asgi_app_wrapper(user_defined_callable(), container_io_manager)
84
-
85
- elif webhook_config.type == api_pb2.WEBHOOK_TYPE_WSGI_APP:
86
- # Function returns an wsgi_app, which we can use as a callable.
87
- return wsgi_app_wrapper(user_defined_callable(), container_io_manager)
88
-
89
- elif webhook_config.type == api_pb2.WEBHOOK_TYPE_FUNCTION:
90
- # Function is a webhook without an ASGI app. Create one for it.
91
- return asgi_app_wrapper(
92
- webhook_asgi_app(user_defined_callable, webhook_config.method, webhook_config.web_endpoint_docs),
93
- container_io_manager,
94
- )
95
-
96
- elif webhook_config.type == api_pb2.WEBHOOK_TYPE_WEB_SERVER:
97
- # Function spawns an HTTP web server listening at a port.
98
- user_defined_callable()
99
-
100
- # We intentionally try to connect to the external interface instead of the loopback
101
- # interface here so users are forced to expose the server. This allows us to potentially
102
- # change the implementation to use an external bridge in the future.
103
- host = get_ip_address(b"eth0")
104
- port = webhook_config.web_server_port
105
- startup_timeout = webhook_config.web_server_startup_timeout
106
- wait_for_web_server(host, port, timeout=startup_timeout)
107
- return asgi_app_wrapper(web_server_proxy(host, port), container_io_manager)
108
- else:
109
- raise InvalidError(f"Unrecognized web endpoint type {webhook_config.type}")
110
-
111
-
112
- class Service(metaclass=ABCMeta):
113
- """Common interface for singular functions and class-based "services"
114
-
115
- There are differences in the importing/finalization logic, and this
116
- "protocol"/abc basically defines a common interface for the two types
117
- of "Services" after the point of import.
118
- """
119
-
120
- user_cls_instance: Any
121
- app: Optional[_App]
122
- code_deps: Optional[List["modal.object._Object"]]
123
-
124
- @abstractmethod
125
- def get_finalized_functions(
126
- self, fun_def: api_pb2.Function, container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager"
127
- ) -> Dict[str, "FinalizedFunction"]:
128
- ...
129
-
130
-
131
- @dataclass
132
- class ImportedFunction(Service):
133
- user_cls_instance: Any
134
- app: Optional[_App]
135
- code_deps: Optional[List["modal.object._Object"]]
136
-
137
- _user_defined_callable: Callable[..., Any]
138
-
139
- def get_finalized_functions(
140
- self, fun_def: api_pb2.Function, container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager"
141
- ) -> Dict[str, "FinalizedFunction"]:
142
- # Check this property before we turn it into a method (overriden by webhooks)
143
- is_async = get_is_async(self._user_defined_callable)
144
- # Use the function definition for whether this is a generator (overriden by webhooks)
145
- is_generator = fun_def.function_type == api_pb2.Function.FUNCTION_TYPE_GENERATOR
146
-
147
- webhook_config = fun_def.webhook_config
148
- if not webhook_config.type:
149
- # for non-webhooks, the runnable is straight forward:
150
- return {
151
- "": FinalizedFunction(
152
- callable=self._user_defined_callable,
153
- is_async=is_async,
154
- is_generator=is_generator,
155
- data_format=api_pb2.DATA_FORMAT_PICKLE,
156
- )
157
- }
158
-
159
- web_callable, lifespan_manager = construct_webhook_callable(
160
- self._user_defined_callable, fun_def.webhook_config, container_io_manager
161
- )
162
-
163
- return {
164
- "": FinalizedFunction(
165
- callable=web_callable,
166
- lifespan_manager=lifespan_manager,
167
- is_async=True,
168
- is_generator=True,
169
- data_format=api_pb2.DATA_FORMAT_ASGI,
170
- )
171
- }
172
-
173
-
174
- @dataclass
175
- class ImportedClass(Service):
176
- user_cls_instance: Any
177
- app: Optional[_App]
178
- code_deps: Optional[List["modal.object._Object"]]
179
-
180
- _partial_functions: Dict[str, _PartialFunction]
181
-
182
- def get_finalized_functions(
183
- self, fun_def: api_pb2.Function, container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager"
184
- ) -> Dict[str, "FinalizedFunction"]:
185
- finalized_functions = {}
186
- for method_name, partial in self._partial_functions.items():
187
- partial = synchronizer._translate_in(partial) # ugly
188
- user_func = partial.raw_f
189
- # Check this property before we turn it into a method (overriden by webhooks)
190
- is_async = get_is_async(user_func)
191
- # Use the function definition for whether this is a generator (overriden by webhooks)
192
- is_generator = partial.is_generator
193
- webhook_config = partial.webhook_config
194
-
195
- bound_func = user_func.__get__(self.user_cls_instance)
196
-
197
- if not webhook_config or webhook_config.type == api_pb2.WEBHOOK_TYPE_UNSPECIFIED:
198
- # for non-webhooks, the runnable is straight forward:
199
- finalized_function = FinalizedFunction(
200
- callable=bound_func,
201
- is_async=is_async,
202
- is_generator=is_generator,
203
- data_format=api_pb2.DATA_FORMAT_PICKLE,
204
- )
205
- else:
206
- web_callable, lifespan_manager = construct_webhook_callable(
207
- bound_func, webhook_config, container_io_manager
208
- )
209
- finalized_function = FinalizedFunction(
210
- callable=web_callable,
211
- lifespan_manager=lifespan_manager,
212
- is_async=True,
213
- is_generator=True,
214
- data_format=api_pb2.DATA_FORMAT_ASGI,
215
- )
216
- finalized_functions[method_name] = finalized_function
217
- return finalized_functions
218
-
219
-
220
57
  class DaemonizedThreadPool:
221
58
  # Used instead of ThreadPoolExecutor, since the latter won't allow
222
59
  # the interpreter to shut down before the currently running tasks
@@ -338,7 +175,7 @@ class UserCodeEventLoop:
338
175
  def call_function(
339
176
  user_code_event_loop: UserCodeEventLoop,
340
177
  container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager",
341
- finalized_functions: Dict[str, FinalizedFunction],
178
+ finalized_functions: Dict[str, "modal._runtime.user_code_imports.FinalizedFunction"],
342
179
  batch_max_size: int,
343
180
  batch_wait_ms: int,
344
181
  ):
@@ -499,180 +336,6 @@ def call_function(
499
336
  signal.signal(signal.SIGUSR1, usr1_handler) # reset signal handler
500
337
 
501
338
 
502
- def import_single_function_service(
503
- function_def: api_pb2.Function,
504
- ser_cls, # used only for @build functions
505
- ser_fun,
506
- cls_args, # used only for @build functions
507
- cls_kwargs, # used only for @build functions
508
- ) -> Service:
509
- """Imports a function dynamically, and locates the app.
510
-
511
- This is somewhat complex because we're dealing with 3 quite different type of functions:
512
- 1. Functions defined in global scope and decorated in global scope (Function objects)
513
- 2. Functions defined in global scope but decorated elsewhere (these will be raw callables)
514
- 3. Serialized functions
515
-
516
- In addition, we also need to handle
517
- * Normal functions
518
- * Methods on classes (in which case we need to instantiate the object)
519
-
520
- This helper also handles web endpoints, ASGI/WSGI servers, and HTTP servers.
521
-
522
- In order to locate the app, we try two things:
523
- * If the function is a Function, we can get the app directly from it
524
- * Otherwise, use the app name and look it up from a global list of apps: this
525
- typically only happens in case 2 above, or in sometimes for case 3
526
-
527
- Note that `import_function` is *not* synchronized, because we need it to run on the main
528
- thread. This is so that any user code running in global scope (which executes as a part of
529
- the import) runs on the right thread.
530
- """
531
- user_defined_callable: Callable
532
- function: Optional[_Function] = None
533
- code_deps: Optional[List["modal.object._Object"]] = None
534
- active_app: Optional[_App] = None
535
-
536
- if ser_fun is not None:
537
- # This is a serialized function we already fetched from the server
538
- cls, user_defined_callable = ser_cls, ser_fun
539
- else:
540
- # Load the module dynamically
541
- module = importlib.import_module(function_def.module_name)
542
- qual_name: str = function_def.function_name
543
-
544
- if not is_global_object(qual_name):
545
- raise LocalFunctionError("Attempted to load a function defined in a function scope")
546
-
547
- parts = qual_name.split(".")
548
- if len(parts) == 1:
549
- # This is a function
550
- cls = None
551
- f = getattr(module, qual_name)
552
- if isinstance(f, Function):
553
- function = synchronizer._translate_in(f)
554
- user_defined_callable = function.get_raw_f()
555
- active_app = function._app
556
- else:
557
- user_defined_callable = f
558
- elif len(parts) == 2:
559
- # As of v0.63 - this path should only be triggered by @build class builder methods
560
- assert not function_def.use_method_name # new "placeholder methods" should not be invoked directly!
561
- assert function_def.is_builder_function
562
- cls_name, fun_name = parts
563
- cls = getattr(module, cls_name)
564
- if isinstance(cls, Cls):
565
- # The cls decorator is in global scope
566
- _cls = synchronizer._translate_in(cls)
567
- user_defined_callable = _cls._callables[fun_name]
568
- function = _cls._method_functions.get(fun_name)
569
- active_app = _cls._app
570
- else:
571
- # This is a raw class
572
- user_defined_callable = getattr(cls, fun_name)
573
- else:
574
- raise InvalidError(f"Invalid function qualname {qual_name}")
575
-
576
- # Instantiate the class if it's defined
577
- if cls:
578
- # This code is only used for @build methods on classes
579
- user_cls_instance = get_user_class_instance(cls, cls_args, cls_kwargs)
580
- # Bind the function to the instance as self (using the descriptor protocol!)
581
- user_defined_callable = user_defined_callable.__get__(user_cls_instance)
582
- else:
583
- user_cls_instance = None
584
-
585
- if function:
586
- code_deps = function.deps(only_explicit_mounts=True)
587
-
588
- return ImportedFunction(
589
- user_cls_instance,
590
- active_app,
591
- code_deps,
592
- user_defined_callable,
593
- )
594
-
595
-
596
- def import_class_service(
597
- function_def: api_pb2.Function,
598
- ser_cls,
599
- cls_args,
600
- cls_kwargs,
601
- ) -> Service:
602
- """
603
- This imports a full class to be able to execute any @method or webhook decorated methods.
604
-
605
- See import_function.
606
- """
607
- active_app: Optional[_App] = None
608
- code_deps: Optional[List["modal.object._Object"]] = None
609
- cls: typing.Union[type, Cls]
610
-
611
- if function_def.definition_type == api_pb2.Function.DEFINITION_TYPE_SERIALIZED:
612
- assert ser_cls is not None
613
- cls = ser_cls
614
- else:
615
- # Load the module dynamically
616
- module = importlib.import_module(function_def.module_name)
617
- qual_name: str = function_def.function_name
618
-
619
- if not is_global_object(qual_name):
620
- raise LocalFunctionError("Attempted to load a class defined in a function scope")
621
-
622
- parts = qual_name.split(".")
623
- if not (
624
- len(parts) == 2 and parts[1] == "*"
625
- ): # the "function name" of a class service "function placeholder" is expected to be "ClassName.*"
626
- raise ExecutionError(
627
- f"Internal error: Invalid 'service function' identifier {qual_name}. Please contact Modal support"
628
- )
629
-
630
- assert not function_def.use_method_name # new "placeholder methods" should not be invoked directly!
631
- cls_name = parts[0]
632
- cls = getattr(module, cls_name)
633
-
634
- if isinstance(cls, Cls):
635
- # The cls decorator is in global scope
636
- _cls = synchronizer._translate_in(cls)
637
- method_partials = _cls._get_partial_functions()
638
- function = _cls._class_service_function
639
- else:
640
- # Undecorated user class - find all methods
641
- method_partials = _find_partial_methods_for_user_cls(cls, _PartialFunctionFlags.all())
642
- function = None
643
-
644
- if function:
645
- code_deps = function.deps(only_explicit_mounts=True)
646
-
647
- user_cls_instance = get_user_class_instance(cls, cls_args, cls_kwargs)
648
-
649
- return ImportedClass(
650
- user_cls_instance,
651
- active_app,
652
- code_deps,
653
- method_partials,
654
- )
655
-
656
-
657
- def get_user_class_instance(cls: typing.Union[type, Cls], args: Tuple, kwargs: Dict[str, Any]) -> typing.Any:
658
- """Returns instance of the underlying class to be used as the `self`
659
-
660
- The input `cls` can either be the raw Python class the user has declared ("user class"),
661
- or an @app.cls-decorated version of it which is a modal.Cls-instance wrapping the user class.
662
- """
663
- if isinstance(cls, Cls):
664
- # globally @app.cls-decorated class
665
- modal_obj: Obj = cls(*args, **kwargs)
666
- modal_obj.entered = True # ugly but prevents .local() from triggering additional enter-logic
667
- # TODO: unify lifecycle logic between .local() and container_entrypoint
668
- user_cls_instance = modal_obj._get_user_cls_instance()
669
- else:
670
- # undecorated class (non-global decoration or serialized)
671
- user_cls_instance = cls(*args, **kwargs)
672
-
673
- return user_cls_instance
674
-
675
-
676
339
  def get_active_app_fallback(function_def: api_pb2.Function) -> Optional[_App]:
677
340
  # This branch is reached in the special case that the imported function/class is:
678
341
  # 1) not serialized, and
@@ -10,7 +10,6 @@ import sys
10
10
  import time
11
11
  import traceback
12
12
  from contextlib import AsyncExitStack
13
- from dataclasses import dataclass
14
13
  from pathlib import Path
15
14
  from typing import (
16
15
  TYPE_CHECKING,
@@ -46,6 +45,8 @@ from modal_proto import api_pb2
46
45
 
47
46
  if TYPE_CHECKING:
48
47
  import modal._runtime.asgi
48
+ import modal._runtime.user_code_imports
49
+
49
50
 
50
51
  DYNAMIC_CONCURRENCY_INTERVAL_SECS = 3
51
52
  DYNAMIC_CONCURRENCY_TIMEOUT_SECS = 10
@@ -62,15 +63,6 @@ class Sentinel:
62
63
  """Used to get type-stubs to work with this object."""
63
64
 
64
65
 
65
- @dataclass
66
- class FinalizedFunction:
67
- callable: Callable[..., Any]
68
- is_async: bool
69
- is_generator: bool
70
- data_format: int # api_pb2.DataFormat
71
- lifespan_manager: Optional["modal._runtime.asgi.LifespanManager"] = None
72
-
73
-
74
66
  class IOContext:
75
67
  """Context object for managing input, function calls, and function executions
76
68
  in a batched or single input context.
@@ -78,7 +70,7 @@ class IOContext:
78
70
 
79
71
  input_ids: List[str]
80
72
  function_call_ids: List[str]
81
- finalized_function: FinalizedFunction
73
+ finalized_function: "modal._runtime.user_code_imports.FinalizedFunction"
82
74
 
83
75
  _cancel_issued: bool = False
84
76
  _cancel_callback: Optional[Callable[[], None]] = None
@@ -87,7 +79,7 @@ class IOContext:
87
79
  self,
88
80
  input_ids: List[str],
89
81
  function_call_ids: List[str],
90
- finalized_function: FinalizedFunction,
82
+ finalized_function: "modal._runtime.user_code_imports.FinalizedFunction",
91
83
  function_inputs: List[api_pb2.FunctionInput],
92
84
  is_batched: bool,
93
85
  client: _Client,
@@ -103,7 +95,7 @@ class IOContext:
103
95
  async def create(
104
96
  cls,
105
97
  client: _Client,
106
- finalized_functions: Dict[str, FinalizedFunction],
98
+ finalized_functions: Dict[str, "modal._runtime.user_code_imports.FinalizedFunction"],
107
99
  inputs: List[Tuple[str, str, api_pb2.FunctionInput]],
108
100
  is_batched: bool,
109
101
  ) -> "IOContext":
@@ -653,7 +645,7 @@ class _ContainerIOManager:
653
645
  @synchronizer.no_io_translation
654
646
  async def run_inputs_outputs(
655
647
  self,
656
- finalized_functions: Dict[str, FinalizedFunction],
648
+ finalized_functions: Dict[str, "modal._runtime.user_code_imports.FinalizedFunction"],
657
649
  batch_max_size: int = 0,
658
650
  batch_wait_ms: int = 0,
659
651
  ) -> AsyncIterator[IOContext]: