modal 0.73.27__py3-none-any.whl → 0.73.28__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.
@@ -1,64 +1,16 @@
1
1
  import collections.abc
2
- import enum
3
- import modal._functions
2
+ import modal._partial_function
4
3
  import modal.functions
5
4
  import modal_proto.api_pb2
6
5
  import typing
7
- import typing_extensions
8
6
 
9
- class _PartialFunctionFlags(enum.IntFlag):
10
- FUNCTION = 1
11
- BUILD = 2
12
- ENTER_PRE_SNAPSHOT = 4
13
- ENTER_POST_SNAPSHOT = 8
14
- EXIT = 16
15
- BATCHED = 32
16
- CLUSTERED = 64 # Experimental: Clustered functions
17
-
18
- @staticmethod
19
- def all() -> int:
20
- return ~_PartialFunctionFlags(0)
21
-
22
- P = typing_extensions.ParamSpec("P")
23
-
24
- ReturnType = typing.TypeVar("ReturnType", covariant=True)
25
-
26
- OriginalReturnType = typing.TypeVar("OriginalReturnType", covariant=True)
27
-
28
- class _PartialFunction(typing.Generic[P, ReturnType, OriginalReturnType]):
29
- raw_f: collections.abc.Callable[P, ReturnType]
30
- flags: _PartialFunctionFlags
31
- webhook_config: typing.Optional[modal_proto.api_pb2.WebhookConfig]
32
- is_generator: bool
33
- keep_warm: typing.Optional[int]
34
- batch_max_size: typing.Optional[int]
35
- batch_wait_ms: typing.Optional[int]
36
- force_build: bool
37
- cluster_size: typing.Optional[int]
38
- build_timeout: typing.Optional[int]
39
-
40
- def __init__(
41
- self,
42
- raw_f: collections.abc.Callable[P, ReturnType],
43
- flags: _PartialFunctionFlags,
44
- webhook_config: typing.Optional[modal_proto.api_pb2.WebhookConfig] = None,
45
- is_generator: typing.Optional[bool] = None,
46
- keep_warm: typing.Optional[int] = None,
47
- batch_max_size: typing.Optional[int] = None,
48
- batch_wait_ms: typing.Optional[int] = None,
49
- cluster_size: typing.Optional[int] = None,
50
- force_build: bool = False,
51
- build_timeout: typing.Optional[int] = None,
52
- ): ...
53
- def _get_raw_f(self) -> collections.abc.Callable[P, ReturnType]: ...
54
- def _is_web_endpoint(self) -> bool: ...
55
- def __get__(self, obj, objtype=None) -> modal._functions._Function[P, ReturnType, OriginalReturnType]: ...
56
- def __del__(self): ...
57
- def add_flags(self, flags) -> _PartialFunction: ...
58
-
59
- class PartialFunction(typing.Generic[P, ReturnType, OriginalReturnType]):
60
- raw_f: collections.abc.Callable[P, ReturnType]
61
- flags: _PartialFunctionFlags
7
+ class PartialFunction(
8
+ typing.Generic[
9
+ modal._partial_function.P, modal._partial_function.ReturnType, modal._partial_function.OriginalReturnType
10
+ ]
11
+ ):
12
+ raw_f: collections.abc.Callable[modal._partial_function.P, modal._partial_function.ReturnType]
13
+ flags: modal._partial_function._PartialFunctionFlags
62
14
  webhook_config: typing.Optional[modal_proto.api_pb2.WebhookConfig]
63
15
  is_generator: bool
64
16
  keep_warm: typing.Optional[int]
@@ -70,8 +22,8 @@ class PartialFunction(typing.Generic[P, ReturnType, OriginalReturnType]):
70
22
 
