modal 1.1.2.dev10__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/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 EPHEMERAL_OBJECT_HEARTBEAT_SLEEP, _get_environment_name, _Object, live_method, live_method_gen
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
modal/queue.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
@@ -30,6 +31,115 @@ class QueueInfo:
30
31
  """Return self==value."""
31
32
  ...
32
33
 
34
+ class _QueueManager:
35
+ """Namespace with methods for managing named Queue objects."""
36
+ @staticmethod
37
+ async def list(
38
+ *,
39
+ max_objects: typing.Optional[int] = None,
40
+ created_before: typing.Union[datetime.datetime, str, None] = None,
41
+ environment_name: str = "",
42
+ client: typing.Optional[modal.client._Client] = None,
43
+ ) -> list[_Queue]:
44
+ """Return a list of hydrated Queue objects.
45
+
46
+ **Examples:**
47
+
48
+ ```python
49
+ queues = modal.Queue.objects.list()
50
+ print([q.name for q in queues])
51
+ ```
52
+
53
+ Queues will be retreived from the active environment, or another one can be specified:
54
+
55
+ ```python notest
56
+ dev_queues = modal.Queue.objects.list(environment_name="dev")
57
+ ```
58
+
59
+ By default, all named Queues are returned, newest to oldest. It's also possible to limit the
60
+ number of results and to filter by creation date:
61
+
62
+ ```python
63
+ queues = modal.Queue.objects.list(max_objects=10, created_before="2025-01-01")
64
+ ```
65
+ """
66
+ ...
67
+
68
+ class QueueManager:
69
+ """Namespace with methods for managing named Queue objects."""
70
+ def __init__(self, /, *args, **kwargs):
71
+ """Initialize self. See help(type(self)) for accurate signature."""
72
+ ...
73
+
74
+ class __list_spec(typing_extensions.Protocol):
75
+ def __call__(
76
+ self,
77
+ /,
78
+ *,
79
+ max_objects: typing.Optional[int] = None,
80
+ created_before: typing.Union[datetime.datetime, str, None] = None,
81
+ environment_name: str = "",
82
+ client: typing.Optional[modal.client.Client] = None,
83
+ ) -> list[Queue]:
84
+ """Return a list of hydrated Queue objects.
85
+
86
+ **Examples:**
87
+
88
+ ```python
89
+ queues = modal.Queue.objects.list()
90
+ print([q.name for q in queues])
91
+ ```
92
+
93
+ Queues will be retreived from the active environment, or another one can be specified:
94
+
95
+ ```python notest
96
+ dev_queues = modal.Queue.objects.list(environment_name="dev")
97
+ ```
98
+
99
+ By default, all named Queues are returned, newest to oldest. It's also possible to limit the
100
+ number of results and to filter by creation date:
101
+
102
+ ```python
103
+ queues = modal.Queue.objects.list(max_objects=10, created_before="2025-01-01")
104
+ ```
105
+ """
106
+ ...
107
+
108
+ async def aio(
109
+ self,
110
+ /,
111
+ *,
112
+ max_objects: typing.Optional[int] = None,
113
+ created_before: typing.Union[datetime.datetime, str, None] = None,
114
+ environment_name: str = "",
115
+ client: typing.Optional[modal.client.Client] = None,
116
+ ) -> list[Queue]:
117
+ """Return a list of hydrated Queue objects.
118
+
119
+ **Examples:**
120
+
121
+ ```python
122
+ queues = modal.Queue.objects.list()
123
+ print([q.name for q in queues])
124
+ ```
125
+
126
+ Queues will be retreived from the active environment, or another one can be specified:
127
+
128
+ ```python notest
129
+ dev_queues = modal.Queue.objects.list(environment_name="dev")
130
+ ```
131
+
132
+ By default, all named Queues are returned, newest to oldest. It's also possible to limit the
133
+ number of results and to filter by creation date:
134
+
135
+ ```python
136
+ queues = modal.Queue.objects.list(max_objects=10, created_before="2025-01-01")
137
+ ```
138
+ """
139
+ ...
140
+
141
+ list: __list_spec
142
+
33
143
  class _Queue(modal._object._Object):
