modal 1.1.2.dev11__py3-none-any.whl → 1.1.2.dev12__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/_utils/time_utils.py +28 -4
- modal/cli/dict.py +6 -7
- modal/cli/queues.py +40 -15
- modal/cli/secret.py +33 -7
- modal/cli/volume.py +6 -9
- modal/client.pyi +2 -2
- modal/dict.py +78 -5
- modal/dict.pyi +115 -1
- modal/queue.py +76 -3
- modal/queue.pyi +114 -0
- modal/secret.py +68 -1
- modal/secret.pyi +114 -0
- modal/volume.py +79 -6
- modal/volume.pyi +118 -4
- {modal-1.1.2.dev11.dist-info → modal-1.1.2.dev12.dist-info}/METADATA +2 -2
- {modal-1.1.2.dev11.dist-info → modal-1.1.2.dev12.dist-info}/RECORD +24 -24
- modal_docs/mdmd/mdmd.py +11 -1
- modal_proto/api.proto +4 -4
- modal_proto/api_pb2.pyi +4 -4
- modal_version/__init__.py +1 -1
- {modal-1.1.2.dev11.dist-info → modal-1.1.2.dev12.dist-info}/WHEEL +0 -0
- {modal-1.1.2.dev11.dist-info → modal-1.1.2.dev12.dist-info}/entry_points.txt +0 -0
- {modal-1.1.2.dev11.dist-info → modal-1.1.2.dev12.dist-info}/licenses/LICENSE +0 -0
- {modal-1.1.2.dev11.dist-info → modal-1.1.2.dev12.dist-info}/top_level.txt +0 -0
modal/_utils/time_utils.py
CHANGED
@@ -1,11 +1,35 @@
|
|
1
1
|
# Copyright Modal Labs 2025
|
2
|
-
from datetime import datetime
|
3
|
-
from typing import Optional
|
2
|
+
from datetime import datetime, tzinfo
|
3
|
+
from typing import Optional, Union
|
4
|
+
|
5
|
+
|
6
|
+
def locale_tz() -> tzinfo:
|
7
|
+
return datetime.now().astimezone().tzinfo
|
8
|
+
|
9
|
+
|
10
|
+
def as_timestamp(arg: Optional[Union[datetime, str]]) -> float:
|
11
|
+
"""Coerce a user-provided argument to a timestamp.
|
12
|
+
|
13
|
+
An argument provided without timezone information will be treated as local time.
|
14
|
+
|
15
|
+
When the argument is null, returns the current time.
|
16
|
+
"""
|
17
|
+
if arg is None:
|
18
|
+
dt = datetime.now().astimezone()
|
19
|
+
elif isinstance(arg, str):
|
20
|
+
dt = datetime.fromisoformat(arg)
|
21
|
+
elif isinstance(arg, datetime):
|
22
|
+
dt = arg
|
23
|
+
else:
|
24
|
+
raise TypeError(f"Invalid argument: {arg}")
|
25
|
+
|
26
|
+
if dt.tzinfo is None:
|
27
|
+
dt = dt.replace(tzinfo=locale_tz())
|
28
|
+
return dt.timestamp()
|
4
29
|
|
5
30
|
|
6
31
|
def timestamp_to_localized_dt(ts: float) -> datetime:
|
7
|
-
|
8
|
-
return datetime.fromtimestamp(ts, tz=locale_tz)
|
32
|
+
return datetime.fromtimestamp(ts, tz=locale_tz())
|
9
33
|
|
10
34
|
|
11
35
|
def timestamp_to_localized_str(ts: float, isotz: bool = True) -> Optional[str]:
|
modal/cli/dict.py
CHANGED
@@ -7,13 +7,11 @@ from typer import Argument, Option, Typer
|
|
7
7
|
from modal._output import make_console
|
8
8
|
from modal._resolver import Resolver
|
9
9
|
from modal._utils.async_utils import synchronizer
|
10
|
-
from modal._utils.grpc_utils import retry_transient_errors
|
11
10
|
from modal._utils.time_utils import timestamp_to_localized_str
|
12
11
|
from modal.cli.utils import ENV_OPTION, YES_OPTION, display_table
|
13
12
|
from modal.client import _Client
|
14
13
|
from modal.dict import _Dict
|
15
14
|
from modal.environments import ensure_env
|
16
|
-
from modal_proto import api_pb2
|
17
15
|
|
18
16
|
dict_cli = Typer(
|
19
17
|
name="dict",
|
@@ -40,12 +38,13 @@ async def create(name: str, *, env: Optional[str] = ENV_OPTION):
|
|
40
38
|
async def list_(*, json: bool = False, env: Optional[str] = ENV_OPTION):
|
41
39
|
"""List all named Dicts."""
|
42
40
|
env = ensure_env(env)
|
43
|
-
|
44
|
-
|
45
|
-
|
41
|
+
dicts = await _Dict.objects.list(environment_name=env)
|
42
|
+
rows = []
|
43
|
+
for obj in dicts:
|
44
|
+
info = await obj.info()
|
45
|
+
rows.append((info.name, timestamp_to_localized_str(info.created_at.timestamp(), json), info.created_by))
|
46
46
|
|
47
|
-
|
48
|
-
display_table(["Name", "Created at"], rows, json)
|
47
|
+
display_table(["Name", "Created at", "Created by"], rows, json)
|
49
48
|
|
50
49
|
|
51
50
|
@dict_cli.command("clear", rich_help_panel="Management")
|
modal/cli/queues.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# Copyright Modal Labs 2024
|
2
|
+
from datetime import datetime
|
2
3
|
from typing import Optional
|
3
4
|
|
4
5
|
import typer
|
@@ -62,22 +63,46 @@ async def delete(name: str, *, yes: bool = YES_OPTION, env: Optional[str] = ENV_
|
|
62
63
|
async def list_(*, json: bool = False, env: Optional[str] = ENV_OPTION):
|
63
64
|
"""List all named Queues."""
|
64
65
|
env = ensure_env(env)
|
65
|
-
|
66
|
-
max_total_size = 100_000
|
67
66
|
client = await _Client.from_env()
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
67
|
+
max_total_size = 100_000 # Limit on the *Queue size* that we report
|
68
|
+
|
69
|
+
items: list[api_pb2.QueueListResponse.QueueInfo] = []
|
70
|
+
|
71
|
+
# Note that we need to continue using the gRPC API directly here rather than using Queue.objects.list.
|
72
|
+
# There is some metadata that historically appears in the CLI output (num_partitions, total_size) that
|
73
|
+
# doesn't make sense to transmit as hydration metadata, because the values can change over time and
|
74
|
+
# the metadata retrieved at hydration time could get stale. Alternatively, we could rewrite this using
|
75
|
+
# only public API by sequentially retrieving the queues and then querying their dynamic metadata, but
|
76
|
+
# that would require multiple round trips and would add lag to the CLI.
|
77
|
+
async def retrieve_page(created_before: float) -> bool:
|
78
|
+
max_page_size = 100
|
79
|
+
pagination = api_pb2.ListPagination(max_objects=max_page_size, created_before=created_before)
|
80
|
+
req = api_pb2.QueueListRequest(environment_name=env, pagination=pagination, total_size_limit=max_total_size)
|
81
|
+
resp = await retry_transient_errors(client.stub.QueueList, req)
|
82
|
+
items.extend(resp.queues)
|
83
|
+
return len(resp.queues) < max_page_size
|
84
|
+
|
85
|
+
finished = await retrieve_page(datetime.now().timestamp())
|
86
|
+
while True:
|
87
|
+
if finished:
|
88
|
+
break
|
89
|
+
finished = await retrieve_page(items[-1].metadata.creation_info.created_at)
|
90
|
+
|
91
|
+
queues = [_Queue._new_hydrated(item.queue_id, client, item.metadata, is_another_app=True) for item in items]
|
92
|
+
|
93
|
+
rows = []
|
94
|
+
for obj, resp_data in zip(queues, items):
|
95
|
+
info = await obj.info()
|
96
|
+
rows.append(
|
97
|
+
(
|
98
|
+
obj.name,
|
99
|
+
timestamp_to_localized_str(info.created_at.timestamp(), json),
|
100
|
+
info.created_by,
|
101
|
+
str(resp_data.num_partitions),
|
102
|
+
str(resp_data.total_size) if resp_data.total_size <= max_total_size else f">{max_total_size}",
|
103
|
+
)
|
77
104
|
)
|
78
|
-
|
79
|
-
]
|
80
|
-
display_table(["Name", "Created at", "Partitions", "Total size"], rows, json)
|
105
|
+
display_table(["Name", "Created at", "Created by", "Partitions", "Total size"], rows, json)
|
81
106
|
|
82
107
|
|
83
108
|
@queue_cli.command(name="clear", rich_help_panel="Management")
|
@@ -119,7 +144,7 @@ async def peek(
|
|
119
144
|
|
120
145
|
@queue_cli.command(name="len", rich_help_panel="Inspection")
|
121
146
|
@synchronizer.create_blocking
|
122
|
-
async def
|
147
|
+
async def len_(
|
123
148
|
name: str,
|
124
149
|
partition: Optional[str] = PARTITION_OPTION,
|
125
150
|
total: bool = Option(False, "-t", "--total", help="Compute the sum of the queue lengths across all partitions"),
|
modal/cli/secret.py
CHANGED
@@ -3,6 +3,7 @@ import json
|
|
3
3
|
import os
|
4
4
|
import platform
|
5
5
|
import subprocess
|
6
|
+
from datetime import datetime
|
6
7
|
from pathlib import Path
|
7
8
|
from tempfile import NamedTemporaryFile
|
8
9
|
from typing import Optional
|
@@ -30,20 +31,45 @@ secret_cli = typer.Typer(name="secret", help="Manage secrets.", no_args_is_help=
|
|
30
31
|
async def list_(env: Optional[str] = ENV_OPTION, json: bool = False):
|
31
32
|
env = ensure_env(env)
|
32
33
|
client = await _Client.from_env()
|
33
|
-
response = await retry_transient_errors(client.stub.SecretList, api_pb2.SecretListRequest(environment_name=env))
|
34
|
-
column_names = ["Name", "Created at", "Last used at"]
|
35
|
-
rows = []
|
36
34
|
|
37
|
-
|
35
|
+
items: list[api_pb2.SecretListItem] = []
|
36
|
+
|
37
|
+
# Note that we need to continue using the gRPC API directly here rather than using Secret.objects.list.
|
38
|
+
# There is some metadata that historically appears in the CLI output (last_used_at) that
|
39
|
+
# doesn't make sense to transmit as hydration metadata, because the value can change over time and
|
40
|
+
# the metadata retrieved at hydration time could get stale. Alternatively, we could rewrite this using
|
41
|
+
# only public API by sequentially retrieving the secrets and then querying their dynamic metadata, but
|
42
|
+
# that would require multiple round trips and would add lag to the CLI.
|
43
|
+
async def retrieve_page(created_before: float) -> bool:
|
44
|
+
max_page_size = 100
|
45
|
+
pagination = api_pb2.ListPagination(max_objects=max_page_size, created_before=created_before)
|
46
|
+
req = api_pb2.SecretListRequest(environment_name=env, pagination=pagination)
|
47
|
+
resp = await retry_transient_errors(client.stub.SecretList, req)
|
48
|
+
items.extend(resp.items)
|
49
|
+
return len(resp.items) < max_page_size
|
50
|
+
|
51
|
+
finished = await retrieve_page(datetime.now().timestamp())
|
52
|
+
while True:
|
53
|
+
if finished:
|
54
|
+
break
|
55
|
+
finished = await retrieve_page(items[-1].metadata.creation_info.created_at)
|
56
|
+
|
57
|
+
secrets = [_Secret._new_hydrated(item.secret_id, client, item.metadata, is_another_app=True) for item in items]
|
58
|
+
|
59
|
+
rows = []
|
60
|
+
for obj, resp_data in zip(secrets, items):
|
61
|
+
info = await obj.info()
|
38
62
|
rows.append(
|
39
63
|
[
|
40
|
-
|
41
|
-
timestamp_to_localized_str(
|
42
|
-
|
64
|
+
obj.name,
|
65
|
+
timestamp_to_localized_str(info.created_at.timestamp(), json),
|
66
|
+
info.created_by,
|
67
|
+
timestamp_to_localized_str(resp_data.last_used_at, json) if resp_data.last_used_at else "-",
|
43
68
|
]
|
44
69
|
)
|
45
70
|
|
46
71
|
env_part = f" in environment '{env}'" if env else ""
|
72
|
+
column_names = ["Name", "Created at", "Created by", "Last used at"]
|
47
73
|
display_table(column_names, rows, json, title=f"Secrets{env_part}")
|
48
74
|
|
49
75
|
|
modal/cli/volume.py
CHANGED
@@ -13,11 +13,9 @@ from typer import Argument, Option, Typer
|
|
13
13
|
import modal
|
14
14
|
from modal._output import OutputManager, ProgressHandler, make_console
|
15
15
|
from modal._utils.async_utils import synchronizer
|
16
|
-
from modal._utils.grpc_utils import retry_transient_errors
|
17
16
|
from modal._utils.time_utils import timestamp_to_localized_str
|
18
17
|
from modal.cli._download import _volume_download
|
19
18
|
from modal.cli.utils import ENV_OPTION, YES_OPTION, display_table
|
20
|
-
from modal.client import _Client
|
21
19
|
from modal.environments import ensure_env
|
22
20
|
from modal.volume import _AbstractVolumeUploadContextManager, _Volume
|
23
21
|
from modal_proto import api_pb2
|
@@ -110,14 +108,13 @@ async def get(
|
|
110
108
|
@synchronizer.create_blocking
|
111
109
|
async def list_(env: Optional[str] = ENV_OPTION, json: Optional[bool] = False):
|
112
110
|
env = ensure_env(env)
|
113
|
-
|
114
|
-
response = await retry_transient_errors(client.stub.VolumeList, api_pb2.VolumeListRequest(environment_name=env))
|
115
|
-
env_part = f" in environment '{env}'" if env else ""
|
116
|
-
column_names = ["Name", "Created at"]
|
111
|
+
volumes = await _Volume.objects.list(environment_name=env)
|
117
112
|
rows = []
|
118
|
-
for
|
119
|
-
|
120
|
-
|
113
|
+
for obj in volumes:
|
114
|
+
info = await obj.info()
|
115
|
+
rows.append((info.name, timestamp_to_localized_str(info.created_at.timestamp(), json), info.created_by))
|
116
|
+
|
117
|
+
display_table(["Name", "Created at", "Created by"], rows, json)
|
121
118
|
|
122
119
|
|
123
120
|
@volume_cli.command(
|
modal/client.pyi
CHANGED
@@ -33,7 +33,7 @@ class _Client:
|
|
33
33
|
server_url: str,
|
34
34
|
client_type: int,
|
35
35
|
credentials: typing.Optional[tuple[str, str]],
|
36
|
-
version: str = "1.1.2.
|
36
|
+
version: str = "1.1.2.dev12",
|
37
37
|
):
|
38
38
|
"""mdmd:hidden
|
39
39
|
The Modal client object is not intended to be instantiated directly by users.
|
@@ -164,7 +164,7 @@ class Client:
|
|
164
164
|
server_url: str,
|
165
165
|
client_type: int,
|
166
166
|
credentials: typing.Optional[tuple[str, str]],
|
167
|
-
version: str = "1.1.2.
|
167
|
+
version: str = "1.1.2.dev12",
|
168
168
|
):
|
169
169
|
"""mdmd:hidden
|
170
170
|
The Modal client object is not intended to be instantiated directly by users.
|
modal/dict.py
CHANGED
@@ -2,25 +2,32 @@
|
|
2
2
|
from collections.abc import AsyncIterator, Mapping
|
3
3
|
from dataclasses import dataclass
|
4
4
|
from datetime import datetime
|
5
|
-
from typing import Any, Optional
|
5
|
+
from typing import Any, Optional, Union
|
6
6
|
|
7
7
|
from google.protobuf.message import Message
|
8
8
|
from grpclib import GRPCError
|
9
|
+
from synchronicity import classproperty
|
9
10
|
from synchronicity.async_wrap import asynccontextmanager
|
10
11
|
|
11
12
|
from modal_proto import api_pb2
|
12
13
|
|
13
|
-
from ._object import
|
14
|
+
from ._object import (
|
15
|
+
EPHEMERAL_OBJECT_HEARTBEAT_SLEEP,
|
16
|
+
_get_environment_name,
|
17
|
+
_Object,
|
18
|
+
live_method,
|
19
|
+
live_method_gen,
|
20
|
+
)
|
14
21
|
from ._resolver import Resolver
|
15
22
|
from ._serialization import deserialize, serialize
|
16
23
|
from ._utils.async_utils import TaskContext, synchronize_api
|
17
24
|
from ._utils.deprecation import deprecation_warning, warn_if_passing_namespace
|
18
25
|
from ._utils.grpc_utils import retry_transient_errors
|
19
26
|
from ._utils.name_utils import check_object_name
|
20
|
-
from ._utils.time_utils import timestamp_to_localized_dt
|
27
|
+
from ._utils.time_utils import as_timestamp, timestamp_to_localized_dt
|
21
28
|
from .client import _Client
|
22
29
|
from .config import logger
|
23
|
-
from .exception import RequestSizeError
|
30
|
+
from .exception import InvalidError, RequestSizeError
|
24
31
|
|
25
32
|
|
26
33
|
def _serialize_dict(data):
|
@@ -29,7 +36,7 @@ def _serialize_dict(data):
|
|
29
36
|
|
30
37
|
@dataclass
|
31
38
|
class DictInfo:
|
32
|
-
"""Information about
|
39
|
+
"""Information about a Dict object."""
|
33
40
|
|
34
41
|
# This dataclass should be limited to information that is unchanging over the lifetime of the Dict,
|
35
42
|
# since it is transmitted from the server when the object is hydrated and could be stale when accessed.
|
@@ -39,6 +46,68 @@ class DictInfo:
|
|
39
46
|
created_by: Optional[str]
|
40
47
|
|
41
48
|
|
49
|
+
class _DictManager:
|
50
|
+
"""Namespace with methods for managing named Dict objects."""
|
51
|
+
|
52
|
+
@staticmethod
|
53
|
+
async def list(
|
54
|
+
*,
|
55
|
+
max_objects: Optional[int] = None, # Limit results to this size
|
56
|
+
created_before: Optional[Union[datetime, str]] = None, # Limit based on creation date
|
57
|
+
environment_name: str = "", # Uses active environment if not specified
|
58
|
+
client: Optional[_Client] = None, # Optional client with Modal credentials
|
59
|
+
) -> list["_Dict"]:
|
60
|
+
"""Return a list of hydrated Dict objects.
|
61
|
+
|
62
|
+
**Examples:**
|
63
|
+
|
64
|
+
```python
|
65
|
+
dicts = modal.Dict.objects.list()
|
66
|
+
print([d.name for d in dicts])
|
67
|
+
```
|
68
|
+
|
69
|
+
Dicts will be retreived from the active environment, or another one can be specified:
|
70
|
+
|
71
|
+
```python notest
|
72
|
+
dev_dicts = modal.Dict.objects.list(environment_name="dev")
|
73
|
+
```
|
74
|
+
|
75
|
+
By default, all named Dict are returned, newest to oldest. It's also possible to limit the
|
76
|
+
number of results and to filter by creation date:
|
77
|
+
|
78
|
+
```python
|
79
|
+
dicts = modal.Dict.objects.list(max_objects=10, created_before="2025-01-01")
|
80
|
+
```
|
81
|
+
|
82
|
+
"""
|
83
|
+
client = await _Client.from_env() if client is None else client
|
84
|
+
if max_objects is not None and max_objects < 0:
|
85
|
+
raise InvalidError("max_objects cannot be negative")
|
86
|
+
|
87
|
+
items: list[api_pb2.DictListResponse.DictInfo] = []
|
88
|
+
|
89
|
+
async def retrieve_page(created_before: float) -> bool:
|
90
|
+
max_page_size = 100 if max_objects is None else min(100, max_objects - len(items))
|
91
|
+
pagination = api_pb2.ListPagination(max_objects=max_page_size, created_before=created_before)
|
92
|
+
req = api_pb2.DictListRequest(environment_name=environment_name, pagination=pagination)
|
93
|
+
resp = await retry_transient_errors(client.stub.DictList, req)
|
94
|
+
items.extend(resp.dicts)
|
95
|
+
finished = (len(resp.dicts) < max_page_size) or (max_objects is not None and len(items) >= max_objects)
|
96
|
+
return finished
|
97
|
+
|
98
|
+
finished = await retrieve_page(as_timestamp(created_before))
|
99
|
+
while True:
|
100
|
+
if finished:
|
101
|
+
break
|
102
|
+
finished = await retrieve_page(items[-1].metadata.creation_info.created_at)
|
103
|
+
|
104
|
+
dicts = [_Dict._new_hydrated(item.dict_id, client, item.metadata, is_another_app=True) for item in items]
|
105
|
+
return dicts[:max_objects] if max_objects is not None else dicts
|
106
|
+
|
107
|
+
|
108
|
+
DictManager = synchronize_api(_DictManager)
|
109
|
+
|
110
|
+
|
42
111
|
class _Dict(_Object, type_prefix="di"):
|
43
112
|
"""Distributed dictionary for storage in Modal apps.
|
44
113
|
|
@@ -90,6 +159,10 @@ class _Dict(_Object, type_prefix="di"):
|
|
90
159
|
"`Dict(...)` constructor is not allowed. Please use `Dict.from_name` or `Dict.ephemeral` instead"
|
91
160
|
)
|
92
161
|
|
162
|
+
@classproperty
|
163
|
+
def objects(cls) -> _DictManager:
|
164
|
+
return _DictManager
|
165
|
+
|
93
166
|
@property
|
94
167
|
def name(self) -> Optional[str]:
|
95
168
|
return self._name
|
modal/dict.pyi
CHANGED
@@ -5,6 +5,7 @@ import modal._object
|
|
5
5
|
import modal.client
|
6
6
|
import modal.object
|
7
7
|
import modal_proto.api_pb2
|
8
|
+
import synchronicity
|
8
9
|
import synchronicity.combined_types
|
9
10
|
import typing
|
10
11
|
import typing_extensions
|
@@ -12,7 +13,7 @@ import typing_extensions
|
|
12
13
|
def _serialize_dict(data): ...
|
13
14
|
|
14
15
|
class DictInfo:
|
15
|
-
"""Information about
|
16
|
+
"""Information about a Dict object."""
|
16
17
|
|
17
18
|
name: typing.Optional[str]
|
18
19
|
created_at: datetime.datetime
|
@@ -32,6 +33,115 @@ class DictInfo:
|
|
32
33
|
"""Return self==value."""
|
33
34
|
...
|
34
35
|
|
36
|
+
class _DictManager:
|
37
|
+
"""Namespace with methods for managing named Dict objects."""
|
38
|
+
@staticmethod
|
39
|
+
async def list(
|
40
|
+
*,
|
41
|
+
max_objects: typing.Optional[int] = None,
|
42
|
+
created_before: typing.Union[datetime.datetime, str, None] = None,
|
43
|
+
environment_name: str = "",
|
44
|
+
client: typing.Optional[modal.client._Client] = None,
|
45
|
+
) -> list[_Dict]:
|
46
|
+
"""Return a list of hydrated Dict objects.
|
47
|
+
|
48
|
+
**Examples:**
|
49
|
+
|
50
|
+
```python
|
51
|
+
dicts = modal.Dict.objects.list()
|
52
|
+
print([d.name for d in dicts])
|
53
|
+
```
|
54
|
+
|
55
|
+
Dicts will be retreived from the active environment, or another one can be specified:
|
56
|
+
|
57
|
+
```python notest
|
58
|
+
dev_dicts = modal.Dict.objects.list(environment_name="dev")
|
59
|
+
```
|
60
|
+
|
61
|
+
By default, all named Dict are returned, newest to oldest. It's also possible to limit the
|
62
|
+
number of results and to filter by creation date:
|
63
|
+
|
64
|
+
```python
|
65
|
+
dicts = modal.Dict.objects.list(max_objects=10, created_before="2025-01-01")
|
66
|
+
```
|
67
|
+
"""
|
68
|
+
...
|
69
|
+
|
70
|
+
class DictManager:
|
71
|
+
"""Namespace with methods for managing named Dict objects."""
|
72
|
+
def __init__(self, /, *args, **kwargs):
|
73
|
+
"""Initialize self. See help(type(self)) for accurate signature."""
|
74
|
+
...
|
75
|
+
|
76
|
+
class __list_spec(typing_extensions.Protocol):
|
77
|
+
def __call__(
|
78
|
+
self,
|
79
|
+
/,
|
80
|
+
*,
|
81
|
+
max_objects: typing.Optional[int] = None,
|
82
|
+
created_before: typing.Union[datetime.datetime, str, None] = None,
|
83
|
+
environment_name: str = "",
|
84
|
+
client: typing.Optional[modal.client.Client] = None,
|
85
|
+
) -> list[Dict]:
|
86
|
+
"""Return a list of hydrated Dict objects.
|
87
|
+
|
88
|
+
**Examples:**
|
89
|
+
|
90
|
+
```python
|
91
|
+
dicts = modal.Dict.objects.list()
|
92
|
+
print([d.name for d in dicts])
|
93
|
+
```
|
94
|
+
|
95
|
+
Dicts will be retreived from the active environment, or another one can be specified:
|
96
|
+
|
97
|
+
```python notest
|
98
|
+
dev_dicts = modal.Dict.objects.list(environment_name="dev")
|
99
|
+
```
|
100
|
+
|
101
|
+
By default, all named Dict are returned, newest to oldest. It's also possible to limit the
|
102
|
+
number of results and to filter by creation date:
|
103
|
+
|
104
|
+
```python
|
105
|
+
dicts = modal.Dict.objects.list(max_objects=10, created_before="2025-01-01")
|
106
|
+
```
|
107
|
+
"""
|
108
|
+
...
|
109
|
+
|
110
|
+
async def aio(
|
111
|
+
self,
|
112
|
+
/,
|
113
|
+
*,
|
114
|
+
max_objects: typing.Optional[int] = None,
|
115
|
+
created_before: typing.Union[datetime.datetime, str, None] = None,
|
116
|
+
environment_name: str = "",
|
117
|
+
client: typing.Optional[modal.client.Client] = None,
|
118
|
+
) -> list[Dict]:
|
119
|
+
"""Return a list of hydrated Dict objects.
|
120
|
+
|
121
|
+
**Examples:**
|
122
|
+
|
123
|
+
```python
|
124
|
+
dicts = modal.Dict.objects.list()
|
125
|
+
print([d.name for d in dicts])
|
126
|
+
```
|
127
|
+
|
128
|
+
Dicts will be retreived from the active environment, or another one can be specified:
|
129
|
+
|
130
|
+
```python notest
|
131
|
+
dev_dicts = modal.Dict.objects.list(environment_name="dev")
|
132
|
+
```
|
133
|
+
|
134
|
+
By default, all named Dict are returned, newest to oldest. It's also possible to limit the
|
135
|
+
number of results and to filter by creation date:
|
136
|
+
|
137
|
+
```python
|
138
|
+
dicts = modal.Dict.objects.list(max_objects=10, created_before="2025-01-01")
|
139
|
+
```
|
140
|
+
"""
|
141
|
+
...
|
142
|
+
|
143
|
+
list: __list_spec
|
144
|
+
|
35
145
|
class _Dict(modal._object._Object):
|
36
146
|
"""Distributed dictionary for storage in Modal apps.
|
37
147
|
|
@@ -81,6 +191,8 @@ class _Dict(modal._object._Object):
|
|
81
191
|
"""mdmd:hidden"""
|
82
192
|
...
|
83
193
|
|
194
|
+
@synchronicity.classproperty
|
195
|
+
def objects(cls) -> _DictManager: ...
|
84
196
|
@property
|
85
197
|
def name(self) -> typing.Optional[str]: ...
|
86
198
|
def _hydrate_metadata(self, metadata: typing.Optional[google.protobuf.message.Message]): ...
|
@@ -308,6 +420,8 @@ class Dict(modal.object.Object):
|
|
308
420
|
"""mdmd:hidden"""
|
309
421
|
...
|
310
422
|
|
423
|
+
@synchronicity.classproperty
|
424
|
+
def objects(cls) -> DictManager: ...
|
311
425
|
@property
|
312
426
|
def name(self) -> typing.Optional[str]: ...
|
313
427
|
def _hydrate_metadata(self, metadata: typing.Optional[google.protobuf.message.Message]): ...
|
modal/queue.py
CHANGED
@@ -5,22 +5,29 @@ import warnings
|
|
5
5
|
from collections.abc import AsyncGenerator, AsyncIterator
|
6
6
|
from dataclasses import dataclass
|
7
7
|
from datetime import datetime
|
8
|
-
from typing import Any, Optional
|
8
|
+
from typing import Any, Optional, Union
|
9
9
|
|
10
10
|
from google.protobuf.message import Message
|
11
11
|
from grpclib import GRPCError, Status
|
12
|
+
from synchronicity import classproperty
|
12
13
|
from synchronicity.async_wrap import asynccontextmanager
|
13
14
|
|
14
15
|
from modal_proto import api_pb2
|
15
16
|
|
16
|
-
from ._object import
|
17
|
+
from ._object import (
|
18
|
+
EPHEMERAL_OBJECT_HEARTBEAT_SLEEP,
|
19
|
+
_get_environment_name,
|
20
|
+
_Object,
|
21
|
+
live_method,
|
22
|
+
live_method_gen,
|
23
|
+
)
|
17
24
|
from ._resolver import Resolver
|
18
25
|
from ._serialization import deserialize, serialize
|
19
26
|
from ._utils.async_utils import TaskContext, synchronize_api, warn_if_generator_is_not_consumed
|
20
27
|
from ._utils.deprecation import deprecation_warning, warn_if_passing_namespace
|
21
28
|
from ._utils.grpc_utils import retry_transient_errors
|
22
29
|
from ._utils.name_utils import check_object_name
|
23
|
-
from ._utils.time_utils import timestamp_to_localized_dt
|
30
|
+
from ._utils.time_utils import as_timestamp, timestamp_to_localized_dt
|
24
31
|
from .client import _Client
|
25
32
|
from .exception import InvalidError, RequestSizeError
|
26
33
|
|
@@ -37,6 +44,68 @@ class QueueInfo:
|
|
37
44
|
created_by: Optional[str]
|
38
45
|
|
39
46
|
|
47
|
+
class _QueueManager:
|
48
|
+
"""Namespace with methods for managing named Queue objects."""
|
49
|
+
|
50
|
+
@staticmethod
|
51
|
+
async def list(
|
52
|
+
*,
|
53
|
+
max_objects: Optional[int] = None, # Limit requests to this size
|
54
|
+
created_before: Optional[Union[datetime, str]] = None, # Limit based on creation date
|
55
|
+
environment_name: str = "", # Uses active environment if not specified
|
56
|
+
client: Optional[_Client] = None, # Optional client with Modal credentials
|
57
|
+
) -> list["_Queue"]:
|
58
|
+
"""Return a list of hydrated Queue objects.
|
59
|
+
|
60
|
+
**Examples:**
|
61
|
+
|
62
|
+
```python
|
63
|
+
queues = modal.Queue.objects.list()
|
64
|
+
print([q.name for q in queues])
|
65
|
+
```
|
66
|
+
|
67
|
+
Queues will be retreived from the active environment, or another one can be specified:
|
68
|
+
|
69
|
+
```python notest
|
70
|
+
dev_queues = modal.Queue.objects.list(environment_name="dev")
|
71
|
+
```
|
72
|
+
|
73
|
+
By default, all named Queues are returned, newest to oldest. It's also possible to limit the
|
74
|
+
number of results and to filter by creation date:
|
75
|
+
|
76
|
+
```python
|
77
|
+
queues = modal.Queue.objects.list(max_objects=10, created_before="2025-01-01")
|
78
|
+
```
|
79
|
+
|
80
|
+
"""
|
81
|
+
client = await _Client.from_env() if client is None else client
|
82
|
+
if max_objects is not None and max_objects < 0:
|
83
|
+
raise InvalidError("max_objects cannot be negative")
|
84
|
+
|
85
|
+
items: list[api_pb2.QueueListResponse.QueueInfo] = []
|
86
|
+
|
87
|
+
async def retrieve_page(created_before: float) -> bool:
|
88
|
+
max_page_size = 100 if max_objects is None else min(100, max_objects - len(items))
|
89
|
+
pagination = api_pb2.ListPagination(max_objects=max_page_size, created_before=created_before)
|
90
|
+
req = api_pb2.QueueListRequest(environment_name=environment_name, pagination=pagination)
|
91
|
+
resp = await retry_transient_errors(client.stub.QueueList, req)
|
92
|
+
items.extend(resp.queues)
|
93
|
+
finished = (len(resp.queues) < max_page_size) or (max_objects is not None and len(items) >= max_objects)
|
94
|
+
return finished
|
95
|
+
|
96
|
+
finished = await retrieve_page(as_timestamp(created_before))
|
97
|
+
while True:
|
98
|
+
if finished:
|
99
|
+
break
|
100
|
+
finished = await retrieve_page(items[-1].metadata.creation_info.created_at)
|
101
|
+
|
102
|
+
queues = [_Queue._new_hydrated(item.queue_id, client, item.metadata, is_another_app=True) for item in items]
|
103
|
+
return queues[:max_objects] if max_objects is not None else queues
|
104
|
+
|
105
|
+
|
106
|
+
QueueManager = synchronize_api(_QueueManager)
|
107
|
+
|
108
|
+
|
40
109
|
class _Queue(_Object, type_prefix="qu"):
|
41
110
|
"""Distributed, FIFO queue for data flow in Modal apps.
|
42
111
|
|
@@ -116,6 +185,10 @@ class _Queue(_Object, type_prefix="qu"):
|
|
116
185
|
"""mdmd:hidden"""
|
117
186
|
raise RuntimeError("Queue() is not allowed. Please use `Queue.from_name(...)` or `Queue.ephemeral()` instead.")
|
118
187
|
|
188
|
+
@classproperty
|
189
|
+
def objects(cls) -> _QueueManager:
|
190
|
+
return _QueueManager
|
191
|
+
|
119
192
|
@property
|
120
193
|
def name(self) -> Optional[str]:
|
121
194
|
return self._name
|