modal 1.2.2.dev30__py3-none-any.whl → 1.2.2.dev36__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/_functions.py +77 -52
- modal/_load_context.py +105 -0
- modal/_object.py +47 -18
- modal/_resolver.py +21 -35
- modal/app.py +7 -0
- modal/app.pyi +3 -0
- modal/cli/dict.py +5 -2
- modal/cli/queues.py +4 -2
- modal/client.pyi +2 -2
- modal/cls.py +71 -32
- modal/cls.pyi +3 -0
- modal/dict.py +14 -5
- modal/dict.pyi +2 -0
- modal/environments.py +16 -7
- modal/environments.pyi +6 -2
- modal/functions.pyi +10 -4
- modal/image.py +22 -22
- modal/mount.py +35 -25
- modal/mount.pyi +33 -7
- modal/network_file_system.py +14 -5
- modal/network_file_system.pyi +12 -2
- modal/object.pyi +35 -8
- modal/proxy.py +14 -6
- modal/proxy.pyi +10 -2
- modal/queue.py +14 -5
- modal/queue.pyi +12 -2
- modal/runner.py +43 -47
- modal/runner.pyi +2 -2
- modal/sandbox.py +21 -12
- modal/secret.py +57 -39
- modal/secret.pyi +21 -4
- modal/serving.py +7 -11
- modal/serving.pyi +7 -8
- modal/snapshot.py +11 -5
- modal/volume.py +25 -7
- modal/volume.pyi +2 -0
- {modal-1.2.2.dev30.dist-info → modal-1.2.2.dev36.dist-info}/METADATA +1 -1
- {modal-1.2.2.dev30.dist-info → modal-1.2.2.dev36.dist-info}/RECORD +46 -45
- modal_proto/api.proto +4 -0
- modal_proto/api_pb2.py +684 -684
- modal_proto/api_pb2.pyi +24 -3
- modal_version/__init__.py +1 -1
- {modal-1.2.2.dev30.dist-info → modal-1.2.2.dev36.dist-info}/WHEEL +0 -0
- {modal-1.2.2.dev30.dist-info → modal-1.2.2.dev36.dist-info}/entry_points.txt +0 -0
- {modal-1.2.2.dev30.dist-info → modal-1.2.2.dev36.dist-info}/licenses/LICENSE +0 -0
- {modal-1.2.2.dev30.dist-info → modal-1.2.2.dev36.dist-info}/top_level.txt +0 -0
modal/queue.py
CHANGED
|
@@ -14,6 +14,7 @@ from synchronicity.async_wrap import asynccontextmanager
|
|
|
14
14
|
|
|
15
15
|
from modal_proto import api_pb2
|
|
16
16
|
|
|
17
|
+
from ._load_context import LoadContext
|
|
17
18
|
from ._object import (
|
|
18
19
|
EPHEMERAL_OBJECT_HEARTBEAT_SLEEP,
|
|
19
20
|
_get_environment_name,
|
|
@@ -361,6 +362,7 @@ class _Queue(_Object, type_prefix="qu"):
|
|
|
361
362
|
namespace=None, # mdmd:line-hidden
|
|
362
363
|
environment_name: Optional[str] = None,
|
|
363
364
|
create_if_missing: bool = False,
|
|
365
|
+
client: Optional[_Client] = None,
|
|
364
366
|
) -> "_Queue":
|
|
365
367
|
"""Reference a named Queue, creating if necessary.
|
|
366
368
|
|
|
@@ -376,17 +378,24 @@ class _Queue(_Object, type_prefix="qu"):
|
|
|
376
378
|
check_object_name(name, "Queue")
|
|
377
379
|
warn_if_passing_namespace(namespace, "modal.Queue.from_name")
|
|
378
380
|
|
|
379
|
-
async def _load(self: _Queue, resolver: Resolver, existing_object_id: Optional[str]):
|
|
381
|
+
async def _load(self: _Queue, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]):
|
|
380
382
|
req = api_pb2.QueueGetOrCreateRequest(
|
|
381
383
|
deployment_name=name,
|
|
382
|
-
environment_name=
|
|
384
|
+
environment_name=load_context.environment_name,
|
|
383
385
|
object_creation_type=(api_pb2.OBJECT_CREATION_TYPE_CREATE_IF_MISSING if create_if_missing else None),
|
|
384
386
|
)
|
|
385
|
-
response = await
|
|
386
|
-
self._hydrate(response.queue_id,
|
|
387
|
+
response = await load_context.client.stub.QueueGetOrCreate(req)
|
|
388
|
+
self._hydrate(response.queue_id, load_context.client, response.metadata)
|
|
387
389
|
|
|
388
390
|
rep = _Queue._repr(name, environment_name)
|
|
389
|
-
return _Queue._from_loader(
|
|
391
|
+
return _Queue._from_loader(
|
|
392
|
+
_load,
|
|
393
|
+
rep,
|
|
394
|
+
is_another_app=True,
|
|
395
|
+
hydrate_lazily=True,
|
|
396
|
+
name=name,
|
|
397
|
+
load_context_overrides=LoadContext(environment_name=environment_name, client=client),
|
|
398
|
+
)
|
|
390
399
|
|
|
391
400
|
@staticmethod
|
|
392
401
|
async def delete(name: str, *, client: Optional[_Client] = None, environment_name: Optional[str] = None):
|
modal/queue.pyi
CHANGED
|
@@ -464,7 +464,12 @@ class _Queue(modal._object._Object):
|
|
|
464
464
|
|
|
465
465
|
@staticmethod
|
|
466
466
|
def from_name(
|
|
467
|
-
name: str,
|
|
467
|
+
name: str,
|
|
468
|
+
*,
|
|
469
|
+
namespace=None,
|
|
470
|
+
environment_name: typing.Optional[str] = None,
|
|
471
|
+
create_if_missing: bool = False,
|
|
472
|
+
client: typing.Optional[modal.client._Client] = None,
|
|
468
473
|
) -> _Queue:
|
|
469
474
|
"""Reference a named Queue, creating if necessary.
|
|
470
475
|
|
|
@@ -721,7 +726,12 @@ class Queue(modal.object.Object):
|
|
|
721
726
|
|
|
722
727
|
@staticmethod
|
|
723
728
|
def from_name(
|
|
724
|
-
name: str,
|
|
729
|
+
name: str,
|
|
730
|
+
*,
|
|
731
|
+
namespace=None,
|
|
732
|
+
environment_name: typing.Optional[str] = None,
|
|
733
|
+
create_if_missing: bool = False,
|
|
734
|
+
client: typing.Optional[modal.client.Client] = None,
|
|
725
735
|
) -> Queue:
|
|
726
736
|
"""Reference a named Queue, creating if necessary.
|
|
727
737
|
|
modal/runner.py
CHANGED
|
@@ -8,7 +8,6 @@ import asyncio
|
|
|
8
8
|
import dataclasses
|
|
9
9
|
import os
|
|
10
10
|
import time
|
|
11
|
-
import typing
|
|
12
11
|
from collections.abc import AsyncGenerator
|
|
13
12
|
from contextlib import nullcontext
|
|
14
13
|
from multiprocessing.synchronize import Event
|
|
@@ -19,6 +18,7 @@ from synchronicity.async_wrap import asynccontextmanager
|
|
|
19
18
|
|
|
20
19
|
import modal._runtime.execution_context
|
|
21
20
|
import modal_proto.api_pb2
|
|
21
|
+
from modal._load_context import LoadContext
|
|
22
22
|
from modal._utils.grpc_utils import Retry
|
|
23
23
|
from modal_proto import api_pb2
|
|
24
24
|
|
|
@@ -125,18 +125,14 @@ async def _init_local_app_from_name(
|
|
|
125
125
|
|
|
126
126
|
|
|
127
127
|
async def _create_all_objects(
|
|
128
|
-
client: _Client,
|
|
129
128
|
running_app: RunningApp,
|
|
130
129
|
local_app_state: "modal.app._LocalAppState",
|
|
131
|
-
|
|
130
|
+
load_context: LoadContext,
|
|
132
131
|
) -> None:
|
|
133
132
|
"""Create objects that have been defined but not created on the server."""
|
|
134
133
|
indexed_objects: dict[str, _Object] = {**local_app_state.functions, **local_app_state.classes}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
environment_name=environment_name,
|
|
138
|
-
app_id=running_app.app_id,
|
|
139
|
-
)
|
|
134
|
+
|
|
135
|
+
resolver = Resolver()
|
|
140
136
|
with resolver.display():
|
|
141
137
|
# Get current objects, and reset all objects
|
|
142
138
|
tag_to_object_id = {**running_app.function_ids, **running_app.class_ids}
|
|
@@ -159,7 +155,7 @@ async def _create_all_objects(
|
|
|
159
155
|
# Note: preload only currently implemented for Functions, returns None otherwise
|
|
160
156
|
# this is to ensure that directly referenced functions from the global scope has
|
|
161
157
|
# ids associated with them when they are serialized into other functions
|
|
162
|
-
await resolver.preload(obj, existing_object_id)
|
|
158
|
+
await resolver.preload(obj, load_context, existing_object_id)
|
|
163
159
|
if obj.is_hydrated:
|
|
164
160
|
tag_to_object_id[tag] = obj.object_id
|
|
165
161
|
|
|
@@ -167,7 +163,8 @@ async def _create_all_objects(
|
|
|
167
163
|
|
|
168
164
|
async def _load(tag, obj):
|
|
169
165
|
existing_object_id = tag_to_object_id.get(tag)
|
|
170
|
-
|
|
166
|
+
# Pass load_context so dependencies can inherit app_id, client, etc.
|
|
167
|
+
await resolver.load(obj, load_context, existing_object_id=existing_object_id)
|
|
171
168
|
if _Function._is_id_type(obj.object_id):
|
|
172
169
|
running_app.function_ids[tag] = obj.object_id
|
|
173
170
|
elif _Cls._is_id_type(obj.object_id):
|
|
@@ -266,8 +263,9 @@ async def _run_app(
|
|
|
266
263
|
interactive: bool = False,
|
|
267
264
|
) -> AsyncGenerator["modal.app._App", None]:
|
|
268
265
|
"""mdmd:hidden"""
|
|
269
|
-
|
|
270
|
-
|
|
266
|
+
load_context = await app._root_load_context.reset().in_place_upgrade(
|
|
267
|
+
client=client, environment_name=environment_name
|
|
268
|
+
)
|
|
271
269
|
|
|
272
270
|
if modal._runtime.execution_context._is_currently_importing:
|
|
273
271
|
raise InvalidError("Can not run an app in global scope within a container")
|
|
@@ -288,9 +286,6 @@ async def _run_app(
|
|
|
288
286
|
# https://docs.python.org/3/library/__main__.html#import-main
|
|
289
287
|
app.set_description(__main__.__name__)
|
|
290
288
|
|
|
291
|
-
if client is None:
|
|
292
|
-
client = await _Client.from_env()
|
|
293
|
-
|
|
294
289
|
app_state = api_pb2.APP_STATE_DETACHED if detach else api_pb2.APP_STATE_EPHEMERAL
|
|
295
290
|
|
|
296
291
|
output_mgr = _get_output_manager()
|
|
@@ -301,21 +296,22 @@ async def _run_app(
|
|
|
301
296
|
local_app_state = app._local_state
|
|
302
297
|
|
|
303
298
|
running_app: RunningApp = await _init_local_app_new(
|
|
304
|
-
client,
|
|
299
|
+
load_context.client,
|
|
305
300
|
app.description or "",
|
|
306
301
|
local_app_state.tags,
|
|
307
|
-
environment_name=environment_name
|
|
302
|
+
environment_name=load_context.environment_name,
|
|
308
303
|
app_state=app_state,
|
|
309
304
|
interactive=interactive,
|
|
310
305
|
)
|
|
306
|
+
await load_context.in_place_upgrade(app_id=running_app.app_id)
|
|
311
307
|
|
|
312
308
|
logs_timeout = config["logs_timeout"]
|
|
313
|
-
async with app._set_local_app(client, running_app), TaskContext(grace=logs_timeout) as tc:
|
|
309
|
+
async with app._set_local_app(load_context.client, running_app), TaskContext(grace=logs_timeout) as tc:
|
|
314
310
|
# Start heartbeats loop to keep the client alive
|
|
315
311
|
# we don't log heartbeat exceptions in detached mode
|
|
316
312
|
# as losing the local connection will not affect the running app
|
|
317
313
|
def heartbeat():
|
|
318
|
-
return _heartbeat(client, running_app.app_id)
|
|
314
|
+
return _heartbeat(load_context.client, running_app.app_id)
|
|
319
315
|
|
|
320
316
|
heartbeat_loop = tc.infinite_loop(heartbeat, sleep=HEARTBEAT_INTERVAL, log_exception=not detach)
|
|
321
317
|
logs_loop: Optional[asyncio.Task] = None
|
|
@@ -336,24 +332,26 @@ async def _run_app(
|
|
|
336
332
|
# Start logs loop
|
|
337
333
|
|
|
338
334
|
logs_loop = tc.create_task(
|
|
339
|
-
get_app_logs_loop(
|
|
335
|
+
get_app_logs_loop(
|
|
336
|
+
load_context.client, output_mgr, app_id=running_app.app_id, app_logs_url=running_app.app_logs_url
|
|
337
|
+
)
|
|
340
338
|
)
|
|
341
339
|
|
|
342
340
|
try:
|
|
343
341
|
# Create all members
|
|
344
|
-
await _create_all_objects(
|
|
342
|
+
await _create_all_objects(running_app, local_app_state, load_context)
|
|
345
343
|
|
|
346
344
|
# Publish the app
|
|
347
|
-
await _publish_app(client, running_app, app_state, local_app_state)
|
|
345
|
+
await _publish_app(load_context.client, running_app, app_state, local_app_state)
|
|
348
346
|
except asyncio.CancelledError as e:
|
|
349
347
|
# this typically happens on sigint/ctrl-C during setup (the KeyboardInterrupt happens in the main thread)
|
|
350
348
|
if output_mgr := _get_output_manager():
|
|
351
349
|
output_mgr.print("Aborting app initialization...\n")
|
|
352
350
|
|
|
353
|
-
await _status_based_disconnect(client, running_app.app_id, e)
|
|
351
|
+
await _status_based_disconnect(load_context.client, running_app.app_id, e)
|
|
354
352
|
raise
|
|
355
353
|
except BaseException as e:
|
|
356
|
-
await _status_based_disconnect(client, running_app.app_id, e)
|
|
354
|
+
await _status_based_disconnect(load_context.client, running_app.app_id, e)
|
|
357
355
|
raise
|
|
358
356
|
|
|
359
357
|
detached_disconnect_msg = (
|
|
@@ -381,7 +379,7 @@ async def _run_app(
|
|
|
381
379
|
yield app
|
|
382
380
|
# successful completion!
|
|
383
381
|
heartbeat_loop.cancel()
|
|
384
|
-
await _status_based_disconnect(client, running_app.app_id, exc_info=None)
|
|
382
|
+
await _status_based_disconnect(load_context.client, running_app.app_id, exc_info=None)
|
|
385
383
|
except KeyboardInterrupt as e:
|
|
386
384
|
# this happens only if sigint comes in during the yield block above
|
|
387
385
|
if detach:
|
|
@@ -390,13 +388,13 @@ async def _run_app(
|
|
|
390
388
|
output_mgr.print(detached_disconnect_msg)
|
|
391
389
|
if logs_loop:
|
|
392
390
|
logs_loop.cancel()
|
|
393
|
-
await _status_based_disconnect(client, running_app.app_id, e)
|
|
391
|
+
await _status_based_disconnect(load_context.client, running_app.app_id, e)
|
|
394
392
|
else:
|
|
395
393
|
if output_mgr := _get_output_manager():
|
|
396
394
|
output_mgr.print(
|
|
397
395
|
"Disconnecting from Modal - This will terminate your Modal app in a few seconds.\n"
|
|
398
396
|
)
|
|
399
|
-
await _status_based_disconnect(client, running_app.app_id, e)
|
|
397
|
+
await _status_based_disconnect(load_context.client, running_app.app_id, e)
|
|
400
398
|
if logs_loop:
|
|
401
399
|
try:
|
|
402
400
|
await asyncio.wait_for(logs_loop, timeout=logs_timeout)
|
|
@@ -421,7 +419,7 @@ async def _run_app(
|
|
|
421
419
|
raise
|
|
422
420
|
except BaseException as e:
|
|
423
421
|
logger.info("Exception during app run")
|
|
424
|
-
await _status_based_disconnect(client, running_app.app_id, e)
|
|
422
|
+
await _status_based_disconnect(load_context.client, running_app.app_id, e)
|
|
425
423
|
raise
|
|
426
424
|
|
|
427
425
|
# wait for logs gracefully, even though the task context would do the same
|
|
@@ -449,21 +447,17 @@ async def _serve_update(
|
|
|
449
447
|
) -> None:
|
|
450
448
|
"""mdmd:hidden"""
|
|
451
449
|
# Used by child process to reinitialize a served app
|
|
452
|
-
|
|
450
|
+
load_context = await app._root_load_context.reset().in_place_upgrade(environment_name=environment_name)
|
|
453
451
|
try:
|
|
454
|
-
running_app: RunningApp = await _init_local_app_existing(client, existing_app_id, environment_name)
|
|
452
|
+
running_app: RunningApp = await _init_local_app_existing(load_context.client, existing_app_id, environment_name)
|
|
453
|
+
await load_context.in_place_upgrade(app_id=running_app.app_id)
|
|
455
454
|
local_app_state = app._local_state
|
|
456
455
|
# Create objects
|
|
457
|
-
await _create_all_objects(
|
|
458
|
-
client,
|
|
459
|
-
running_app,
|
|
460
|
-
local_app_state,
|
|
461
|
-
environment_name,
|
|
462
|
-
)
|
|
456
|
+
await _create_all_objects(running_app, local_app_state, load_context)
|
|
463
457
|
|
|
464
458
|
# Publish the updated app
|
|
465
459
|
await _publish_app(
|
|
466
|
-
client,
|
|
460
|
+
load_context.client,
|
|
467
461
|
running_app,
|
|
468
462
|
app_state=api_pb2.APP_STATE_UNSPECIFIED,
|
|
469
463
|
app_local_state=local_app_state,
|
|
@@ -498,9 +492,6 @@ async def _deploy_app(
|
|
|
498
492
|
|
|
499
493
|
Users should prefer the `modal deploy` CLI or the `App.deploy` method.
|
|
500
494
|
"""
|
|
501
|
-
if environment_name is None:
|
|
502
|
-
environment_name = typing.cast(str, config.get("environment"))
|
|
503
|
-
|
|
504
495
|
warn_if_passing_namespace(namespace, "modal.runner.deploy_app")
|
|
505
496
|
|
|
506
497
|
name = name or app.name or ""
|
|
@@ -532,8 +523,18 @@ async def _deploy_app(
|
|
|
532
523
|
# Get git information to track deployment history
|
|
533
524
|
commit_info_task = asyncio.create_task(get_git_commit_info())
|
|
534
525
|
|
|
526
|
+
# We need to do in-place replacement of fields in self._root_load_context in case it has already "spread"
|
|
527
|
+
# to with_options() instances or similar before load
|
|
528
|
+
root_load_context = await app._root_load_context.reset().in_place_upgrade(
|
|
529
|
+
client=client,
|
|
530
|
+
environment_name=environment_name,
|
|
531
|
+
)
|
|
535
532
|
running_app: RunningApp = await _init_local_app_from_name(
|
|
536
|
-
client, name, local_app_state.tags, environment_name=environment_name
|
|
533
|
+
root_load_context.client, name, local_app_state.tags, environment_name=root_load_context.environment_name
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
await root_load_context.in_place_upgrade(
|
|
537
|
+
app_id=running_app.app_id,
|
|
537
538
|
)
|
|
538
539
|
|
|
539
540
|
async with TaskContext(0) as tc:
|
|
@@ -545,12 +546,7 @@ async def _deploy_app(
|
|
|
545
546
|
|
|
546
547
|
try:
|
|
547
548
|
# Create all members
|
|
548
|
-
await _create_all_objects(
|
|
549
|
-
client,
|
|
550
|
-
running_app,
|
|
551
|
-
local_app_state,
|
|
552
|
-
environment_name=environment_name,
|
|
553
|
-
)
|
|
549
|
+
await _create_all_objects(running_app, local_app_state, root_load_context)
|
|
554
550
|
|
|
555
551
|
commit_info = None
|
|
556
552
|
try:
|
modal/runner.pyi
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import modal._load_context
|
|
1
2
|
import modal.app
|
|
2
3
|
import modal.client
|
|
3
4
|
import modal.running_app
|
|
@@ -25,10 +26,9 @@ async def _init_local_app_from_name(
|
|
|
25
26
|
client: modal.client._Client, name: str, tags: dict[str, str], environment_name: str = ""
|
|
26
27
|
) -> modal.running_app.RunningApp: ...
|
|
27
28
|
async def _create_all_objects(
|
|
28
|
-
client: modal.client._Client,
|
|
29
29
|
running_app: modal.running_app.RunningApp,
|
|
30
30
|
local_app_state: modal.app._LocalAppState,
|
|
31
|
-
|
|
31
|
+
load_context: modal._load_context.LoadContext,
|
|
32
32
|
) -> None:
|
|
33
33
|
"""Create objects that have been defined but not created on the server."""
|
|
34
34
|
...
|
modal/sandbox.py
CHANGED
|
@@ -23,6 +23,7 @@ from modal.mount import _Mount
|
|
|
23
23
|
from modal.volume import _Volume
|
|
24
24
|
from modal_proto import api_pb2, task_command_router_pb2 as sr_pb2
|
|
25
25
|
|
|
26
|
+
from ._load_context import LoadContext
|
|
26
27
|
from ._object import _get_environment_name, _Object
|
|
27
28
|
from ._resolver import Resolver
|
|
28
29
|
from ._resources import convert_fn_config_to_resources_config
|
|
@@ -191,7 +192,9 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
191
192
|
deps.append(proxy)
|
|
192
193
|
return deps
|
|
193
194
|
|
|
194
|
-
async def _load(
|
|
195
|
+
async def _load(
|
|
196
|
+
self: _Sandbox, resolver: Resolver, load_context: LoadContext, _existing_object_id: Optional[str]
|
|
197
|
+
):
|
|
195
198
|
# Relies on dicts being ordered (true as of Python 3.6).
|
|
196
199
|
volume_mounts = [
|
|
197
200
|
api_pb2.VolumeMount(
|
|
@@ -260,18 +263,18 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
260
263
|
experimental_options=experimental_options,
|
|
261
264
|
)
|
|
262
265
|
|
|
263
|
-
create_req = api_pb2.SandboxCreateRequest(app_id=
|
|
266
|
+
create_req = api_pb2.SandboxCreateRequest(app_id=load_context.app_id, definition=definition)
|
|
264
267
|
try:
|
|
265
|
-
create_resp = await
|
|
268
|
+
create_resp = await load_context.client.stub.SandboxCreate(create_req)
|
|
266
269
|
except GRPCError as exc:
|
|
267
270
|
if exc.status == Status.ALREADY_EXISTS:
|
|
268
271
|
raise AlreadyExistsError(exc.message)
|
|
269
272
|
raise exc
|
|
270
273
|
|
|
271
274
|
sandbox_id = create_resp.sandbox_id
|
|
272
|
-
self._hydrate(sandbox_id,
|
|
275
|
+
self._hydrate(sandbox_id, load_context.client, None)
|
|
273
276
|
|
|
274
|
-
return _Sandbox._from_loader(_load, "Sandbox()", deps=_deps)
|
|
277
|
+
return _Sandbox._from_loader(_load, "Sandbox()", deps=_deps, load_context_overrides=LoadContext.empty())
|
|
275
278
|
|
|
276
279
|
@staticmethod
|
|
277
280
|
async def create(
|
|
@@ -486,6 +489,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
486
489
|
app_id = app.app_id
|
|
487
490
|
app_client = app._client
|
|
488
491
|
elif (container_app := _App._get_container_app()) is not None:
|
|
492
|
+
# implicit app/client provided by running in a modal Function
|
|
489
493
|
app_id = container_app.app_id
|
|
490
494
|
app_client = container_app._client
|
|
491
495
|
else:
|
|
@@ -498,10 +502,11 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
498
502
|
"```",
|
|
499
503
|
)
|
|
500
504
|
|
|
501
|
-
client = client or app_client
|
|
505
|
+
client = client or app_client
|
|
502
506
|
|
|
503
|
-
resolver = Resolver(
|
|
504
|
-
|
|
507
|
+
resolver = Resolver()
|
|
508
|
+
load_context = LoadContext(client=client, app_id=app_id)
|
|
509
|
+
await resolver.load(obj, load_context)
|
|
505
510
|
return obj
|
|
506
511
|
|
|
507
512
|
def _hydrate_metadata(self, handle_metadata: Optional[Message]):
|
|
@@ -606,12 +611,13 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
606
611
|
image_id = resp.image_id
|
|
607
612
|
metadata = resp.image_metadata
|
|
608
613
|
|
|
609
|
-
async def _load(self: _Image, resolver: Resolver, existing_object_id: Optional[str]):
|
|
614
|
+
async def _load(self: _Image, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]):
|
|
610
615
|
# no need to hydrate again since we do it eagerly below
|
|
611
616
|
pass
|
|
612
617
|
|
|
613
618
|
rep = "Image()"
|
|
614
|
-
|
|
619
|
+
# TODO: use ._new_hydrated instead
|
|
620
|
+
image = _Image._from_loader(_load, rep, hydrate_lazily=True, load_context_overrides=LoadContext.empty())
|
|
615
621
|
image._hydrate(image_id, self._client, metadata) # hydrating eagerly since we have all of the data
|
|
616
622
|
|
|
617
623
|
return image
|
|
@@ -990,12 +996,15 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
990
996
|
if wait_resp.result.status != api_pb2.GenericResult.GENERIC_STATUS_SUCCESS:
|
|
991
997
|
raise ExecutionError(wait_resp.result.exception)
|
|
992
998
|
|
|
993
|
-
async def _load(
|
|
999
|
+
async def _load(
|
|
1000
|
+
self: _SandboxSnapshot, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
|
|
1001
|
+
):
|
|
994
1002
|
# we eagerly hydrate the sandbox snapshot below
|
|
995
1003
|
pass
|
|
996
1004
|
|
|
997
1005
|
rep = "SandboxSnapshot()"
|
|
998
|
-
|
|
1006
|
+
# TODO: use ._new_hydrated instead
|
|
1007
|
+
obj = _SandboxSnapshot._from_loader(_load, rep, hydrate_lazily=True, load_context_overrides=LoadContext.empty())
|
|
999
1008
|
obj._hydrate(snapshot_id, self._client, None)
|
|
1000
1009
|
|
|
1001
1010
|
return obj
|
modal/secret.py
CHANGED
|
@@ -10,6 +10,7 @@ from synchronicity import classproperty
|
|
|
10
10
|
|
|
11
11
|
from modal_proto import api_pb2
|
|
12
12
|
|
|
13
|
+
from ._load_context import LoadContext
|
|
13
14
|
from ._object import _get_environment_name, _Object, live_method
|
|
14
15
|
from ._resolver import Resolver
|
|
15
16
|
from ._runtime.execution_context import is_local
|
|
@@ -205,6 +206,33 @@ class _SecretManager:
|
|
|
205
206
|
SecretManager = synchronize_api(_SecretManager)
|
|
206
207
|
|
|
207
208
|
|
|
209
|
+
async def _load_from_env_dict(instance: "_Secret", load_context: LoadContext, env_dict: dict[str, str]):
|
|
210
|
+
"""helper method for loaders .from_dict and .from_dotenv etc."""
|
|
211
|
+
if load_context.app_id is not None:
|
|
212
|
+
req = api_pb2.SecretGetOrCreateRequest(
|
|
213
|
+
object_creation_type=api_pb2.OBJECT_CREATION_TYPE_ANONYMOUS_OWNED_BY_APP,
|
|
214
|
+
env_dict=env_dict,
|
|
215
|
+
app_id=load_context.app_id,
|
|
216
|
+
environment_name=load_context.environment_name,
|
|
217
|
+
)
|
|
218
|
+
else:
|
|
219
|
+
req = api_pb2.SecretGetOrCreateRequest(
|
|
220
|
+
object_creation_type=api_pb2.OBJECT_CREATION_TYPE_EPHEMERAL,
|
|
221
|
+
env_dict=env_dict,
|
|
222
|
+
environment_name=load_context.environment_name,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
resp = await load_context.client.stub.SecretGetOrCreate(req)
|
|
227
|
+
except GRPCError as exc:
|
|
228
|
+
if exc.status == Status.INVALID_ARGUMENT:
|
|
229
|
+
raise InvalidError(exc.message)
|
|
230
|
+
if exc.status == Status.FAILED_PRECONDITION:
|
|
231
|
+
raise InvalidError(exc.message)
|
|
232
|
+
raise
|
|
233
|
+
instance._hydrate(resp.secret_id, load_context.client, resp.metadata)
|
|
234
|
+
|
|
235
|
+
|
|
208
236
|
class _Secret(_Object, type_prefix="st"):
|
|
209
237
|
"""Secrets provide a dictionary of environment variables for images.
|
|
210
238
|
|
|
@@ -259,30 +287,14 @@ class _Secret(_Object, type_prefix="st"):
|
|
|
259
287
|
if not all(isinstance(v, str) for v in env_dict_filtered.values()):
|
|
260
288
|
raise InvalidError(ENV_DICT_WRONG_TYPE_ERR)
|
|
261
289
|
|
|
262
|
-
async def _load(
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
object_creation_type = api_pb2.OBJECT_CREATION_TYPE_EPHEMERAL
|
|
267
|
-
|
|
268
|
-
req = api_pb2.SecretGetOrCreateRequest(
|
|
269
|
-
object_creation_type=object_creation_type,
|
|
270
|
-
env_dict=env_dict_filtered,
|
|
271
|
-
app_id=resolver.app_id,
|
|
272
|
-
environment_name=resolver.environment_name,
|
|
273
|
-
)
|
|
274
|
-
try:
|
|
275
|
-
resp = await resolver.client.stub.SecretGetOrCreate(req)
|
|
276
|
-
except GRPCError as exc:
|
|
277
|
-
if exc.status == Status.INVALID_ARGUMENT:
|
|
278
|
-
raise InvalidError(exc.message)
|
|
279
|
-
if exc.status == Status.FAILED_PRECONDITION:
|
|
280
|
-
raise InvalidError(exc.message)
|
|
281
|
-
raise
|
|
282
|
-
self._hydrate(resp.secret_id, resolver.client, resp.metadata)
|
|
290
|
+
async def _load(
|
|
291
|
+
self: _Secret, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
|
|
292
|
+
):
|
|
293
|
+
await _load_from_env_dict(self, load_context, env_dict_filtered)
|
|
283
294
|
|
|
284
295
|
rep = f"Secret.from_dict([{', '.join(env_dict.keys())}])"
|
|
285
|
-
|
|
296
|
+
# TODO: scoping - these should probably not be lazily hydrated without having an app and/or sandbox association
|
|
297
|
+
return _Secret._from_loader(_load, rep, hydrate_lazily=True, load_context_overrides=LoadContext.empty())
|
|
286
298
|
|
|
287
299
|
@staticmethod
|
|
288
300
|
def from_local_environ(
|
|
@@ -302,7 +314,7 @@ class _Secret(_Object, type_prefix="st"):
|
|
|
302
314
|
return _Secret.from_dict({})
|
|
303
315
|
|
|
304
316
|
@staticmethod
|
|
305
|
-
def from_dotenv(path=None, *, filename=".env") -> "_Secret":
|
|
317
|
+
def from_dotenv(path=None, *, filename=".env", client: Optional[_Client] = None) -> "_Secret":
|
|
306
318
|
"""Create secrets from a .env file automatically.
|
|
307
319
|
|
|
308
320
|
If no argument is provided, it will use the current working directory as the starting
|
|
@@ -330,7 +342,9 @@ class _Secret(_Object, type_prefix="st"):
|
|
|
330
342
|
```
|
|
331
343
|
"""
|
|
332
344
|
|
|
333
|
-
async def _load(
|
|
345
|
+
async def _load(
|
|
346
|
+
self: _Secret, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
|
|
347
|
+
):
|
|
334
348
|
try:
|
|
335
349
|
from dotenv import dotenv_values, find_dotenv
|
|
336
350
|
from dotenv.main import _walk_to_root
|
|
@@ -354,18 +368,13 @@ class _Secret(_Object, type_prefix="st"):
|
|
|
354
368
|
# To simplify this, we just support the cwd and don't do any automatic path inference.
|
|
355
369
|
dotenv_path = find_dotenv(filename, usecwd=True)
|
|
356
370
|
|
|
357
|
-
env_dict = dotenv_values(dotenv_path)
|
|
371
|
+
env_dict = {k: v or "" for k, v in dotenv_values(dotenv_path).items()}
|
|
358
372
|
|
|
359
|
-
|
|
360
|
-
object_creation_type=api_pb2.OBJECT_CREATION_TYPE_ANONYMOUS_OWNED_BY_APP,
|
|
361
|
-
env_dict=env_dict,
|
|
362
|
-
app_id=resolver.app_id,
|
|
363
|
-
)
|
|
364
|
-
resp = await resolver.client.stub.SecretGetOrCreate(req)
|
|
365
|
-
|
|
366
|
-
self._hydrate(resp.secret_id, resolver.client, resp.metadata)
|
|
373
|
+
await _load_from_env_dict(self, load_context, env_dict)
|
|
367
374
|
|
|
368
|
-
return _Secret._from_loader(
|
|
375
|
+
return _Secret._from_loader(
|
|
376
|
+
_load, "Secret.from_dotenv()", hydrate_lazily=True, load_context_overrides=LoadContext(client=client)
|
|
377
|
+
)
|
|
369
378
|
|
|
370
379
|
@staticmethod
|
|
371
380
|
def from_name(
|
|
@@ -376,6 +385,7 @@ class _Secret(_Object, type_prefix="st"):
|
|
|
376
385
|
required_keys: list[
|
|
377
386
|
str
|
|
378
387
|
] = [], # Optionally, a list of required environment variables (will be asserted server-side)
|
|
388
|
+
client: Optional[_Client] = None,
|
|
379
389
|
) -> "_Secret":
|
|
380
390
|
"""Reference a Secret by its name.
|
|
381
391
|
|
|
@@ -393,23 +403,31 @@ class _Secret(_Object, type_prefix="st"):
|
|
|
393
403
|
"""
|
|
394
404
|
warn_if_passing_namespace(namespace, "modal.Secret.from_name")
|
|
395
405
|
|
|
396
|
-
async def _load(
|
|
406
|
+
async def _load(
|
|
407
|
+
self: _Secret, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
|
|
408
|
+
):
|
|
397
409
|
req = api_pb2.SecretGetOrCreateRequest(
|
|
398
410
|
deployment_name=name,
|
|
399
|
-
environment_name=
|
|
411
|
+
environment_name=load_context.environment_name,
|
|
400
412
|
required_keys=required_keys,
|
|
401
413
|
)
|
|
402
414
|
try:
|
|
403
|
-
response = await
|
|
415
|
+
response = await load_context.client.stub.SecretGetOrCreate(req)
|
|
404
416
|
except GRPCError as exc:
|
|
405
417
|
if exc.status == Status.NOT_FOUND:
|
|
406
418
|
raise NotFoundError(exc.message)
|
|
407
419
|
else:
|
|
408
420
|
raise
|
|
409
|
-
self._hydrate(response.secret_id,
|
|
421
|
+
self._hydrate(response.secret_id, load_context.client, response.metadata)
|
|
410
422
|
|
|
411
423
|
rep = _Secret._repr(name, environment_name)
|
|
412
|
-
return _Secret._from_loader(
|
|
424
|
+
return _Secret._from_loader(
|
|
425
|
+
_load,
|
|
426
|
+
rep,
|
|
427
|
+
hydrate_lazily=True,
|
|
428
|
+
name=name,
|
|
429
|
+
load_context_overrides=LoadContext(environment_name=environment_name, client=client),
|
|
430
|
+
)
|
|
413
431
|
|
|
414
432
|
@staticmethod
|
|
415
433
|
async def create_deployed(
|
modal/secret.pyi
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
import google.protobuf.message
|
|
3
|
+
import modal._load_context
|
|
3
4
|
import modal._object
|
|
4
5
|
import modal.client
|
|
5
6
|
import modal.object
|
|
@@ -355,6 +356,12 @@ class SecretManager:
|
|
|
355
356
|
|
|
356
357
|
delete: __delete_spec
|
|
357
358
|
|
|
359
|
+
async def _load_from_env_dict(
|
|
360
|
+
instance: _Secret, load_context: modal._load_context.LoadContext, env_dict: dict[str, str]
|
|
361
|
+
):
|
|
362
|
+
"""helper method for loaders .from_dict and .from_dotenv etc."""
|
|
363
|
+
...
|
|
364
|
+
|
|
358
365
|
class _Secret(modal._object._Object):
|
|
359
366
|
"""Secrets provide a dictionary of environment variables for images.
|
|
360
367
|
|
|
@@ -392,7 +399,7 @@ class _Secret(modal._object._Object):
|
|
|
392
399
|
...
|
|
393
400
|
|
|
394
401
|
@staticmethod
|
|
395
|
-
def from_dotenv(path=None, *, filename=".env") -> _Secret:
|
|
402
|
+
def from_dotenv(path=None, *, filename=".env", client: typing.Optional[modal.client._Client] = None) -> _Secret:
|
|
396
403
|
"""Create secrets from a .env file automatically.
|
|
397
404
|
|
|
398
405
|
If no argument is provided, it will use the current working directory as the starting
|
|
@@ -423,7 +430,12 @@ class _Secret(modal._object._Object):
|
|
|
423
430
|
|
|
424
431
|
@staticmethod
|
|
425
432
|
def from_name(
|
|
426
|
-
name: str,
|
|
433
|
+
name: str,
|
|
434
|
+
*,
|
|
435
|
+
namespace=None,
|
|
436
|
+
environment_name: typing.Optional[str] = None,
|
|
437
|
+
required_keys: list[str] = [],
|
|
438
|
+
client: typing.Optional[modal.client._Client] = None,
|
|
427
439
|
) -> _Secret:
|
|
428
440
|
"""Reference a Secret by its name.
|
|
429
441
|
|
|
@@ -512,7 +524,7 @@ class Secret(modal.object.Object):
|
|
|
512
524
|
...
|
|
513
525
|
|
|
514
526
|
@staticmethod
|
|
515
|
-
def from_dotenv(path=None, *, filename=".env") -> Secret:
|
|
527
|
+
def from_dotenv(path=None, *, filename=".env", client: typing.Optional[modal.client.Client] = None) -> Secret:
|
|
516
528
|
"""Create secrets from a .env file automatically.
|
|
517
529
|
|
|
518
530
|
If no argument is provided, it will use the current working directory as the starting
|
|
@@ -543,7 +555,12 @@ class Secret(modal.object.Object):
|
|
|
543
555
|
|
|
544
556
|
@staticmethod
|
|
545
557
|
def from_name(
|
|
546
|
-
name: str,
|
|
558
|
+
name: str,
|
|
559
|
+
*,
|
|
560
|
+
namespace=None,
|
|
561
|
+
environment_name: typing.Optional[str] = None,
|
|
562
|
+
required_keys: list[str] = [],
|
|
563
|
+
client: typing.Optional[modal.client.Client] = None,
|
|
547
564
|
) -> Secret:
|
|
548
565
|
"""Reference a Secret by its name.
|
|
549
566
|
|