34
144
  """Distributed, FIFO queue for data flow in Modal apps.
35
145
 
@@ -109,6 +219,8 @@ class _Queue(modal._object._Object):
109
219
  """mdmd:hidden"""
110
220
  ...
111
221
 
222
+ @synchronicity.classproperty
223
+ def objects(cls) -> _QueueManager: ...
112
224
  @property
113
225
  def name(self) -> typing.Optional[str]: ...
114
226
  def _hydrate_metadata(self, metadata: typing.Optional[google.protobuf.message.Message]): ...
@@ -377,6 +489,8 @@ class Queue(modal.object.Object):
377
489
  """mdmd:hidden"""
378
490
  ...
379
491
 
492
+ @synchronicity.classproperty
493
+ def objects(cls) -> QueueManager: ...
380
494
  @property
381
495
  def name(self) -> typing.Optional[str]: ...
382
496
  def _hydrate_metadata(self, metadata: typing.Optional[google.protobuf.message.Message]): ...
modal/secret.py CHANGED
@@ -6,6 +6,7 @@ from typing import Optional, Union
6
6
 
7
7
  from google.protobuf.message import Message
8
8
  from grpclib import GRPCError, Status
9
+ from synchronicity import classproperty
9
10
 
10
11
  from modal_proto import api_pb2
11
12
 
@@ -16,7 +17,7 @@ from ._utils.async_utils import synchronize_api
16
17
  from ._utils.deprecation import deprecation_warning, warn_if_passing_namespace
17
18
  from ._utils.grpc_utils import retry_transient_errors
18
19
  from ._utils.name_utils import check_object_name
19
- from ._utils.time_utils import timestamp_to_localized_dt
20
+ from ._utils.time_utils import as_timestamp, timestamp_to_localized_dt
20
21
  from .client import _Client
21
22
  from .exception import InvalidError, NotFoundError
22
23
 
@@ -35,6 +36,68 @@ class SecretInfo:
35
36
  created_by: Optional[str]
36
37
 
37
38
 
39
+ class _SecretManager:
40
+ """Namespace with methods for managing named Secret objects."""
41
+
42
+ @staticmethod
43
+ async def list(
44
+ *,
45
+ max_objects: Optional[int] = None, # Limit requests to this size
46
+ created_before: Optional[Union[datetime, str]] = None, # Limit based on creation date
47
+ environment_name: str = "", # Uses active environment if not specified
48
+ client: Optional[_Client] = None, # Optional client with Modal credentials
49
+ ) -> list["_Secret"]:
50
+ """Return a list of hydrated Secret objects.
51
+
52
+ **Examples:**
53
+
54
+ ```python
55
+ secrets = modal.Secret.objects.list()
56
+ print([s.name for s in secrets])
57
+ ```
58
+
59
+ Secrets will be retreived from the active environment, or another one can be specified:
60
+
61
+ ```python notest
62
+ dev_secrets = modal.Secret.objects.list(environment_name="dev")
63
+ ```
64
+
65
+ By default, all named Secrets are returned, newest to oldest. It's also possible to limit the
66
+ number of results and to filter by creation date:
67
+
68
+ ```python
69
+ secrets = modal.Secret.objects.list(max_objects=10, created_before="2025-01-01")
70
+ ```
71
+
72
+ """
73
+ client = await _Client.from_env() if client is None else client
74
+ if max_objects is not None and max_objects < 0:
75
+ raise InvalidError("max_objects cannot be negative")
76
+
77
+ items: list[api_pb2.SecretListItem] = []
78
+
79
+ async def retrieve_page(created_before: float) -> bool:
80
+ max_page_size = 100 if max_objects is None else min(100, max_objects - len(items))
81
+ pagination = api_pb2.ListPagination(max_objects=max_page_size, created_before=created_before)
82
+ req = api_pb2.SecretListRequest(environment_name=environment_name, pagination=pagination)
83
+ resp = await retry_transient_errors(client.stub.SecretList, req)
84
+ items.extend(resp.items)
85
+ finished = (len(resp.items) < max_page_size) or (max_objects is not None and len(items) >= max_objects)
86
+ return finished
87
+
88
+ finished = await retrieve_page(as_timestamp(created_before))
89
+ while True:
90
+ if finished:
91
+ break
92
+ finished = await retrieve_page(items[-1].metadata.creation_info.created_at)
93
+
94
+ secrets = [_Secret._new_hydrated(item.secret_id, client, item.metadata, is_another_app=True) for item in items]
95
+ return secrets[:max_objects] if max_objects is not None else secrets
96
+
97
+
98
+ SecretManager = synchronize_api(_SecretManager)
99
+
100
+
38
101
  class _Secret(_Object, type_prefix="st"):
39
102
  """Secrets provide a dictionary of environment variables for images.
