modal 1.4.1.dev5__tar.gz → 1.4.2__tar.gz
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-1.4.1.dev5 → modal-1.4.2}/PKG-INFO +2 -2
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_functions.py +0 -3
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_runtime/asgi.py +5 -2
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/grpc_utils.py +29 -5
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/rand_pb_testing.py +3 -1
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/sandbox_fs_utils.py +72 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/task_command_router_client.py +79 -24
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/time_utils.py +55 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/app.py +2 -6
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/app.pyi +4 -8
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/app.py +171 -21
- modal-1.4.2/modal/cli/bootstrap.py +136 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/container.py +20 -2
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/entry_point.py +8 -24
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/import_refs.py +21 -2
- modal-1.4.2/modal/cli/logo.py +70 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/run.py +3 -3
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/selector.py +12 -4
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/shell.py +1 -1
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/utils.py +10 -17
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/client.py +7 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/client.pyi +14 -10
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/config.py +2 -1
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/dict.py +5 -5
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/dict.pyi +8 -8
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/exception.py +12 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/file_io.py +2 -2
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/functions.pyi +0 -1
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/image.py +2 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/image.pyi +2 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/queue.py +5 -5
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/queue.pyi +8 -8
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/sandbox.py +116 -40
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/sandbox.pyi +106 -45
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/sandbox_fs.py +91 -6
- modal-1.4.2/modal/sandbox_fs.pyi +672 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/secret.py +5 -5
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/secret.pyi +8 -8
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/volume.py +5 -5
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/volume.pyi +8 -8
- {modal-1.4.1.dev5 → modal-1.4.2}/modal.egg-info/PKG-INFO +2 -2
- {modal-1.4.1.dev5 → modal-1.4.2}/modal.egg-info/SOURCES.txt +2 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal.egg-info/requires.txt +1 -1
- {modal-1.4.1.dev5 → modal-1.4.2}/modal_docs/mdmd/mdmd.py +30 -13
- {modal-1.4.1.dev5 → modal-1.4.2}/modal_proto/api_grpc.py +128 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal_proto/api_pb2.py +1250 -1051
- {modal-1.4.1.dev5 → modal-1.4.2}/modal_proto/api_pb2.pyi +453 -5
- {modal-1.4.1.dev5 → modal-1.4.2}/modal_proto/api_pb2_grpc.py +266 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal_proto/api_pb2_grpc.pyi +84 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal_proto/modal_api_grpc.py +8 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal_proto/task_command_router_grpc.py +16 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal_proto/task_command_router_pb2.py +19 -9
- {modal-1.4.1.dev5 → modal-1.4.2}/modal_proto/task_command_router_pb2.pyi +17 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal_proto/task_command_router_pb2_grpc.py +34 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal_proto/task_command_router_pb2_grpc.pyi +12 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal_version/__init__.py +1 -1
- {modal-1.4.1.dev5 → modal-1.4.2}/pyproject.toml +5 -5
- modal-1.4.1.dev5/modal/sandbox_fs.pyi +0 -507
- {modal-1.4.1.dev5 → modal-1.4.2}/LICENSE +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/README.md +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/__init__.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/__main__.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_billing.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_clustered_functions.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_clustered_functions.pyi +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_container_entrypoint.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_grpc_client.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_ipython.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_load_context.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_location.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_logs.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_object.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_output/__init__.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_output/manager.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_output/pty.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_output/rich.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_output/status.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_partial_function.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_resolver.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_resources.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_runtime/__init__.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_runtime/execution_context.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_runtime/telemetry.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_runtime/user_code_event_loop.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_serialization.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_server.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_traceback.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_tunnel.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_tunnel.pyi +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_type_manager.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/__init__.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/app_utils.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/async_utils.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/blob_utils.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/browser_utils.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/deprecation.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/docker_utils.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/function_utils.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/git_utils.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/hash_utils.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/http_utils.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/logger.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/mount_utils.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/name_utils.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/package_utils.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_utils/shell_utils.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_vendor/__init__.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_vendor/tblib.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_vendor/version.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/_watcher.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/billing.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/builder/2023.12.txt +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/builder/2024.04.txt +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/builder/2024.10.txt +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/builder/2025.06.txt +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/builder/README.md +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/builder/base-images.json +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/call_graph.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/__init__.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/_download.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/_traceback.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/billing.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/changelog.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/cluster.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/config.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/dashboard.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/dict.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/environment.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/launch.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/network_file_system.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/profile.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/programs/__init__.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/programs/vscode.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/queues.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/secret.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/token.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/cli/volume.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/cls.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/cls.pyi +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/container_process.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/container_process.pyi +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/environments.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/environments.pyi +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/experimental/__init__.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/experimental/flash.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/experimental/flash.pyi +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/experimental/ipython.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/file_io.pyi +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/file_pattern_matcher.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/functions.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/io_streams.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/io_streams.pyi +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/mount.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/mount.pyi +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/network_file_system.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/network_file_system.pyi +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/object.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/object.pyi +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/output.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/parallel_map.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/parallel_map.pyi +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/partial_function.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/partial_function.pyi +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/proxy.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/proxy.pyi +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/py.typed +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/retries.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/runner.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/runner.pyi +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/running_app.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/schedule.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/scheduler_placement.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/server.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/server.pyi +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/serving.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/serving.pyi +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/snapshot.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/snapshot.pyi +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/stream_type.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/token_flow.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal/token_flow.pyi +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal.egg-info/top_level.txt +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal_docs/__init__.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal_docs/gen_cli_docs_main.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal_docs/gen_reference_docs_main.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal_proto/__init__.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal_proto/py.typed +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/modal_version/__main__.py +0 -0
- {modal-1.4.1.dev5 → modal-1.4.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: modal
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.2
|
|
4
4
|
Summary: Python client library for Modal
|
|
5
5
|
Author-email: Modal Labs <support@modal.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -23,7 +23,7 @@ Requires-Dist: grpclib<0.4.10,>=0.4.7; python_version < "3.14"
|
|
|
23
23
|
Requires-Dist: grpclib<0.4.10,>=0.4.9; python_version >= "3.14"
|
|
24
24
|
Requires-Dist: protobuf!=4.24.0,<7.0,>=3.19
|
|
25
25
|
Requires-Dist: rich>=12.0.0
|
|
26
|
-
Requires-Dist: synchronicity~=0.12.
|
|
26
|
+
Requires-Dist: synchronicity~=0.12.1
|
|
27
27
|
Requires-Dist: toml
|
|
28
28
|
Requires-Dist: typer>=0.9
|
|
29
29
|
Requires-Dist: types-certifi
|
|
@@ -700,7 +700,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
700
700
|
ephemeral_disk: Optional[int] = None,
|
|
701
701
|
include_source: bool = True,
|
|
702
702
|
experimental_options: Optional[dict[str, str]] = None,
|
|
703
|
-
_experimental_proxy_ip: Optional[str] = None,
|
|
704
703
|
restrict_output: bool = False,
|
|
705
704
|
http_config: Optional[api_pb2.HTTPConfig] = None,
|
|
706
705
|
) -> "_Function":
|
|
@@ -1041,7 +1040,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1041
1040
|
# ---
|
|
1042
1041
|
_experimental_group_size=cluster_size or 0, # Experimental: Clustered functions
|
|
1043
1042
|
_experimental_concurrent_cancellations=True,
|
|
1044
|
-
_experimental_proxy_ip=_experimental_proxy_ip,
|
|
1045
1043
|
# --- These are deprecated in favor of autoscaler_settings
|
|
1046
1044
|
warm_pool_size=min_containers or 0,
|
|
1047
1045
|
concurrency_limit=max_containers or 0,
|
|
@@ -1084,7 +1082,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1084
1082
|
_experimental_group_size=function_definition._experimental_group_size,
|
|
1085
1083
|
_experimental_buffer_containers=function_definition._experimental_buffer_containers,
|
|
1086
1084
|
_experimental_custom_scaling=function_definition._experimental_custom_scaling,
|
|
1087
|
-
_experimental_proxy_ip=function_definition._experimental_proxy_ip,
|
|
1088
1085
|
snapshot_debug=function_definition.snapshot_debug,
|
|
1089
1086
|
runtime_perf_record=function_definition.runtime_perf_record,
|
|
1090
1087
|
function_schema=function_schema,
|
|
@@ -474,8 +474,11 @@ async def _proxy_lifespan_request(base_url, scope, receive, send) -> None:
|
|
|
474
474
|
auto_decompress=False,
|
|
475
475
|
read_bufsize=1024 * 1024, # 1 MiB
|
|
476
476
|
connector=aiohttp.TCPConnector(
|
|
477
|
-
limit=1000
|
|
478
|
-
|
|
477
|
+
limit=1000, # 100 is the default max, but 1000 is the max for `@modal.concurrent`.
|
|
478
|
+
# Disable keep-alive connection reuse to avoid ServerDisconnectedError
|
|
479
|
+
# from stale pooled connections.
|
|
480
|
+
force_close=True,
|
|
481
|
+
),
|
|
479
482
|
# Note: these values will need to be kept in sync.
|
|
480
483
|
**(
|
|
481
484
|
# These options were introduced in aiohttp 3.9, and we can remove the
|
|
@@ -216,7 +216,11 @@ def create_channel(
|
|
|
216
216
|
for k, v in metadata.items():
|
|
217
217
|
event.metadata[k] = v
|
|
218
218
|
|
|
219
|
-
|
|
219
|
+
idempotency_key = typing.cast(Optional[str], event.metadata.get("x-idempotency-key"))
|
|
220
|
+
if idempotency_key is None:
|
|
221
|
+
logger.debug(f"Sending request to {event.method_name}")
|
|
222
|
+
else:
|
|
223
|
+
logger.debug(f"Sending request to {event.method_name} ({idempotency_key[:8]})")
|
|
220
224
|
|
|
221
225
|
grpclib.events.listen(channel, grpclib.events.SendRequest, send_request)
|
|
222
226
|
|
|
@@ -288,12 +292,14 @@ def process_exception_before_retry(
|
|
|
288
292
|
n_retries: int,
|
|
289
293
|
delay: float,
|
|
290
294
|
idempotency_key: str,
|
|
295
|
+
rpc_elapsed: float,
|
|
291
296
|
):
|
|
292
297
|
"""Process exception before retry, used by `_retry_transient_errors`."""
|
|
293
298
|
with suppress_tb_frame():
|
|
294
299
|
if final_attempt:
|
|
295
300
|
logger.debug(
|
|
296
|
-
f"Final attempt failed with {repr(exc)} {n_retries=} {delay=}
|
|
301
|
+
f"Final attempt failed with {repr(exc)} {n_retries=} {delay=} {rpc_elapsed=:0.2f}s "
|
|
302
|
+
f"for {fn_name} ({idempotency_key[:8]})"
|
|
297
303
|
)
|
|
298
304
|
if isinstance(exc, OSError):
|
|
299
305
|
raise ConnectionError(str(exc))
|
|
@@ -310,7 +316,10 @@ def process_exception_before_retry(
|
|
|
310
316
|
# we handle in the retry logic once we drop this check!
|
|
311
317
|
raise exc
|
|
312
318
|
|
|
313
|
-
logger.debug(
|
|
319
|
+
logger.debug(
|
|
320
|
+
f"Retryable failure {repr(exc)} {n_retries=} {delay=} {rpc_elapsed=:0.2f}s "
|
|
321
|
+
f"for {fn_name} ({idempotency_key[:8]})"
|
|
322
|
+
)
|
|
314
323
|
|
|
315
324
|
|
|
316
325
|
async def _retry_transient_errors(
|
|
@@ -373,6 +382,7 @@ async def _retry_transient_errors(
|
|
|
373
382
|
else:
|
|
374
383
|
timeout = None
|
|
375
384
|
|
|
385
|
+
attempt_started_at = time.monotonic()
|
|
376
386
|
try:
|
|
377
387
|
with suppress_tb_frame():
|
|
378
388
|
return await fn_callable(req, metadata=attempt_metadata, timeout=timeout)
|
|
@@ -409,7 +419,13 @@ async def _retry_transient_errors(
|
|
|
409
419
|
|
|
410
420
|
with suppress_tb_frame():
|
|
411
421
|
process_exception_before_retry(
|
|
412
|
-
exc,
|
|
422
|
+
exc,
|
|
423
|
+
final_attempt,
|
|
424
|
+
fn.name,
|
|
425
|
+
n_retries,
|
|
426
|
+
server_delay,
|
|
427
|
+
idempotency_key,
|
|
428
|
+
time.monotonic() - attempt_started_at,
|
|
413
429
|
)
|
|
414
430
|
|
|
415
431
|
now = time.time()
|
|
@@ -438,7 +454,15 @@ async def _retry_transient_errors(
|
|
|
438
454
|
final_attempt = False
|
|
439
455
|
|
|
440
456
|
with suppress_tb_frame():
|
|
441
|
-
process_exception_before_retry(
|
|
457
|
+
process_exception_before_retry(
|
|
458
|
+
exc,
|
|
459
|
+
final_attempt,
|
|
460
|
+
fn.name,
|
|
461
|
+
n_retries,
|
|
462
|
+
delay,
|
|
463
|
+
idempotency_key,
|
|
464
|
+
time.monotonic() - attempt_started_at,
|
|
465
|
+
)
|
|
442
466
|
|
|
443
467
|
n_retries += 1
|
|
444
468
|
|
|
@@ -52,8 +52,9 @@ def _fill(msg, desc: Descriptor, rand: Random) -> None:
|
|
|
52
52
|
if hasattr(field, "is_repeated"):
|
|
53
53
|
is_repeated = field.is_repeated # type: ignore
|
|
54
54
|
else:
|
|
55
|
-
is_repeated = field.label == FieldDescriptor.LABEL_REPEATED
|
|
55
|
+
is_repeated = field.label == FieldDescriptor.LABEL_REPEATED # type: ignore[attr-defined]
|
|
56
56
|
if is_message:
|
|
57
|
+
assert field.message_type is not None
|
|
57
58
|
msg_field = getattr(msg, field.name)
|
|
58
59
|
if is_repeated:
|
|
59
60
|
num = rand.randint(0, 2)
|
|
@@ -64,6 +65,7 @@ def _fill(msg, desc: Descriptor, rand: Random) -> None:
|
|
|
64
65
|
_fill(msg_field, field.message_type, rand)
|
|
65
66
|
else:
|
|
66
67
|
if field.type == FieldDescriptor.TYPE_ENUM:
|
|
68
|
+
assert field.enum_type is not None
|
|
67
69
|
enum_values = [x.number for x in field.enum_type.values]
|
|
68
70
|
generator = lambda rand: rand.choice(enum_values) # noqa: E731
|
|
69
71
|
|
|
@@ -12,11 +12,13 @@ from ..exception import (
|
|
|
12
12
|
Error as ModalError,
|
|
13
13
|
InvalidError,
|
|
14
14
|
NotFoundError,
|
|
15
|
+
SandboxFilesystemDirectoryNotEmptyError,
|
|
15
16
|
SandboxFilesystemError,
|
|
16
17
|
SandboxFilesystemFileTooLargeError,
|
|
17
18
|
SandboxFilesystemIsADirectoryError,
|
|
18
19
|
SandboxFilesystemNotADirectoryError,
|
|
19
20
|
SandboxFilesystemNotFoundError,
|
|
21
|
+
SandboxFilesystemPathAlreadyExistsError,
|
|
20
22
|
SandboxFilesystemPermissionError,
|
|
21
23
|
ServiceError,
|
|
22
24
|
)
|
|
@@ -153,6 +155,76 @@ def make_read_file_command(remote_path: str) -> str:
|
|
|
153
155
|
return json.dumps({"ReadFile": {"path": remote_path}})
|
|
154
156
|
|
|
155
157
|
|
|
158
|
+
def make_remove_command(remote_path: str, recursive: bool) -> str:
|
|
159
|
+
"""Build the JSON command string for a Remove operation.
|
|
160
|
+
|
|
161
|
+
The returned JSON must match the `Command` enum in the modal-sandbox-fs-tools
|
|
162
|
+
Rust crate (crates/modal-sandbox-fs-tools/src/lib.rs). Treat changes to
|
|
163
|
+
this schema like protobuf changes: fields must not be removed or renamed,
|
|
164
|
+
only added with backwards-compatible defaults.
|
|
165
|
+
"""
|
|
166
|
+
return json.dumps({"Remove": {"path": remote_path, "recursive": recursive}})
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def raise_remove_error(returncode: int, stderr: Union[str, bytes], remote_path: str) -> NoReturn:
|
|
170
|
+
if payload := try_parse_error_payload(stderr):
|
|
171
|
+
logger.debug(
|
|
172
|
+
f"sandbox-fs-tools remove error: path={remote_path}, "
|
|
173
|
+
f"error_kind={payload.error_kind}, message={payload.message}, detail={payload.detail}"
|
|
174
|
+
)
|
|
175
|
+
if payload.error_kind == "NotFound":
|
|
176
|
+
raise SandboxFilesystemNotFoundError(f"{payload.message}: {remote_path}")
|
|
177
|
+
if payload.error_kind == "DirectoryNotEmpty":
|
|
178
|
+
raise SandboxFilesystemDirectoryNotEmptyError(f"{payload.message}: {remote_path}")
|
|
179
|
+
if payload.error_kind == "NotSupported":
|
|
180
|
+
raise InvalidError(
|
|
181
|
+
f"{payload.message}: {remote_path} - this operation is not supported for CloudBucketMounts"
|
|
182
|
+
)
|
|
183
|
+
if payload.error_kind == "PermissionDenied":
|
|
184
|
+
raise SandboxFilesystemPermissionError(f"{payload.message}: {remote_path}")
|
|
185
|
+
raise SandboxFilesystemError(payload.message)
|
|
186
|
+
|
|
187
|
+
if stderr_text := _stderr_to_text(stderr):
|
|
188
|
+
logger.debug(f"Unstructured modal-sandbox-fs-tools stderr: {stderr_text}")
|
|
189
|
+
raise SandboxFilesystemError(f"Operation on '{remote_path}' failed with exit code {returncode}")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def make_make_directory_command(remote_path: str, create_parents: bool) -> str:
|
|
193
|
+
"""Build the JSON command string for a MakeDirectory operation.
|
|
194
|
+
|
|
195
|
+
The returned JSON must match the `Command` enum in the modal-sandbox-fs-tools
|
|
196
|
+
Rust crate (crates/modal-sandbox-fs-tools/src/lib.rs). Treat changes to
|
|
197
|
+
this schema like protobuf changes: fields must not be removed or renamed,
|
|
198
|
+
only added with backwards-compatible defaults.
|
|
199
|
+
"""
|
|
200
|
+
return json.dumps({"MakeDirectory": {"path": remote_path, "parents": create_parents}})
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def raise_make_directory_error(returncode: int, stderr: Union[str, bytes], remote_path: str) -> NoReturn:
|
|
204
|
+
if payload := try_parse_error_payload(stderr):
|
|
205
|
+
logger.debug(
|
|
206
|
+
f"sandbox-fs-tools make_directory error: path={remote_path}, "
|
|
207
|
+
f"error_kind={payload.error_kind}, message={payload.message}, detail={payload.detail}"
|
|
208
|
+
)
|
|
209
|
+
if payload.error_kind == "NotFound":
|
|
210
|
+
raise SandboxFilesystemNotFoundError(f"{payload.message}: {remote_path}")
|
|
211
|
+
if payload.error_kind == "PathAlreadyExists":
|
|
212
|
+
raise SandboxFilesystemPathAlreadyExistsError(f"{payload.message}: {remote_path}")
|
|
213
|
+
if payload.error_kind == "NotDirectory":
|
|
214
|
+
raise SandboxFilesystemNotADirectoryError(f"{payload.message}: {remote_path}")
|
|
215
|
+
if payload.error_kind == "PermissionDenied":
|
|
216
|
+
raise SandboxFilesystemPermissionError(f"{payload.message}: {remote_path}")
|
|
217
|
+
if payload.error_kind == "NotSupported":
|
|
218
|
+
raise InvalidError(
|
|
219
|
+
f"{payload.message}: {remote_path} - this operation is not supported for CloudBucketMounts"
|
|
220
|
+
)
|
|
221
|
+
raise SandboxFilesystemError(payload.message)
|
|
222
|
+
|
|
223
|
+
if stderr_text := _stderr_to_text(stderr):
|
|
224
|
+
logger.debug(f"Unstructured modal-sandbox-fs-tools stderr: {stderr_text}")
|
|
225
|
+
raise SandboxFilesystemError(f"Operation on '{remote_path}' failed with exit code {returncode}")
|
|
226
|
+
|
|
227
|
+
|
|
156
228
|
def validate_absolute_remote_path(remote_path: str, operation: str) -> None:
|
|
157
229
|
if not PurePosixPath(remote_path).is_absolute():
|
|
158
230
|
raise InvalidError(f"Sandbox.filesystem.{operation}() currently only supports absolute remote_path values")
|
|
@@ -122,6 +122,18 @@ async def fetch_command_router_access(server_client, task_id: str) -> api_pb2.Ta
|
|
|
122
122
|
)
|
|
123
123
|
|
|
124
124
|
|
|
125
|
+
async def fetch_command_router_access_v2(
|
|
126
|
+
server_client, sandbox_id: str
|
|
127
|
+
) -> api_pb2.SandboxGetCommandRouterAccessResponse:
|
|
128
|
+
"""Fetch direct command router access info from Modal server for a V2 sandbox."""
|
|
129
|
+
assert server_client._auth_token_manager
|
|
130
|
+
auth_token = await server_client._auth_token_manager.get_token()
|
|
131
|
+
return await server_client.stub.SandboxGetCommandRouterAccess(
|
|
132
|
+
api_pb2.SandboxGetCommandRouterAccessRequest(sandbox_id=sandbox_id),
|
|
133
|
+
metadata=[("x-modal-auth-token", auth_token)],
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
125
137
|
def _finalize_channel(loop, channel):
|
|
126
138
|
if not loop.is_closed():
|
|
127
139
|
# only run if loop has not shut down
|
|
@@ -139,27 +151,19 @@ class TaskCommandRouterClient:
|
|
|
139
151
|
"""
|
|
140
152
|
|
|
141
153
|
@classmethod
|
|
142
|
-
async def
|
|
154
|
+
async def _connect(
|
|
143
155
|
cls,
|
|
144
156
|
server_client,
|
|
145
157
|
task_id: str,
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
except ConflictError:
|
|
154
|
-
logger.debug(f"Command router access is not enabled for task {task_id}")
|
|
155
|
-
return None
|
|
156
|
-
|
|
157
|
-
logger.debug(f"Using command router access for task {task_id}")
|
|
158
|
-
|
|
159
|
-
# Build and connect a channel to the task command router now that we have access info.
|
|
160
|
-
o = urllib.parse.urlparse(resp.url)
|
|
158
|
+
url: str,
|
|
159
|
+
jwt: str,
|
|
160
|
+
*,
|
|
161
|
+
sandbox_id: Optional[str] = None,
|
|
162
|
+
) -> "TaskCommandRouterClient":
|
|
163
|
+
"""Build a connected client from a jwt and url."""
|
|
164
|
+
o = urllib.parse.urlparse(url)
|
|
161
165
|
if o.scheme != "https":
|
|
162
|
-
raise ValueError(f"Task router URL must be https, got: {
|
|
166
|
+
raise ValueError(f"Task router URL must be https, got: {url}")
|
|
163
167
|
|
|
164
168
|
host, _, port_str = o.netloc.partition(":")
|
|
165
169
|
port = int(port_str) if port_str else 443
|
|
@@ -189,7 +193,38 @@ class TaskCommandRouterClient:
|
|
|
189
193
|
loop = asyncio.get_running_loop()
|
|
190
194
|
jwt_refresh_lock = asyncio.Lock()
|
|
191
195
|
|
|
192
|
-
return cls(server_client, task_id,
|
|
196
|
+
return cls(server_client, task_id, url, jwt, channel, loop, jwt_refresh_lock, sandbox_id=sandbox_id)
|
|
197
|
+
|
|
198
|
+
@classmethod
|
|
199
|
+
async def try_init(
|
|
200
|
+
cls,
|
|
201
|
+
server_client,
|
|
202
|
+
task_id: str,
|
|
203
|
+
) -> Optional["TaskCommandRouterClient"]:
|
|
204
|
+
"""Attempt to initialize a TaskCommandRouterClient by fetching direct access.
|
|
205
|
+
|
|
206
|
+
Returns None if command router access is not enabled (FAILED_PRECONDITION).
|
|
207
|
+
"""
|
|
208
|
+
try:
|
|
209
|
+
resp = await fetch_command_router_access(server_client, task_id)
|
|
210
|
+
except ConflictError:
|
|
211
|
+
logger.debug(f"Command router access is not enabled for task {task_id}")
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
logger.debug(f"Using command router access for task {task_id}")
|
|
215
|
+
return await cls._connect(server_client, task_id, resp.url, resp.jwt)
|
|
216
|
+
|
|
217
|
+
@classmethod
|
|
218
|
+
async def init_v2(
|
|
219
|
+
cls,
|
|
220
|
+
server_client,
|
|
221
|
+
sandbox_id: str,
|
|
222
|
+
task_id: str,
|
|
223
|
+
) -> "TaskCommandRouterClient":
|
|
224
|
+
"""Initialize a TaskCommandRouterClient for a V2 sandbox."""
|
|
225
|
+
resp = await fetch_command_router_access_v2(server_client, sandbox_id)
|
|
226
|
+
logger.debug(f"Using command router access for sandbox {sandbox_id}")
|
|
227
|
+
return await cls._connect(server_client, task_id, resp.url, resp.jwt, sandbox_id=sandbox_id)
|
|
193
228
|
|
|
194
229
|
def __init__(
|
|
195
230
|
self,
|
|
@@ -201,6 +236,7 @@ class TaskCommandRouterClient:
|
|
|
201
236
|
loop: asyncio.AbstractEventLoop,
|
|
202
237
|
jwt_refresh_lock: asyncio.Lock,
|
|
203
238
|
*,
|
|
239
|
+
sandbox_id: Optional[str] = None,
|
|
204
240
|
stream_stdio_retry_delay_secs: float = 0.01,
|
|
205
241
|
stream_stdio_retry_delay_factor: float = 2,
|
|
206
242
|
stream_stdio_max_retries: int = 10,
|
|
@@ -213,6 +249,7 @@ class TaskCommandRouterClient:
|
|
|
213
249
|
# Attach bearer token on all requests to the worker-side router service.
|
|
214
250
|
self._server_client = server_client
|
|
215
251
|
self._task_id = task_id
|
|
252
|
+
self._sandbox_id = sandbox_id
|
|
216
253
|
self._server_url = server_url
|
|
217
254
|
self._jwt = jwt
|
|
218
255
|
self._channel = channel
|
|
@@ -237,6 +274,10 @@ class TaskCommandRouterClient:
|
|
|
237
274
|
|
|
238
275
|
self._stub = TaskCommandRouterStub(self._channel)
|
|
239
276
|
|
|
277
|
+
@property
|
|
278
|
+
def _is_v2_sandbox(self) -> bool:
|
|
279
|
+
return self._sandbox_id is not None
|
|
280
|
+
|
|
240
281
|
def _get_metadata(self):
|
|
241
282
|
return {"authorization": f"Bearer {self._jwt}"}
|
|
242
283
|
|
|
@@ -452,14 +493,22 @@ class TaskCommandRouterClient:
|
|
|
452
493
|
)
|
|
453
494
|
return
|
|
454
495
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
496
|
+
if self._is_v2_sandbox:
|
|
497
|
+
logger.debug(f"Refreshing JWT for exec with sandbox ID {self._sandbox_id}")
|
|
498
|
+
v2_resp = await fetch_command_router_access_v2(self._server_client, self._sandbox_id)
|
|
499
|
+
logger.debug(f"Finished refreshing JWT for exec with sandbox ID {self._sandbox_id}")
|
|
500
|
+
jwt, url = v2_resp.jwt, v2_resp.url
|
|
501
|
+
else:
|
|
502
|
+
logger.debug(f"Refreshing JWT for exec with task ID {self._task_id}")
|
|
503
|
+
v1_resp = await fetch_command_router_access(self._server_client, self._task_id)
|
|
504
|
+
logger.debug(f"Finished refreshing JWT for exec with task ID {self._task_id}")
|
|
505
|
+
jwt, url = v1_resp.jwt, v1_resp.url
|
|
458
506
|
|
|
459
507
|
# Ensure the server URL remains stable for the lifetime of this client.
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
self.
|
|
508
|
+
if url != self._server_url:
|
|
509
|
+
logger.warning("Task router URL changed during session")
|
|
510
|
+
self._jwt = jwt
|
|
511
|
+
self._jwt_exp = _parse_jwt_expiration(jwt)
|
|
463
512
|
|
|
464
513
|
async def _call_with_auth_retry(self, func, *args, **kwargs):
|
|
465
514
|
try:
|
|
@@ -571,6 +620,12 @@ class TaskCommandRouterClient:
|
|
|
571
620
|
lambda: self._call_with_auth_retry(self._stub.TaskMountDirectory, request)
|
|
572
621
|
)
|
|
573
622
|
|
|
623
|
+
async def unmount_image(self, request: sr_pb2.TaskUnmountDirectoryRequest):
|
|
624
|
+
with grpc_error_converter():
|
|
625
|
+
return await call_with_retries_on_transient_errors(
|
|
626
|
+
lambda: self._call_with_auth_retry(self._stub.TaskUnmountDirectory, request)
|
|
627
|
+
)
|
|
628
|
+
|
|
574
629
|
async def snapshot_directory(
|
|
575
630
|
self, request: sr_pb2.TaskSnapshotDirectoryRequest
|
|
576
631
|
) -> sr_pb2.TaskSnapshotDirectoryResponse:
|
|
@@ -164,6 +164,61 @@ def parse_date_range(s: str, tz: Optional[tzinfo] = None) -> tuple[datetime, dat
|
|
|
164
164
|
raise ValueError(f"Unrecognized range: '{s}'. Accepted values: {accepted}")
|
|
165
165
|
|
|
166
166
|
|
|
167
|
+
def relative_timestamp(dt: datetime) -> str:
|
|
168
|
+
"""Convert a tz-aware datetime to a human-readable relative time string.
|
|
169
|
+
|
|
170
|
+
Examples: "just now", "30 seconds ago", "5 minutes ago", "2 hours ago",
|
|
171
|
+
"yesterday", "3 days ago", "2 weeks ago", "3 months ago", "1 year ago".
|
|
172
|
+
|
|
173
|
+
Raises ValueError if the datetime is naive (no tzinfo).
|
|
174
|
+
"""
|
|
175
|
+
if dt.tzinfo is None:
|
|
176
|
+
raise ValueError("datetime must be timezone-aware")
|
|
177
|
+
|
|
178
|
+
now = datetime.now(timezone.utc)
|
|
179
|
+
delta = now - dt
|
|
180
|
+
total_seconds = int(delta.total_seconds())
|
|
181
|
+
|
|
182
|
+
if total_seconds < 0:
|
|
183
|
+
return "just now"
|
|
184
|
+
|
|
185
|
+
if total_seconds < 10:
|
|
186
|
+
return "just now"
|
|
187
|
+
if total_seconds < 60:
|
|
188
|
+
return f"{total_seconds} seconds ago"
|
|
189
|
+
if total_seconds < 120:
|
|
190
|
+
return "1 minute ago"
|
|
191
|
+
|
|
192
|
+
minutes = total_seconds // 60
|
|
193
|
+
if minutes < 60:
|
|
194
|
+
return f"{minutes} minutes ago"
|
|
195
|
+
if minutes < 120:
|
|
196
|
+
return "1 hour ago"
|
|
197
|
+
|
|
198
|
+
hours = minutes // 60
|
|
199
|
+
if hours < 24:
|
|
200
|
+
return f"{hours} hours ago"
|
|
201
|
+
if hours < 48:
|
|
202
|
+
return "yesterday"
|
|
203
|
+
|
|
204
|
+
days = hours // 24
|
|
205
|
+
if days < 14:
|
|
206
|
+
return f"{days} days ago"
|
|
207
|
+
|
|
208
|
+
weeks = days // 7
|
|
209
|
+
if days < 60:
|
|
210
|
+
return f"{weeks} weeks ago"
|
|
211
|
+
|
|
212
|
+
months = days // 30
|
|
213
|
+
if days < 365:
|
|
214
|
+
return f"{months} months ago"
|
|
215
|
+
|
|
216
|
+
years = days // 365
|
|
217
|
+
if years == 1:
|
|
218
|
+
return "1 year ago"
|
|
219
|
+
return f"{years} years ago"
|
|
220
|
+
|
|
221
|
+
|
|
167
222
|
def locale_tz() -> tzinfo:
|
|
168
223
|
return datetime.now().astimezone().tzinfo
|
|
169
224
|
|
|
@@ -753,7 +753,6 @@ class _App:
|
|
|
753
753
|
include_source: Optional[bool] = None,
|
|
754
754
|
experimental_options: Optional[dict[str, Any]] = None,
|
|
755
755
|
# Parameters below here are experimental. Use with caution!
|
|
756
|
-
_experimental_proxy_ip: Optional[str] = None, # IP address of proxy
|
|
757
756
|
_experimental_restrict_output: bool = False, # Don't use pickle for return values
|
|
758
757
|
# Parameters below here are deprecated. Please update your code as suggested
|
|
759
758
|
max_inputs: Optional[int] = None, # Replaced with `single_use_containers`
|
|
@@ -914,7 +913,6 @@ class _App:
|
|
|
914
913
|
rdma=rdma,
|
|
915
914
|
include_source=include_source if include_source is not None else local_state.include_source_default,
|
|
916
915
|
experimental_options={k: str(v) for k, v in (experimental_options or {}).items()},
|
|
917
|
-
_experimental_proxy_ip=_experimental_proxy_ip,
|
|
918
916
|
restrict_output=_experimental_restrict_output,
|
|
919
917
|
)
|
|
920
918
|
|
|
@@ -968,7 +966,6 @@ class _App:
|
|
|
968
966
|
include_source: Optional[bool] = None, # When `False`, don't automatically add the App source to the container.
|
|
969
967
|
experimental_options: Optional[dict[str, Any]] = None,
|
|
970
968
|
# Parameters below here are experimental. Use with caution!
|
|
971
|
-
_experimental_proxy_ip: Optional[str] = None, # IP address of proxy
|
|
972
969
|
_experimental_restrict_output: bool = False, # Don't use pickle for return values
|
|
973
970
|
# Parameters below here are deprecated. Please update your code as suggested
|
|
974
971
|
max_inputs: Optional[int] = None, # Replaced with `single_use_containers`
|
|
@@ -1115,7 +1112,6 @@ class _App:
|
|
|
1115
1112
|
rdma=rdma,
|
|
1116
1113
|
include_source=include_source if include_source is not None else local_state.include_source_default,
|
|
1117
1114
|
experimental_options={k: str(v) for k, v in (experimental_options or {}).items()},
|
|
1118
|
-
_experimental_proxy_ip=_experimental_proxy_ip,
|
|
1119
1115
|
restrict_output=_experimental_restrict_output,
|
|
1120
1116
|
)
|
|
1121
1117
|
|
|
@@ -1290,12 +1286,12 @@ class _App:
|
|
|
1290
1286
|
|
|
1291
1287
|
```python
|
|
1292
1288
|
app_a = modal.App("a")
|
|
1293
|
-
@
|
|
1289
|
+
@app_a.function()
|
|
1294
1290
|
def foo():
|
|
1295
1291
|
...
|
|
1296
1292
|
|
|
1297
1293
|
app_b = modal.App("b")
|
|
1298
|
-
@
|
|
1294
|
+
@app_b.function()
|
|
1299
1295
|
def bar():
|
|
1300
1296
|
...
|
|
1301
1297
|
|
|
@@ -497,7 +497,6 @@ class _App:
|
|
|
497
497
|
i6pn: typing.Optional[bool] = None,
|
|
498
498
|
include_source: typing.Optional[bool] = None,
|
|
499
499
|
experimental_options: typing.Optional[dict[str, typing.Any]] = None,
|
|
500
|
-
_experimental_proxy_ip: typing.Optional[str] = None,
|
|
501
500
|
_experimental_restrict_output: bool = False,
|
|
502
501
|
max_inputs: typing.Optional[int] = None,
|
|
503
502
|
) -> _FunctionDecoratorType:
|
|
@@ -545,7 +544,6 @@ class _App:
|
|
|
545
544
|
i6pn: typing.Optional[bool] = None,
|
|
546
545
|
include_source: typing.Optional[bool] = None,
|
|
547
546
|
experimental_options: typing.Optional[dict[str, typing.Any]] = None,
|
|
548
|
-
_experimental_proxy_ip: typing.Optional[str] = None,
|
|
549
547
|
_experimental_restrict_output: bool = False,
|
|
550
548
|
max_inputs: typing.Optional[int] = None,
|
|
551
549
|
) -> collections.abc.Callable[[typing.Union[CLS_T, modal._partial_function._PartialFunction]], CLS_T]:
|
|
@@ -618,12 +616,12 @@ class _App:
|
|
|
618
616
|
|
|
619
617
|
```python
|
|
620
618
|
app_a = modal.App("a")
|
|
621
|
-
@
|
|
619
|
+
@app_a.function()
|
|
622
620
|
def foo():
|
|
623
621
|
...
|
|
624
622
|
|
|
625
623
|
app_b = modal.App("b")
|
|
626
|
-
@
|
|
624
|
+
@app_b.function()
|
|
627
625
|
def bar():
|
|
628
626
|
...
|
|
629
627
|
|
|
@@ -1215,7 +1213,6 @@ class App:
|
|
|
1215
1213
|
i6pn: typing.Optional[bool] = None,
|
|
1216
1214
|
include_source: typing.Optional[bool] = None,
|
|
1217
1215
|
experimental_options: typing.Optional[dict[str, typing.Any]] = None,
|
|
1218
|
-
_experimental_proxy_ip: typing.Optional[str] = None,
|
|
1219
1216
|
_experimental_restrict_output: bool = False,
|
|
1220
1217
|
max_inputs: typing.Optional[int] = None,
|
|
1221
1218
|
) -> _FunctionDecoratorType:
|
|
@@ -1263,7 +1260,6 @@ class App:
|
|
|
1263
1260
|
i6pn: typing.Optional[bool] = None,
|
|
1264
1261
|
include_source: typing.Optional[bool] = None,
|
|
1265
1262
|
experimental_options: typing.Optional[dict[str, typing.Any]] = None,
|
|
1266
|
-
_experimental_proxy_ip: typing.Optional[str] = None,
|
|
1267
1263
|
_experimental_restrict_output: bool = False,
|
|
1268
1264
|
max_inputs: typing.Optional[int] = None,
|
|
1269
1265
|
) -> collections.abc.Callable[[typing.Union[CLS_T, modal.partial_function.PartialFunction]], CLS_T]:
|
|
@@ -1334,12 +1330,12 @@ class App:
|
|
|
1334
1330
|
|
|
1335
1331
|
```python
|
|
1336
1332
|
app_a = modal.App("a")
|
|
1337
|
-
@
|
|
1333
|
+
@app_a.function()
|
|
1338
1334
|
def foo():
|
|
1339
1335
|
...
|
|
1340
1336
|
|
|
1341
1337
|
app_b = modal.App("b")
|
|
1342
|
-
@
|
|
1338
|
+
@app_b.function()
|
|
1343
1339
|
def bar():
|
|
1344
1340
|
...
|
|
1345
1341
|
|