71
23
  def __init__(
72
24
  self,
73
- raw_f: collections.abc.Callable[P, ReturnType],
74
- flags: _PartialFunctionFlags,
25
+ raw_f: collections.abc.Callable[modal._partial_function.P, modal._partial_function.ReturnType],
26
+ flags: modal._partial_function._PartialFunctionFlags,
75
27
  webhook_config: typing.Optional[modal_proto.api_pb2.WebhookConfig] = None,
76
28
  is_generator: typing.Optional[bool] = None,
77
29
  keep_warm: typing.Optional[int] = None,
@@ -81,113 +33,22 @@ class PartialFunction(typing.Generic[P, ReturnType, OriginalReturnType]):
81
33
  force_build: bool = False,
82
34
  build_timeout: typing.Optional[int] = None,
83
35
  ): ...
84
- def _get_raw_f(self) -> collections.abc.Callable[P, ReturnType]: ...
36
+ def _get_raw_f(self) -> collections.abc.Callable[modal._partial_function.P, modal._partial_function.ReturnType]: ...
85
37
  def _is_web_endpoint(self) -> bool: ...
86
- def __get__(self, obj, objtype=None) -> modal.functions.Function[P, ReturnType, OriginalReturnType]: ...
38
+ def __get__(
39
+ self, obj, objtype=None
40
+ ) -> modal.functions.Function[
41
+ modal._partial_function.P, modal._partial_function.ReturnType, modal._partial_function.OriginalReturnType
42
+ ]: ...
87
43
  def __del__(self): ...
88
44
  def add_flags(self, flags) -> PartialFunction: ...
89
45
 