40
103
 
@@ -47,6 +110,10 @@ class _Secret(_Object, type_prefix="st"):
47
110
 
48
111
  _metadata: Optional[api_pb2.SecretMetadata] = None
49
112
 
113
+ @classproperty
114
+ def objects(cls) -> _SecretManager:
115
+ return _SecretManager
116
+
50
117
  @property
51
118
  def name(self) -> Optional[str]:
52
119
  return self._name
modal/secret.pyi CHANGED
@@ -4,6 +4,7 @@ import modal._object
4
4
  import modal.client
5
5
  import modal.object
6
6
  import modal_proto.api_pb2
7
+ import synchronicity
7
8
  import typing
8
9
  import typing_extensions
9
10
 
@@ -28,6 +29,115 @@ class SecretInfo:
28
29
  """Return self==value."""
29
30
  ...
30
31
 
32
+ class _SecretManager:
33
+ """Namespace with methods for managing named Secret objects."""
34
+ @staticmethod
35
+ async def list(
36
+ *,
37
+ max_objects: typing.Optional[int] = None,
38
+ created_before: typing.Union[datetime.datetime, str, None] = None,
39
+ environment_name: str = "",
40
+ client: typing.Optional[modal.client._Client] = None,
41
+ ) -> list[_Secret]:
42
+ """Return a list of hydrated Secret objects.
43
+
44
+ **Examples:**
45
+
46
+ ```python
47
+ secrets = modal.Secret.objects.list()
48
+ print([s.name for s in secrets])
49
+ ```
50
+
51
+ Secrets will be retreived from the active environment, or another one can be specified:
52
+
53
+ ```python notest
54
+ dev_secrets = modal.Secret.objects.list(environment_name="dev")
55
+ ```
56
+
57
+ By default, all named Secrets are returned, newest to oldest. It's also possible to limit the
58
+ number of results and to filter by creation date:
59
+
60
+ ```python
61
+ secrets = modal.Secret.objects.list(max_objects=10, created_before="2025-01-01")
62
+ ```
63
+ """
64
+ ...
65
+
66
+ class SecretManager:
67
+ """Namespace with methods for managing named Secret objects."""
68
+ def __init__(self, /, *args, **kwargs):
69
+ """Initialize self. See help(type(self)) for accurate signature."""
70
+ ...
71
+
72
+ class __list_spec(typing_extensions.Protocol):
73
+ def __call__(
74
+ self,
75
+ /,
76
+ *,
77
+ max_objects: typing.Optional[int] = None,
78
+ created_before: typing.Union[datetime.datetime, str, None] = None,
79
+ environment_name: str = "",
80
+ client: typing.Optional[modal.client.Client] = None,
81
+ ) -> list[Secret]:
82
+ """Return a list of hydrated Secret objects.
83
+
84
+ **Examples:**
85
+
86
+ ```python
87
+ secrets = modal.Secret.objects.list()
88
+ print([s.name for s in secrets])
89
+ ```
90
+
91
+ Secrets will be retreived from the active environment, or another one can be specified:
92
+
93
+ ```python notest
94
+ dev_secrets = modal.Secret.objects.list(environment_name="dev")
95
+ ```
96
+
97
+ By default, all named Secrets are returned, newest to oldest. It's also possible to limit the
98
+ number of results and to filter by creation date:
99
+
100
+ ```python
101
+ secrets = modal.Secret.objects.list(max_objects=10, created_before="2025-01-01")
102
+ ```
103
+ """
104
+ ...
105
+
106
+ async def aio(
107
+ self,
108
+ /,
109
+ *,
110
+ max_objects: typing.Optional[int] = None,
111
+ created_before: typing.Union[datetime.datetime, str, None] = None,
112
+ environment_name: str = "",
113
+ client: typing.Optional[modal.client.Client] = None,
114
+ ) -> list[Secret]:
115
+ """Return a list of hydrated Secret objects.
116
+
117
+ **Examples:**
118
+
119
+ ```python
120
+ secrets = modal.Secret.objects.list()
121
+ print([s.name for s in secrets])
122
+ ```
123
+
124
+ Secrets will be retreived from the active environment, or another one can be specified:
125
+
126
+ ```python notest
127
+ dev_secrets = modal.Secret.objects.list(environment_name="dev")
128
+ ```
129
+
130
+ By default, all named Secrets are returned, newest to oldest. It's also possible to limit the
131
+ number of results and to filter by creation date:
132
+
133
+ ```python
134
+ secrets = modal.Secret.objects.list(max_objects=10, created_before="2025-01-01")
135
+ ```
136
+ """
137
+ ...
138
+
139
+ list: __list_spec
140
+
31
141
  class _Secret(modal._object._Object):
32
142
  """Secrets provide a dictionary of environment variables for images.
33
143
 
@@ -40,6 +150,8 @@ class _Secret(modal._object._Object):
40
150
 
41
151
  _metadata: typing.Optional[modal_proto.api_pb2.SecretMetadata]
42
152
 
153
+ @synchronicity.classproperty
154
+ def objects(cls) -> _SecretManager: ...
43
155
  @property
44
156
  def name(self) -> typing.Optional[str]: ...
45
157
  def _hydrate_metadata(self, metadata: typing.Optional[google.protobuf.message.Message]): ...
@@ -157,6 +269,8 @@ class Secret(modal.object.Object):
157
269
  """mdmd:hidden"""
158
270
  ...
159
271
 
272
+ @synchronicity.classproperty
273
+ def objects(cls) -> SecretManager: ...
160
274
  @property
161
275
  def name(self) -> typing.Optional[str]: ...
162
276
  def _hydrate_metadata(self, metadata: typing.Optional[google.protobuf.message.Message]): ...
modal/volume.py CHANGED
@@ -25,6 +25,7 @@ from typing import (
25
25
 
26
26
  from google.protobuf.message import Message
27
27
  from grpclib import GRPCError, Status
28
+ from synchronicity import classproperty
28
29
  from synchronicity.async_wrap import asynccontextmanager
29
30
 
30
31
  import modal.exception
@@ -32,7 +33,13 @@ import modal_proto.api_pb2
32
33
  from modal.exception import InvalidError, VolumeUploadTimeoutError
33
34
  from modal_proto import api_pb2
34
35
 
35
- from ._object import EPHEMERAL_OBJECT_HEARTBEAT_SLEEP, _get_environment_name, _Object, live_method, live_method_gen
36
+ from ._object import (
37
+ EPHEMERAL_OBJECT_HEARTBEAT_SLEEP,
38
+ _get_environment_name,
39
+ _Object,
40
+ live_method,
41
+ live_method_gen,
42
+ )
36
43
  from ._resolver import Resolver
37
44
  from ._utils.async_utils import (
38
45
  TaskContext,
@@ -55,7 +62,7 @@ from ._utils.deprecation import deprecation_warning, warn_if_passing_namespace
55
62
  from ._utils.grpc_utils import retry_transient_errors
56
63
  from ._utils.http_utils import ClientSessionRegistry
57
64
  from ._utils.name_utils import check_object_name
58
- from ._utils.time_utils import timestamp_to_localized_dt
65
+ from ._utils.time_utils import as_timestamp, timestamp_to_localized_dt
59
66
  from .client import _Client
60
67
  from .config import logger
61
68
 
@@ -106,6 +113,68 @@ class VolumeInfo:
106
113
  created_by: Optional[str]
107
114
 
108
115
 
116
+ class _VolumeManager:
117
+ """Namespace with methods for managing named Volume objects."""
118
+
119
+ @staticmethod
120
+ async def list(
121
+ *,
122
+ max_objects: Optional[int] = None, # Limit requests to this size
123
+ created_before: Optional[Union[datetime, str]] = None, # Limit based on creation date
124
+ environment_name: str = "", # Uses active environment if not specified
125
+ client: Optional[_Client] = None, # Optional client with Modal credentials
126
+ ) -> list["_Volume"]:
127
+ """Return a list of hydrated Volume objects.
128
+
129
+ **Examples:**
130
+
131
+ ```python
132
+ volumes = modal.Volume.objects.list()
133
+ print([v.name for v in volumes])
134
+ ```
135
+
136
+ Volumes will be retreived from the active environment, or another one can be specified:
137
+
138
+ ```python notest
139
+ dev_volumes = modal.Volume.objects.list(environment_name="dev")
140
+ ```
141
+
142
+ By default, all named Volumes are returned, newest to oldest. It's also possible to limit the
143
+ number of results and to filter by creation date:
144
+
145
+ ```python
146
+ volumes = modal.Volume.objects.list(max_objects=10, created_before="2025-01-01")
147
+ ```
148
+
149
+ """
150
+ client = await _Client.from_env() if client is None else client
151
+ if max_objects is not None and max_objects < 0:
152
+ raise InvalidError("max_objects cannot be negative")
153
+
154
+ items: list[api_pb2.VolumeListItem] = []
155
+
156
+ async def retrieve_page(created_before: float) -> bool:
157
+ max_page_size = 100 if max_objects is None else min(100, max_objects - len(items))
158
+ pagination = api_pb2.ListPagination(max_objects=max_page_size, created_before=created_before)
159
+ req = api_pb2.VolumeListRequest(environment_name=environment_name, pagination=pagination)
160
+ resp = await retry_transient_errors(client.stub.VolumeList, req)
161
+ items.extend(resp.items)
162
+ finished = (len(resp.items) < max_page_size) or (max_objects is not None and len(items) >= max_objects)
163
+ return finished
164
+
165
+ finished = await retrieve_page(as_timestamp(created_before))
166
+ while True:
167
+ if finished:
168
+ break
169
+ finished = await retrieve_page(items[-1].metadata.creation_info.created_at)
170
+
171
+ volumes = [_Volume._new_hydrated(item.volume_id, client, item.metadata, is_another_app=True) for item in items]
172
+ return volumes[:max_objects] if max_objects is not None else volumes
173
+
174
+
175
+ VolumeManager = synchronize_api(_VolumeManager)
176
+
177
+
109
178
  class _Volume(_Object, type_prefix="vo"):
110
179
  """A writeable volume that can be used to share files between one or more Modal functions.
111
180
 
@@ -152,6 +221,14 @@ class _Volume(_Object, type_prefix="vo"):
152
221
  _metadata: "typing.Optional[api_pb2.VolumeMetadata]"
153
222
  _read_only: bool = False
154
223
 
224
+ @classproperty
225
+ def objects(cls) -> _VolumeManager:
226
+ return _VolumeManager
227
+
228
+ @property
229
+ def name(self) -> Optional[str]:
230
+ return self._name
231
+
155
232
  def read_only(self) -> "_Volume":
156
233
  """Configure Volume to mount as read-only.
157
234
 
@@ -181,10 +258,6 @@ class _Volume(_Object, type_prefix="vo"):
181
258
  obj = _Volume._from_loader(_load, "Volume()", hydrate_lazily=True, deps=lambda: [self])
182
259
  return obj
183
260
 
184
- @property
185
- def name(self) -> Optional[str]:
186
- return self._name
187
-
188
261
  def _hydrate_metadata(self, metadata: Optional[Message]):
189
262
  if metadata:
190
263
  assert isinstance(metadata, api_pb2.VolumeMetadata)