90
- def _find_partial_methods_for_user_cls(user_cls: type[typing.Any], flags: int) -> dict[str, _PartialFunction]: ...
91
- def _find_callables_for_obj(
92
- user_obj: typing.Any, flags: int
93
- ) -> dict[str, collections.abc.Callable[..., typing.Any]]: ...
94
-
95
- class _MethodDecoratorType:
96
- @typing.overload
97
- def __call__(
98
- self, func: PartialFunction[typing_extensions.Concatenate[typing.Any, P], ReturnType, OriginalReturnType]
99
- ) -> PartialFunction[P, ReturnType, OriginalReturnType]: ...
100
- @typing.overload
101
- def __call__(
102
- self,
103
- func: collections.abc.Callable[
104
- typing_extensions.Concatenate[typing.Any, P], collections.abc.Coroutine[typing.Any, typing.Any, ReturnType]
105
- ],
106
- ) -> PartialFunction[P, ReturnType, collections.abc.Coroutine[typing.Any, typing.Any, ReturnType]]: ...
107
- @typing.overload
108
- def __call__(
109
- self, func: collections.abc.Callable[typing_extensions.Concatenate[typing.Any, P], ReturnType]
110
- ) -> PartialFunction[P, ReturnType, ReturnType]: ...
111
-
112
- def _method(
113
- _warn_parentheses_missing=None,
114
- *,
115
- is_generator: typing.Optional[bool] = None,
116
- keep_warm: typing.Optional[int] = None,
117
- ) -> _MethodDecoratorType: ...
118
- def _parse_custom_domains(
119
- custom_domains: typing.Optional[collections.abc.Iterable[str]] = None,
120
- ) -> list[modal_proto.api_pb2.CustomDomainConfig]: ...
121
- def _web_endpoint(
122
- _warn_parentheses_missing=None,
123
- *,
124
- method: str = "GET",
125
- label: typing.Optional[str] = None,
126
- docs: bool = False,
127
- custom_domains: typing.Optional[collections.abc.Iterable[str]] = None,
128
- requires_proxy_auth: bool = False,
129
- wait_for_response: bool = True,
130
- ) -> collections.abc.Callable[
131
- [collections.abc.Callable[P, ReturnType]], _PartialFunction[P, ReturnType, ReturnType]
132
- ]: ...
133
- def _asgi_app(
134
- _warn_parentheses_missing=None,
135
- *,
136
- label: typing.Optional[str] = None,
137
- custom_domains: typing.Optional[collections.abc.Iterable[str]] = None,
138
- requires_proxy_auth: bool = False,
139
- wait_for_response: bool = True,
140
- ) -> collections.abc.Callable[[collections.abc.Callable[..., typing.Any]], _PartialFunction]: ...
141
- def _wsgi_app(
142
- _warn_parentheses_missing=None,
143
- *,
144
- label: typing.Optional[str] = None,
145
- custom_domains: typing.Optional[collections.abc.Iterable[str]] = None,
146
- requires_proxy_auth: bool = False,
147
- wait_for_response: bool = True,
148
- ) -> collections.abc.Callable[[collections.abc.Callable[..., typing.Any]], _PartialFunction]: ...
149
- def _web_server(
150
- port: int,
151
- *,
152
- startup_timeout: float = 5.0,
153
- label: typing.Optional[str] = None,
154
- custom_domains: typing.Optional[collections.abc.Iterable[str]] = None,
155
- requires_proxy_auth: bool = False,
156
- ) -> collections.abc.Callable[[collections.abc.Callable[..., typing.Any]], _PartialFunction]: ...
157
- def _disallow_wrapping_method(f: _PartialFunction, wrapper: str) -> None: ...
158
- def _build(
159
- _warn_parentheses_missing=None, *, force: bool = False, timeout: int = 86400
160
- ) -> collections.abc.Callable[
161
- [typing.Union[collections.abc.Callable[[typing.Any], typing.Any], _PartialFunction]], _PartialFunction
162
- ]: ...
163
- def _enter(
164
- _warn_parentheses_missing=None, *, snap: bool = False
165
- ) -> collections.abc.Callable[
166
- [typing.Union[collections.abc.Callable[[typing.Any], typing.Any], _PartialFunction]], _PartialFunction
167
- ]: ...
168
- def _exit(
169
- _warn_parentheses_missing=None,
170
- ) -> collections.abc.Callable[
171
- [
172
- typing.Union[
173
- collections.abc.Callable[
174
- [typing.Any, typing.Optional[type[BaseException]], typing.Optional[BaseException], typing.Any],
175
- typing.Any,
176
- ],
177
- collections.abc.Callable[[typing.Any], typing.Any],
178
- ]
179
- ],
180
- _PartialFunction,
181
- ]: ...
182
- def _batched(
183
- _warn_parentheses_missing=None, *, max_batch_size: int, wait_ms: int
184
- ) -> collections.abc.Callable[[collections.abc.Callable[..., typing.Any]], _PartialFunction]: ...
185
46
  def method(
186
47
  _warn_parentheses_missing=None,
187
48
  *,
188
49
  is_generator: typing.Optional[bool] = None,
189
50
  keep_warm: typing.Optional[int] = None,
190
- ) -> _MethodDecoratorType: ...
51
+ ) -> modal._partial_function._MethodDecoratorType: ...
191
52
  def web_endpoint(
192
53
  _warn_parentheses_missing=None,
193
54
  *,
@@ -198,7 +59,8 @@ def web_endpoint(
198
59
  requires_proxy_auth: bool = False,
199
60
  wait_for_response: bool = True,
200
61
  ) -> collections.abc.Callable[
201
- [collections.abc.Callable[P, ReturnType]], PartialFunction[P, ReturnType, ReturnType]
62
+ [collections.abc.Callable[modal._partial_function.P, modal._partial_function.ReturnType]],
63
+ PartialFunction[modal._partial_function.P, modal._partial_function.ReturnType, modal._partial_function.ReturnType],
202
64
  ]: ...
203
65
  def asgi_app(
204
66
  _warn_parentheses_missing=None,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: modal
3
- Version: 0.73.27
3
+ Version: 0.73.28
4
4
  Summary: Python client library for Modal
5
5
  Author: Modal Labs
6
6
  Author-email: support@modal.com
@@ -2,12 +2,13 @@ modal/__init__.py,sha256=df6aKAigSPFXnmIohWySf_1zZ9Gzgrb7-oprSbopD4w,2299
2
2
  modal/__main__.py,sha256=scYhGFqh8OJcVDo-VOxIT6CCwxOgzgflYWMnIZiMRqE,2871
3
3
  modal/_clustered_functions.py,sha256=kTf-9YBXY88NutC1akI-gCbvf01RhMPCw-zoOI_YIUE,2700
4
4
  modal/_clustered_functions.pyi,sha256=vllkegc99A0jrUOWa8mdlSbdp6uz36TsHhGxysAOpaQ,771
5
- modal/_container_entrypoint.py,sha256=MlKDaCYso7cK3JxNmVErdm1ADdwK40HsPnrrJKCa8vk,29285
6
- modal/_functions.py,sha256=N9Sp4SDv7oFung2abqy3VAaehaaWR9Y9SWxRlKLARI8,71984
5
+ modal/_container_entrypoint.py,sha256=YtfJ852XUDtAWBD-yVs99zy933-VBEKQyIngEj36Qcw,29286
6
+ modal/_functions.py,sha256=W0gU1MV0105oadLGjuXQErEKh1f0J7_5vX3nYmuQf9k,72021
7
7
  modal/_ipython.py,sha256=TW1fkVOmZL3YYqdS2YlM1hqpf654Yf8ZyybHdBnlhSw,301
8
8
  modal/_location.py,sha256=S3lSxIU3h9HkWpkJ3Pwo0pqjIOSB1fjeSgUsY3x7eec,1202
9
9
  modal/_object.py,sha256=ItQcsMNkz9Y3kdTsvfNarbW-paJ2qabDyQ7njaqY0XI,11359
10
10
  modal/_output.py,sha256=Z0nngPh2mKHMQc4MQ92YjVPc3ewOLa3I4dFBlL9nvQY,25656
11
+ modal/_partial_function.py,sha256=qE4c7vxnvLvFCnvrtS7ARnr-SMQPFPg-bDklN_S-2TM,29110
11
12
  modal/_proxy_tunnel.py,sha256=gnKyCfmVB7x2d1A6c-JDysNIP3kEFxmXzhcXhPrzPn0,1906
12
13
  modal/_pty.py,sha256=JZfPDDpzqICZqtyPI_oMJf_9w-p_lLNuzHhwhodUXio,1329
13
14
  modal/_resolver.py,sha256=D9IAdZKNqRPwgPDaB-XMKGtO8G0GwtBzG6xdgiXKdCk,6945
@@ -17,15 +18,15 @@ modal/_traceback.py,sha256=IZQzB3fVlUfMHOSyKUgw0H6qv4yHnpyq-XVCNZKfUdA,5023
17
18
  modal/_tunnel.py,sha256=zTBxBiuH1O22tS1OliAJdIsSmaZS8PlnifS_6S5z-mk,6320
18
19
  modal/_tunnel.pyi,sha256=JmmDYAy9F1FpgJ_hWx0xkom2nTOFQjn4mTPYlU3PFo4,1245
19
20
  modal/_watcher.py,sha256=K6LYnlmSGQB4tWWI9JADv-tvSvQ1j522FwT71B51CX8,3584
20
- modal/app.py,sha256=MaWCYgNx8y2GQhmaXQBMKKAAfCYfdxrdYs6zCBoJzwI,44628
21
+ modal/app.py,sha256=ILnkDzTWe4tKCDaKgh30MLMuK6zUDhvW-nCWMjuO8uI,44654
21
22
  modal/app.pyi,sha256=lxiuWzE_OLb3WHg-H7Pek9DGBuCUzZ55P594VhJL5LA,26113
22
23
  modal/call_graph.py,sha256=1g2DGcMIJvRy-xKicuf63IVE98gJSnQsr8R_NVMptNc,2581
23
24
  modal/client.py,sha256=8SQawr7P1PNUCq1UmJMUQXG2jIo4Nmdcs311XqrNLRE,15276
24
- modal/client.pyi,sha256=NNU3kdFXj6deZST1JWu_j9164F4hiVkNeNtK9ltc4DE,7593
25
+ modal/client.pyi,sha256=5VmgE3-kvEbZ_vVd91WZnDv4OVzpi5oSX5BJRtw8dOo,7593
25
26
  modal/cloud_bucket_mount.py,sha256=YOe9nnvSr4ZbeCn587d7_VhE9IioZYRvF9VYQTQux08,5914
26
27
  modal/cloud_bucket_mount.pyi,sha256=30T3K1a89l6wzmEJ_J9iWv9SknoGqaZDx59Xs-ZQcmk,1607
27
- modal/cls.py,sha256=agxclIXZbzBbgcI5PPVD7IfOiHzv-B82xaaXtw9cpv8,31126
28
- modal/cls.pyi,sha256=gb6QNwfX3HSJfcZXPY36N9ywF7aBJTwwtoARnf3G1HQ,8877
28
+ modal/cls.py,sha256=5Er9L9tGpLGIrbiHOI7c9266gPG6nhxoJ_BX8op96nU,31096
29
+ modal/cls.pyi,sha256=TaQEWPZ-zzAQJJSZem6E4KuyOeEJLignUD1anSRzwBg,8909
29
30
  modal/config.py,sha256=XT1W4Y9PVkbYMAXjJRshvQEPDhZmnfW_ZRMwl8XKoqA,11149
30
31
  modal/container_process.py,sha256=WTqLn01dJPVkPpwR_0w_JH96ceN5mV4TGtiu1ZR2RRA,6108
31
32
  modal/container_process.pyi,sha256=Hf0J5JyDdCCXBJSKx6gvkPOo0XrztCm78xzxamtzUjQ,2828
@@ -34,7 +35,7 @@ modal/dict.pyi,sha256=kKb0Kc6RUabtQ5Hwslg_vwL_OIrwIAJ2NXrJTepTtp4,7684
34
35
  modal/environments.py,sha256=Q1MpUt4SmBUaLFPzrYxCgGkMaqNBLxRDSLzNxK_Abe4,6949
35
36
  modal/environments.pyi,sha256=JvSroVOIXDIILL40Z5G4HyY16bmih2YMWMvWL-SFTwo,3373
36
37
  modal/exception.py,sha256=4JyO-SACaLNDe2QC48EjsK8GMkZ8AgEurZ8j1YdRu8E,5263
37
- modal/experimental.py,sha256=HgD7luvxjQV1wNlApu6dyvR0wK-v7G3-px3rSqwsBec,4016
38
+ modal/experimental.py,sha256=e625Ekpo2HtYkk6ZltM_XYcI9xhLxic8_7Na91PbdUg,4017
38
39
  modal/experimental.pyi,sha256=24tIYu_w9RLwFrz1cIsgYuqmDCtV8eg6-bQNz3zjhDo,939
39
40
  modal/file_io.py,sha256=lcMs_E9Xfm0YX1t9U2wNIBPnqHRxmImqjLW1GHqVmyg,20945
40
41
  modal/file_io.pyi,sha256=NTRft1tbPSWf9TlWVeZmTlgB5AZ_Zhu2srWIrWr7brk,9445
@@ -55,8 +56,8 @@ modal/object.pyi,sha256=kyJkRQcVv3ct7zSAxvvXcuhBVeH914v80uSlqeS7cA4,5632
55
56
  modal/output.py,sha256=N0xf4qeudEaYrslzdAl35VKV8rapstgIM2e9wO8_iy0,1967
56
57
  modal/parallel_map.py,sha256=POBTyiWabe2e4qBNlsjjksiu1AAPEsNqI-mM8cgNFco,16042
57
58
  modal/parallel_map.pyi,sha256=-YKY_bVuQv8B4gtFrHnXtuNV0_JpmU9vqMJzR7beeCU,2524
58
- modal/partial_function.py,sha256=vlZz1eVDoTWZWkyyG5peujEi_jvQ07U0_qmRflh0Dt8,28708
59
- modal/partial_function.pyi,sha256=UI0YJpp8sQw8vWA-S_G3qT6HVKfeiUN9kmk4H1YofkI,9895
59
+ modal/partial_function.py,sha256=KkATL56zEe_Kzp4hDX0c0e6BrXeO8V8mZBjfIs_ldqI,1016
60
+ modal/partial_function.pyi,sha256=SwjQHTqjdRqoPxIsfNFeiMt-E_IfFECPGlGZPEcDB7E,4649
60
61
  modal/proxy.py,sha256=NrOevrWxG3G7-zlyRzG6BcIvop7AWLeyahZxitbBaOk,1418
61
62
  modal/proxy.pyi,sha256=1OEKIVUyC-xb7fHMzngakQso0nTsK60TVhXtlcMj6Wk,390
62
63
  modal/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -87,7 +88,7 @@ modal/_runtime/container_io_manager.py,sha256=EHjdCky8RiM2kDpkwDhBqs2kCqC-ixc-TN
87
88
  modal/_runtime/execution_context.py,sha256=E6ofm6j1POXGPxS841X3V7JU6NheVb8OkQc7JpLq4Kg,2712
88
89
  modal/_runtime/gpu_memory_snapshot.py,sha256=tA3m1d1cwnmHpvpCeN_WijDd6n8byn7LWlpicbIxiOI,3144
89
90
  modal/_runtime/telemetry.py,sha256=T1RoAGyjBDr1swiM6pPsGRSITm7LI5FDK18oNXxY08U,5163
90
- modal/_runtime/user_code_imports.py,sha256=zl_Mq9dsrVF62x3w-iNK1YAhZKYAXeFaGpd4G7AySTc,14746
91
+ modal/_runtime/user_code_imports.py,sha256=iQCl2TtsmouuNb2Y3-Rpu8CYlqhAJWHJ7zrSdFrFyBQ,14748
91
92
  modal/_utils/__init__.py,sha256=waLjl5c6IPDhSsdWAm9Bji4e2PVxamYABKAze6CHVXY,28
92
93
  modal/_utils/app_utils.py,sha256=88BT4TPLWfYAQwKTHcyzNQRHg8n9B-QE2UyJs96iV-0,108
93
94
  modal/_utils/async_utils.py,sha256=9ubwMkwiDB4gzOYG2jL9j7Fs-5dxHjcifZe3r7JRg-k,25091
@@ -171,10 +172,10 @@ modal_proto/options_pb2_grpc.pyi,sha256=CImmhxHsYnF09iENPoe8S4J-n93jtgUYD2JPAc0y
171
172
  modal_proto/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
172
173
  modal_version/__init__.py,sha256=wiJQ53c-OMs0Xf1UeXOxQ7FwlV1VzIjnX6o-pRYZ_Pk,470
173
174
  modal_version/__main__.py,sha256=2FO0yYQQwDTh6udt1h-cBnGd1c4ZyHnHSI4BksxzVac,105
174
- modal_version/_version_generated.py,sha256=sQVJ0gz9EdPuIRQye6HG69QC52sbpNWI8nHV1qb_C7o,149
175
- modal-0.73.27.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
176
- modal-0.73.27.dist-info/METADATA,sha256=XbkIba_nQ8FVEZfNt6qn2LZA-gCZq42-zS5TtwNGrrg,2330
177
- modal-0.73.27.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
178
- modal-0.73.27.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
179
- modal-0.73.27.dist-info/top_level.txt,sha256=1nvYbOSIKcmU50fNrpnQnrrOpj269ei3LzgB6j9xGqg,64
180
- modal-0.73.27.dist-info/RECORD,,
175
+ modal_version/_version_generated.py,sha256=vx2bfyAfCd_GVBSI6V-sjHBP7rYb49mGeoKWnyqdwPo,149
176
+ modal-0.73.28.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
177
+ modal-0.73.28.dist-info/METADATA,sha256=SBFv75FYMZLdonBirLUi0SvSrSQ0uThOlhAcg2X5nMY,2330
178
+ modal-0.73.28.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
179
+ modal-0.73.28.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
180
+ modal-0.73.28.dist-info/top_level.txt,sha256=1nvYbOSIKcmU50fNrpnQnrrOpj269ei3LzgB6j9xGqg,64
181
+ modal-0.73.28.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  # Copyright Modal Labs 2025
2
2
 
3
3
  # Note: Reset this value to -1 whenever you make a minor `0.X` release of the client.
4
- build_number = 27 # git: 4e70d23
4
+ build_number = 28 # git: 6a1f081