flyte 0.0.1b0__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.
Potentially problematic release.
This version of flyte might be problematic. Click here for more details.
- flyte/__init__.py +62 -0
- flyte/_api_commons.py +3 -0
- flyte/_bin/__init__.py +0 -0
- flyte/_bin/runtime.py +126 -0
- flyte/_build.py +25 -0
- flyte/_cache/__init__.py +12 -0
- flyte/_cache/cache.py +146 -0
- flyte/_cache/defaults.py +9 -0
- flyte/_cache/policy_function_body.py +42 -0
- flyte/_cli/__init__.py +0 -0
- flyte/_cli/_common.py +287 -0
- flyte/_cli/_create.py +42 -0
- flyte/_cli/_delete.py +23 -0
- flyte/_cli/_deploy.py +140 -0
- flyte/_cli/_get.py +235 -0
- flyte/_cli/_run.py +152 -0
- flyte/_cli/main.py +72 -0
- flyte/_code_bundle/__init__.py +8 -0
- flyte/_code_bundle/_ignore.py +113 -0
- flyte/_code_bundle/_packaging.py +187 -0
- flyte/_code_bundle/_utils.py +339 -0
- flyte/_code_bundle/bundle.py +178 -0
- flyte/_context.py +146 -0
- flyte/_datastructures.py +342 -0
- flyte/_deploy.py +202 -0
- flyte/_doc.py +29 -0
- flyte/_docstring.py +32 -0
- flyte/_environment.py +43 -0
- flyte/_group.py +31 -0
- flyte/_hash.py +23 -0
- flyte/_image.py +760 -0
- flyte/_initialize.py +634 -0
- flyte/_interface.py +84 -0
- flyte/_internal/__init__.py +3 -0
- flyte/_internal/controllers/__init__.py +115 -0
- flyte/_internal/controllers/_local_controller.py +118 -0
- flyte/_internal/controllers/_trace.py +40 -0
- flyte/_internal/controllers/pbhash.py +39 -0
- flyte/_internal/controllers/remote/__init__.py +40 -0
- flyte/_internal/controllers/remote/_action.py +141 -0
- flyte/_internal/controllers/remote/_client.py +43 -0
- flyte/_internal/controllers/remote/_controller.py +361 -0
- flyte/_internal/controllers/remote/_core.py +402 -0
- flyte/_internal/controllers/remote/_informer.py +361 -0
- flyte/_internal/controllers/remote/_service_protocol.py +50 -0
- flyte/_internal/imagebuild/__init__.py +11 -0
- flyte/_internal/imagebuild/docker_builder.py +416 -0
- flyte/_internal/imagebuild/image_builder.py +241 -0
- flyte/_internal/imagebuild/remote_builder.py +0 -0
- flyte/_internal/resolvers/__init__.py +0 -0
- flyte/_internal/resolvers/_task_module.py +54 -0
- flyte/_internal/resolvers/common.py +31 -0
- flyte/_internal/resolvers/default.py +28 -0
- flyte/_internal/runtime/__init__.py +0 -0
- flyte/_internal/runtime/convert.py +199 -0
- flyte/_internal/runtime/entrypoints.py +135 -0
- flyte/_internal/runtime/io.py +136 -0
- flyte/_internal/runtime/resources_serde.py +138 -0
- flyte/_internal/runtime/task_serde.py +210 -0
- flyte/_internal/runtime/taskrunner.py +190 -0
- flyte/_internal/runtime/types_serde.py +54 -0
- flyte/_logging.py +124 -0
- flyte/_protos/__init__.py +0 -0
- flyte/_protos/common/authorization_pb2.py +66 -0
- flyte/_protos/common/authorization_pb2.pyi +108 -0
- flyte/_protos/common/authorization_pb2_grpc.py +4 -0
- flyte/_protos/common/identifier_pb2.py +71 -0
- flyte/_protos/common/identifier_pb2.pyi +82 -0
- flyte/_protos/common/identifier_pb2_grpc.py +4 -0
- flyte/_protos/common/identity_pb2.py +48 -0
- flyte/_protos/common/identity_pb2.pyi +72 -0
- flyte/_protos/common/identity_pb2_grpc.py +4 -0
- flyte/_protos/common/list_pb2.py +36 -0
- flyte/_protos/common/list_pb2.pyi +69 -0
- flyte/_protos/common/list_pb2_grpc.py +4 -0
- flyte/_protos/common/policy_pb2.py +37 -0
- flyte/_protos/common/policy_pb2.pyi +27 -0
- flyte/_protos/common/policy_pb2_grpc.py +4 -0
- flyte/_protos/common/role_pb2.py +37 -0
- flyte/_protos/common/role_pb2.pyi +53 -0
- flyte/_protos/common/role_pb2_grpc.py +4 -0
- flyte/_protos/common/runtime_version_pb2.py +28 -0
- flyte/_protos/common/runtime_version_pb2.pyi +24 -0
- flyte/_protos/common/runtime_version_pb2_grpc.py +4 -0
- flyte/_protos/logs/dataplane/payload_pb2.py +96 -0
- flyte/_protos/logs/dataplane/payload_pb2.pyi +168 -0
- flyte/_protos/logs/dataplane/payload_pb2_grpc.py +4 -0
- flyte/_protos/secret/definition_pb2.py +49 -0
- flyte/_protos/secret/definition_pb2.pyi +93 -0
- flyte/_protos/secret/definition_pb2_grpc.py +4 -0
- flyte/_protos/secret/payload_pb2.py +62 -0
- flyte/_protos/secret/payload_pb2.pyi +94 -0
- flyte/_protos/secret/payload_pb2_grpc.py +4 -0
- flyte/_protos/secret/secret_pb2.py +38 -0
- flyte/_protos/secret/secret_pb2.pyi +6 -0
- flyte/_protos/secret/secret_pb2_grpc.py +198 -0
- flyte/_protos/secret/secret_pb2_grpc_grpc.py +198 -0
- flyte/_protos/validate/validate/validate_pb2.py +76 -0
- flyte/_protos/workflow/node_execution_service_pb2.py +26 -0
- flyte/_protos/workflow/node_execution_service_pb2.pyi +4 -0
- flyte/_protos/workflow/node_execution_service_pb2_grpc.py +32 -0
- flyte/_protos/workflow/queue_service_pb2.py +106 -0
- flyte/_protos/workflow/queue_service_pb2.pyi +141 -0
- flyte/_protos/workflow/queue_service_pb2_grpc.py +172 -0
- flyte/_protos/workflow/run_definition_pb2.py +128 -0
- flyte/_protos/workflow/run_definition_pb2.pyi +310 -0
- flyte/_protos/workflow/run_definition_pb2_grpc.py +4 -0
- flyte/_protos/workflow/run_logs_service_pb2.py +41 -0
- flyte/_protos/workflow/run_logs_service_pb2.pyi +28 -0
- flyte/_protos/workflow/run_logs_service_pb2_grpc.py +69 -0
- flyte/_protos/workflow/run_service_pb2.py +133 -0
- flyte/_protos/workflow/run_service_pb2.pyi +175 -0
- flyte/_protos/workflow/run_service_pb2_grpc.py +412 -0
- flyte/_protos/workflow/state_service_pb2.py +58 -0
- flyte/_protos/workflow/state_service_pb2.pyi +71 -0
- flyte/_protos/workflow/state_service_pb2_grpc.py +138 -0
- flyte/_protos/workflow/task_definition_pb2.py +72 -0
- flyte/_protos/workflow/task_definition_pb2.pyi +65 -0
- flyte/_protos/workflow/task_definition_pb2_grpc.py +4 -0
- flyte/_protos/workflow/task_service_pb2.py +44 -0
- flyte/_protos/workflow/task_service_pb2.pyi +31 -0
- flyte/_protos/workflow/task_service_pb2_grpc.py +104 -0
- flyte/_resources.py +226 -0
- flyte/_retry.py +32 -0
- flyte/_reusable_environment.py +25 -0
- flyte/_run.py +411 -0
- flyte/_secret.py +61 -0
- flyte/_task.py +367 -0
- flyte/_task_environment.py +200 -0
- flyte/_timeout.py +47 -0
- flyte/_tools.py +27 -0
- flyte/_trace.py +128 -0
- flyte/_utils/__init__.py +20 -0
- flyte/_utils/asyn.py +119 -0
- flyte/_utils/coro_management.py +25 -0
- flyte/_utils/file_handling.py +72 -0
- flyte/_utils/helpers.py +108 -0
- flyte/_utils/lazy_module.py +54 -0
- flyte/_utils/uv_script_parser.py +49 -0
- flyte/_version.py +21 -0
- flyte/connectors/__init__.py +0 -0
- flyte/errors.py +143 -0
- flyte/extras/__init__.py +5 -0
- flyte/extras/_container.py +273 -0
- flyte/io/__init__.py +11 -0
- flyte/io/_dataframe.py +0 -0
- flyte/io/_dir.py +448 -0
- flyte/io/_file.py +468 -0
- flyte/io/pickle/__init__.py +0 -0
- flyte/io/pickle/transformer.py +117 -0
- flyte/io/structured_dataset/__init__.py +129 -0
- flyte/io/structured_dataset/basic_dfs.py +219 -0
- flyte/io/structured_dataset/structured_dataset.py +1061 -0
- flyte/py.typed +0 -0
- flyte/remote/__init__.py +25 -0
- flyte/remote/_client/__init__.py +0 -0
- flyte/remote/_client/_protocols.py +131 -0
- flyte/remote/_client/auth/__init__.py +12 -0
- flyte/remote/_client/auth/_authenticators/__init__.py +0 -0
- flyte/remote/_client/auth/_authenticators/base.py +397 -0
- flyte/remote/_client/auth/_authenticators/client_credentials.py +73 -0
- flyte/remote/_client/auth/_authenticators/device_code.py +118 -0
- flyte/remote/_client/auth/_authenticators/external_command.py +79 -0
- flyte/remote/_client/auth/_authenticators/factory.py +200 -0
- flyte/remote/_client/auth/_authenticators/pkce.py +516 -0
- flyte/remote/_client/auth/_channel.py +184 -0
- flyte/remote/_client/auth/_client_config.py +83 -0
- flyte/remote/_client/auth/_default_html.py +32 -0
- flyte/remote/_client/auth/_grpc_utils/__init__.py +0 -0
- flyte/remote/_client/auth/_grpc_utils/auth_interceptor.py +288 -0
- flyte/remote/_client/auth/_grpc_utils/default_metadata_interceptor.py +151 -0
- flyte/remote/_client/auth/_keyring.py +143 -0
- flyte/remote/_client/auth/_token_client.py +260 -0
- flyte/remote/_client/auth/errors.py +16 -0
- flyte/remote/_client/controlplane.py +95 -0
- flyte/remote/_console.py +18 -0
- flyte/remote/_data.py +155 -0
- flyte/remote/_logs.py +116 -0
- flyte/remote/_project.py +86 -0
- flyte/remote/_run.py +873 -0
- flyte/remote/_secret.py +132 -0
- flyte/remote/_task.py +227 -0
- flyte/report/__init__.py +3 -0
- flyte/report/_report.py +178 -0
- flyte/report/_template.html +124 -0
- flyte/storage/__init__.py +24 -0
- flyte/storage/_remote_fs.py +34 -0
- flyte/storage/_storage.py +251 -0
- flyte/storage/_utils.py +5 -0
- flyte/types/__init__.py +13 -0
- flyte/types/_interface.py +25 -0
- flyte/types/_renderer.py +162 -0
- flyte/types/_string_literals.py +120 -0
- flyte/types/_type_engine.py +2210 -0
- flyte/types/_utils.py +80 -0
- flyte-0.0.1b0.dist-info/METADATA +179 -0
- flyte-0.0.1b0.dist-info/RECORD +390 -0
- flyte-0.0.1b0.dist-info/WHEEL +5 -0
- flyte-0.0.1b0.dist-info/entry_points.txt +3 -0
- flyte-0.0.1b0.dist-info/top_level.txt +1 -0
- union/__init__.py +54 -0
- union/_api_commons.py +3 -0
- union/_bin/__init__.py +0 -0
- union/_bin/runtime.py +113 -0
- union/_build.py +25 -0
- union/_cache/__init__.py +12 -0
- union/_cache/cache.py +141 -0
- union/_cache/defaults.py +9 -0
- union/_cache/policy_function_body.py +42 -0
- union/_cli/__init__.py +0 -0
- union/_cli/_common.py +263 -0
- union/_cli/_create.py +40 -0
- union/_cli/_delete.py +23 -0
- union/_cli/_deploy.py +120 -0
- union/_cli/_get.py +162 -0
- union/_cli/_params.py +579 -0
- union/_cli/_run.py +150 -0
- union/_cli/main.py +72 -0
- union/_code_bundle/__init__.py +8 -0
- union/_code_bundle/_ignore.py +113 -0
- union/_code_bundle/_packaging.py +187 -0
- union/_code_bundle/_utils.py +342 -0
- union/_code_bundle/bundle.py +176 -0
- union/_context.py +146 -0
- union/_datastructures.py +295 -0
- union/_deploy.py +185 -0
- union/_doc.py +29 -0
- union/_docstring.py +26 -0
- union/_environment.py +43 -0
- union/_group.py +31 -0
- union/_hash.py +23 -0
- union/_image.py +760 -0
- union/_initialize.py +585 -0
- union/_interface.py +84 -0
- union/_internal/__init__.py +3 -0
- union/_internal/controllers/__init__.py +77 -0
- union/_internal/controllers/_local_controller.py +77 -0
- union/_internal/controllers/pbhash.py +39 -0
- union/_internal/controllers/remote/__init__.py +40 -0
- union/_internal/controllers/remote/_action.py +131 -0
- union/_internal/controllers/remote/_client.py +43 -0
- union/_internal/controllers/remote/_controller.py +169 -0
- union/_internal/controllers/remote/_core.py +341 -0
- union/_internal/controllers/remote/_informer.py +260 -0
- union/_internal/controllers/remote/_service_protocol.py +44 -0
- union/_internal/imagebuild/__init__.py +11 -0
- union/_internal/imagebuild/docker_builder.py +416 -0
- union/_internal/imagebuild/image_builder.py +243 -0
- union/_internal/imagebuild/remote_builder.py +0 -0
- union/_internal/resolvers/__init__.py +0 -0
- union/_internal/resolvers/_task_module.py +31 -0
- union/_internal/resolvers/common.py +24 -0
- union/_internal/resolvers/default.py +27 -0
- union/_internal/runtime/__init__.py +0 -0
- union/_internal/runtime/convert.py +163 -0
- union/_internal/runtime/entrypoints.py +121 -0
- union/_internal/runtime/io.py +136 -0
- union/_internal/runtime/resources_serde.py +134 -0
- union/_internal/runtime/task_serde.py +202 -0
- union/_internal/runtime/taskrunner.py +179 -0
- union/_internal/runtime/types_serde.py +53 -0
- union/_logging.py +124 -0
- union/_protos/__init__.py +0 -0
- union/_protos/common/authorization_pb2.py +66 -0
- union/_protos/common/authorization_pb2.pyi +106 -0
- union/_protos/common/authorization_pb2_grpc.py +4 -0
- union/_protos/common/identifier_pb2.py +71 -0
- union/_protos/common/identifier_pb2.pyi +82 -0
- union/_protos/common/identifier_pb2_grpc.py +4 -0
- union/_protos/common/identity_pb2.py +48 -0
- union/_protos/common/identity_pb2.pyi +72 -0
- union/_protos/common/identity_pb2_grpc.py +4 -0
- union/_protos/common/list_pb2.py +36 -0
- union/_protos/common/list_pb2.pyi +69 -0
- union/_protos/common/list_pb2_grpc.py +4 -0
- union/_protos/common/policy_pb2.py +37 -0
- union/_protos/common/policy_pb2.pyi +27 -0
- union/_protos/common/policy_pb2_grpc.py +4 -0
- union/_protos/common/role_pb2.py +37 -0
- union/_protos/common/role_pb2.pyi +51 -0
- union/_protos/common/role_pb2_grpc.py +4 -0
- union/_protos/common/runtime_version_pb2.py +28 -0
- union/_protos/common/runtime_version_pb2.pyi +24 -0
- union/_protos/common/runtime_version_pb2_grpc.py +4 -0
- union/_protos/logs/dataplane/payload_pb2.py +96 -0
- union/_protos/logs/dataplane/payload_pb2.pyi +168 -0
- union/_protos/logs/dataplane/payload_pb2_grpc.py +4 -0
- union/_protos/secret/definition_pb2.py +49 -0
- union/_protos/secret/definition_pb2.pyi +93 -0
- union/_protos/secret/definition_pb2_grpc.py +4 -0
- union/_protos/secret/payload_pb2.py +62 -0
- union/_protos/secret/payload_pb2.pyi +94 -0
- union/_protos/secret/payload_pb2_grpc.py +4 -0
- union/_protos/secret/secret_pb2.py +38 -0
- union/_protos/secret/secret_pb2.pyi +6 -0
- union/_protos/secret/secret_pb2_grpc.py +198 -0
- union/_protos/validate/validate/validate_pb2.py +76 -0
- union/_protos/workflow/node_execution_service_pb2.py +26 -0
- union/_protos/workflow/node_execution_service_pb2.pyi +4 -0
- union/_protos/workflow/node_execution_service_pb2_grpc.py +32 -0
- union/_protos/workflow/queue_service_pb2.py +75 -0
- union/_protos/workflow/queue_service_pb2.pyi +103 -0
- union/_protos/workflow/queue_service_pb2_grpc.py +172 -0
- union/_protos/workflow/run_definition_pb2.py +100 -0
- union/_protos/workflow/run_definition_pb2.pyi +256 -0
- union/_protos/workflow/run_definition_pb2_grpc.py +4 -0
- union/_protos/workflow/run_logs_service_pb2.py +41 -0
- union/_protos/workflow/run_logs_service_pb2.pyi +28 -0
- union/_protos/workflow/run_logs_service_pb2_grpc.py +69 -0
- union/_protos/workflow/run_service_pb2.py +133 -0
- union/_protos/workflow/run_service_pb2.pyi +173 -0
- union/_protos/workflow/run_service_pb2_grpc.py +412 -0
- union/_protos/workflow/state_service_pb2.py +58 -0
- union/_protos/workflow/state_service_pb2.pyi +69 -0
- union/_protos/workflow/state_service_pb2_grpc.py +138 -0
- union/_protos/workflow/task_definition_pb2.py +72 -0
- union/_protos/workflow/task_definition_pb2.pyi +65 -0
- union/_protos/workflow/task_definition_pb2_grpc.py +4 -0
- union/_protos/workflow/task_service_pb2.py +44 -0
- union/_protos/workflow/task_service_pb2.pyi +31 -0
- union/_protos/workflow/task_service_pb2_grpc.py +104 -0
- union/_resources.py +226 -0
- union/_retry.py +32 -0
- union/_reusable_environment.py +25 -0
- union/_run.py +374 -0
- union/_secret.py +61 -0
- union/_task.py +354 -0
- union/_task_environment.py +186 -0
- union/_timeout.py +47 -0
- union/_tools.py +27 -0
- union/_utils/__init__.py +11 -0
- union/_utils/asyn.py +119 -0
- union/_utils/file_handling.py +71 -0
- union/_utils/helpers.py +46 -0
- union/_utils/lazy_module.py +54 -0
- union/_utils/uv_script_parser.py +49 -0
- union/_version.py +21 -0
- union/connectors/__init__.py +0 -0
- union/errors.py +128 -0
- union/extras/__init__.py +5 -0
- union/extras/_container.py +263 -0
- union/io/__init__.py +11 -0
- union/io/_dataframe.py +0 -0
- union/io/_dir.py +425 -0
- union/io/_file.py +418 -0
- union/io/pickle/__init__.py +0 -0
- union/io/pickle/transformer.py +117 -0
- union/io/structured_dataset/__init__.py +122 -0
- union/io/structured_dataset/basic_dfs.py +219 -0
- union/io/structured_dataset/structured_dataset.py +1057 -0
- union/py.typed +0 -0
- union/remote/__init__.py +23 -0
- union/remote/_client/__init__.py +0 -0
- union/remote/_client/_protocols.py +129 -0
- union/remote/_client/auth/__init__.py +12 -0
- union/remote/_client/auth/_authenticators/__init__.py +0 -0
- union/remote/_client/auth/_authenticators/base.py +391 -0
- union/remote/_client/auth/_authenticators/client_credentials.py +73 -0
- union/remote/_client/auth/_authenticators/device_code.py +120 -0
- union/remote/_client/auth/_authenticators/external_command.py +77 -0
- union/remote/_client/auth/_authenticators/factory.py +200 -0
- union/remote/_client/auth/_authenticators/pkce.py +515 -0
- union/remote/_client/auth/_channel.py +184 -0
- union/remote/_client/auth/_client_config.py +83 -0
- union/remote/_client/auth/_default_html.py +32 -0
- union/remote/_client/auth/_grpc_utils/__init__.py +0 -0
- union/remote/_client/auth/_grpc_utils/auth_interceptor.py +204 -0
- union/remote/_client/auth/_grpc_utils/default_metadata_interceptor.py +144 -0
- union/remote/_client/auth/_keyring.py +154 -0
- union/remote/_client/auth/_token_client.py +258 -0
- union/remote/_client/auth/errors.py +16 -0
- union/remote/_client/controlplane.py +86 -0
- union/remote/_data.py +149 -0
- union/remote/_logs.py +74 -0
- union/remote/_project.py +86 -0
- union/remote/_run.py +820 -0
- union/remote/_secret.py +132 -0
- union/remote/_task.py +193 -0
- union/report/__init__.py +3 -0
- union/report/_report.py +178 -0
- union/report/_template.html +124 -0
- union/storage/__init__.py +24 -0
- union/storage/_remote_fs.py +34 -0
- union/storage/_storage.py +247 -0
- union/storage/_utils.py +5 -0
- union/types/__init__.py +11 -0
- union/types/_renderer.py +162 -0
- union/types/_string_literals.py +120 -0
- union/types/_type_engine.py +2131 -0
- union/types/_utils.py +80 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import gzip
|
|
4
|
+
import hashlib
|
|
5
|
+
import os
|
|
6
|
+
import pathlib
|
|
7
|
+
import posixpath
|
|
8
|
+
import shutil
|
|
9
|
+
import stat
|
|
10
|
+
import subprocess
|
|
11
|
+
import tarfile
|
|
12
|
+
import time
|
|
13
|
+
import typing
|
|
14
|
+
from typing import List, Optional, Tuple, Union
|
|
15
|
+
|
|
16
|
+
import click
|
|
17
|
+
from rich import print as rich_print
|
|
18
|
+
from rich.tree import Tree
|
|
19
|
+
|
|
20
|
+
from flyte._logging import logger
|
|
21
|
+
|
|
22
|
+
from ._ignore import Ignore, IgnoreGroup
|
|
23
|
+
from ._utils import CopyFiles, _filehash_update, _pathhash_update, ls_files, tar_strip_file_attributes
|
|
24
|
+
|
|
25
|
+
FAST_PREFIX = "fast"
|
|
26
|
+
FAST_FILEENDING = ".tar.gz"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def print_ls_tree(source: os.PathLike, ls: typing.List[str]):
|
|
30
|
+
click.secho("Files to be copied for fast registration...", fg="bright_blue")
|
|
31
|
+
|
|
32
|
+
tree_root = Tree(
|
|
33
|
+
f":open_file_folder: [link file://{source}]{source} (detected source root)",
|
|
34
|
+
guide_style="bold bright_blue",
|
|
35
|
+
)
|
|
36
|
+
trees = {pathlib.Path(source): tree_root}
|
|
37
|
+
|
|
38
|
+
for f in ls:
|
|
39
|
+
fpp = pathlib.Path(f)
|
|
40
|
+
if fpp.parent not in trees:
|
|
41
|
+
# add trees for all intermediate folders
|
|
42
|
+
current = tree_root
|
|
43
|
+
current_path = pathlib.Path(source)
|
|
44
|
+
for subdir in fpp.parent.relative_to(source).parts:
|
|
45
|
+
current_path = current_path / subdir
|
|
46
|
+
if current_path not in trees:
|
|
47
|
+
current = current.add(f"{subdir}", guide_style="bold bright_blue")
|
|
48
|
+
trees[current_path] = current
|
|
49
|
+
else:
|
|
50
|
+
current = trees[current_path]
|
|
51
|
+
trees[fpp.parent].add(f"{fpp.name}", guide_style="bold bright_blue")
|
|
52
|
+
rich_print(tree_root)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _compress_tarball(source: pathlib.Path, output: pathlib.Path) -> None:
|
|
56
|
+
"""Compress code tarball using pigz if available, otherwise gzip"""
|
|
57
|
+
if pigz := shutil.which("pigz"):
|
|
58
|
+
with open(str(output), "wb") as gzipped:
|
|
59
|
+
subprocess.run([pigz, "--no-time", "-c", str(source)], stdout=gzipped, check=True)
|
|
60
|
+
else:
|
|
61
|
+
start_time = time.time()
|
|
62
|
+
with gzip.GzipFile(filename=str(output), mode="wb", mtime=0) as gzipped:
|
|
63
|
+
with open(source, "rb") as source_file:
|
|
64
|
+
gzipped.write(source_file.read())
|
|
65
|
+
|
|
66
|
+
end_time = time.time()
|
|
67
|
+
warning_time = 10
|
|
68
|
+
if end_time - start_time > warning_time:
|
|
69
|
+
click.secho(
|
|
70
|
+
f"Code tarball compression took {end_time - start_time:.0f} seconds. "
|
|
71
|
+
f"Consider installing `pigz` for faster compression.",
|
|
72
|
+
fg="yellow",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def list_files_to_bundle(
|
|
77
|
+
source: pathlib.Path,
|
|
78
|
+
deref_symlinks: bool = False,
|
|
79
|
+
*ignores: typing.Type[Ignore],
|
|
80
|
+
copy_style: CopyFiles = "all",
|
|
81
|
+
) -> typing.Tuple[List[str], str]:
|
|
82
|
+
"""
|
|
83
|
+
Takes a source directory and returns a list of all files to be included in the code bundle and a hexdigest of the
|
|
84
|
+
included files.
|
|
85
|
+
:param source: The source directory to package
|
|
86
|
+
:param deref_symlinks: Whether to dereference symlinks or not
|
|
87
|
+
:param ignores: A list of Ignore classes to use for ignoring files
|
|
88
|
+
:param copy_style: The copy style to use for the tarball
|
|
89
|
+
:return: A list of all files to be included in the code bundle and a hexdigest of the included files
|
|
90
|
+
"""
|
|
91
|
+
ignore = IgnoreGroup(source, *ignores)
|
|
92
|
+
|
|
93
|
+
ls, ls_digest = ls_files(source, copy_style, deref_symlinks, ignore)
|
|
94
|
+
logger.debug(f"Hash digest: {ls_digest}")
|
|
95
|
+
return ls, ls_digest
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def create_bundle(
|
|
99
|
+
source: pathlib.Path, output_dir: pathlib.Path, ls: List[str], ls_digest: str, deref_symlinks: bool = False
|
|
100
|
+
) -> Tuple[pathlib.Path, float, float]:
|
|
101
|
+
"""
|
|
102
|
+
Takes a source directory and packages everything not covered by common ignores into a tarball.
|
|
103
|
+
The output_dir is the directory where the tarball and a compressed version of the tarball will be written.
|
|
104
|
+
The output_dir can be a temporary directory.
|
|
105
|
+
|
|
106
|
+
:param source: The source directory to package
|
|
107
|
+
:param output_dir: The directory to write the tarball to
|
|
108
|
+
:param deref_symlinks: Whether to dereference symlinks or not
|
|
109
|
+
:param ls: The list of files to include in the tarball
|
|
110
|
+
:param ls_digest: The hexdigest of the included files
|
|
111
|
+
:return: The path to the tarball, the size of the tarball in MB, and the size of the compressed tarball in MB
|
|
112
|
+
"""
|
|
113
|
+
# Compute where the archive should be written
|
|
114
|
+
archive_fname = output_dir / f"{FAST_PREFIX}{ls_digest}{FAST_FILEENDING}"
|
|
115
|
+
tar_path = output_dir / "tmp.tar"
|
|
116
|
+
with tarfile.open(str(tar_path), "w", dereference=deref_symlinks) as tar:
|
|
117
|
+
for ws_file in ls:
|
|
118
|
+
rel_path = os.path.relpath(ws_file, start=source)
|
|
119
|
+
tar.add(
|
|
120
|
+
os.path.join(source, ws_file),
|
|
121
|
+
recursive=False,
|
|
122
|
+
arcname=rel_path,
|
|
123
|
+
filter=lambda x: tar_strip_file_attributes(x),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
size_mbs = tar_path.stat().st_size / 1024 / 1024
|
|
127
|
+
_compress_tarball(tar_path, archive_fname)
|
|
128
|
+
asize_mbs = archive_fname.stat().st_size / 1024 / 1024
|
|
129
|
+
|
|
130
|
+
return archive_fname, size_mbs, asize_mbs
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def compute_digest(source: Union[os.PathLike, List[os.PathLike]], filter: Optional[typing.Callable] = None) -> str:
|
|
134
|
+
"""
|
|
135
|
+
Walks the entirety of the source dir to compute a deterministic md5 hex digest of the dir contents.
|
|
136
|
+
:param os.PathLike source:
|
|
137
|
+
:param callable filter:
|
|
138
|
+
:return Text:
|
|
139
|
+
"""
|
|
140
|
+
hasher = hashlib.md5()
|
|
141
|
+
|
|
142
|
+
def compute_digest_for_file(path: os.PathLike, rel_path: os.PathLike) -> None:
|
|
143
|
+
# Only consider files that exist (e.g. disregard symlinks that point to non-existent files)
|
|
144
|
+
if not os.path.exists(path):
|
|
145
|
+
logger.info(f"Skipping non-existent file {path}")
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
# Skip socket files
|
|
149
|
+
if stat.S_ISSOCK(os.stat(path).st_mode):
|
|
150
|
+
logger.info(f"Skip socket file {path}")
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
if filter:
|
|
154
|
+
if filter(rel_path):
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
_filehash_update(path, hasher)
|
|
158
|
+
_pathhash_update(rel_path, hasher)
|
|
159
|
+
|
|
160
|
+
def compute_digest_for_dir(source: os.PathLike) -> None:
|
|
161
|
+
for root, _, files in os.walk(str(source), topdown=True):
|
|
162
|
+
files.sort()
|
|
163
|
+
|
|
164
|
+
for fname in files:
|
|
165
|
+
abspath = os.path.join(root, fname)
|
|
166
|
+
relpath = os.path.relpath(abspath, source)
|
|
167
|
+
compute_digest_for_file(pathlib.Path(abspath), pathlib.Path(relpath))
|
|
168
|
+
|
|
169
|
+
if isinstance(source, list):
|
|
170
|
+
for src in source:
|
|
171
|
+
if os.path.isdir(src):
|
|
172
|
+
compute_digest_for_dir(src)
|
|
173
|
+
else:
|
|
174
|
+
compute_digest_for_file(src, os.path.basename(src))
|
|
175
|
+
else:
|
|
176
|
+
compute_digest_for_dir(source)
|
|
177
|
+
|
|
178
|
+
return hasher.hexdigest()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def get_additional_distribution_loc(remote_location: str, identifier: str) -> str:
|
|
182
|
+
"""
|
|
183
|
+
:param Text remote_location:
|
|
184
|
+
:param Text identifier:
|
|
185
|
+
:return Text:
|
|
186
|
+
"""
|
|
187
|
+
return posixpath.join(remote_location, "{}.{}".format(identifier, "tar.gz"))
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import gzip
|
|
4
|
+
import hashlib
|
|
5
|
+
import importlib.util
|
|
6
|
+
import os
|
|
7
|
+
import pathlib
|
|
8
|
+
import shutil
|
|
9
|
+
import site
|
|
10
|
+
import stat
|
|
11
|
+
import sys
|
|
12
|
+
import tarfile
|
|
13
|
+
import tempfile
|
|
14
|
+
import typing
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
|
+
from functools import lru_cache
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from types import ModuleType
|
|
19
|
+
from typing import List, Literal, Optional, Tuple, Union
|
|
20
|
+
|
|
21
|
+
from flyte._logging import logger
|
|
22
|
+
|
|
23
|
+
from ._ignore import IgnoreGroup
|
|
24
|
+
|
|
25
|
+
CopyFiles = Literal["loaded_modules", "all", "none"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def compress_scripts(source_path: str, destination: str, modules: List[ModuleType]):
|
|
29
|
+
"""
|
|
30
|
+
Compresses the single script while maintaining the folder structure for that file.
|
|
31
|
+
|
|
32
|
+
For example, given the follow file structure:
|
|
33
|
+
.
|
|
34
|
+
├── flyte
|
|
35
|
+
├── __init__.py
|
|
36
|
+
└── workflows
|
|
37
|
+
├── example.py
|
|
38
|
+
├── another_example.py
|
|
39
|
+
├── yet_another_example.py
|
|
40
|
+
├── unused_example.py
|
|
41
|
+
└── __init__.py
|
|
42
|
+
|
|
43
|
+
Let's say you want to compress `example.py` imports `another_example.py`. And `another_example.py`
|
|
44
|
+
imports on `yet_another_example.py`. This will produce a tar file that contains only that
|
|
45
|
+
file alongside with the folder structure, i.e.:
|
|
46
|
+
|
|
47
|
+
.
|
|
48
|
+
├── flyte
|
|
49
|
+
├── __init__.py
|
|
50
|
+
└── workflows
|
|
51
|
+
├── example.py
|
|
52
|
+
├── another_example.py
|
|
53
|
+
├── yet_another_example.py
|
|
54
|
+
└── __init__.py
|
|
55
|
+
|
|
56
|
+
"""
|
|
57
|
+
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
58
|
+
destination_path = os.path.join(tmp_dir, "code")
|
|
59
|
+
os.mkdir(destination_path)
|
|
60
|
+
add_imported_modules_from_source(source_path, destination_path, modules)
|
|
61
|
+
|
|
62
|
+
tar_path = os.path.join(tmp_dir, "tmp.tar")
|
|
63
|
+
with tarfile.open(tar_path, "w") as tar:
|
|
64
|
+
tmp_path: str = os.path.join(tmp_dir, "code")
|
|
65
|
+
files: typing.List[str] = os.listdir(tmp_path)
|
|
66
|
+
for ws_file in files:
|
|
67
|
+
tar.add(os.path.join(tmp_path, ws_file), arcname=ws_file, filter=tar_strip_file_attributes)
|
|
68
|
+
with gzip.GzipFile(filename=destination, mode="wb", mtime=0) as gzipped:
|
|
69
|
+
with open(tar_path, "rb") as tar_file:
|
|
70
|
+
gzipped.write(tar_file.read())
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# Takes in a TarInfo and returns the modified TarInfo:
|
|
74
|
+
# https://docs.python.org/3/library/tarfile.html#tarinfo-objects
|
|
75
|
+
# intended to be passed as a filter to tarfile.add
|
|
76
|
+
# https://docs.python.org/3/library/tarfile.html#tarfile.TarFile.add
|
|
77
|
+
def tar_strip_file_attributes(tar_info: tarfile.TarInfo) -> tarfile.TarInfo:
|
|
78
|
+
# set time to epoch timestamp 0, aka 00:00:00 UTC on 1 January 1980
|
|
79
|
+
# note that when extracting this tarfile, this time will be shown as the modified date
|
|
80
|
+
tar_info.mtime = datetime(1980, 1, 1, tzinfo=timezone.utc).timestamp()
|
|
81
|
+
|
|
82
|
+
# user/group info
|
|
83
|
+
tar_info.uid = 0
|
|
84
|
+
tar_info.uname = ""
|
|
85
|
+
tar_info.gid = 0
|
|
86
|
+
tar_info.gname = ""
|
|
87
|
+
|
|
88
|
+
# stripping paxheaders may not be required
|
|
89
|
+
# see https://stackoverflow.com/questions/34688392/paxheaders-in-tarball
|
|
90
|
+
tar_info.pax_headers = {}
|
|
91
|
+
|
|
92
|
+
return tar_info
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def ls_files(
|
|
96
|
+
source_path: pathlib.Path,
|
|
97
|
+
copy_file_detection: CopyFiles,
|
|
98
|
+
deref_symlinks: bool = False,
|
|
99
|
+
ignore_group: Optional[IgnoreGroup] = None,
|
|
100
|
+
) -> Tuple[List[str], str]:
|
|
101
|
+
"""
|
|
102
|
+
user_modules_and_packages is a list of the Python modules and packages, expressed as absolute paths, that the
|
|
103
|
+
user has run this command with. For flyte run for instance, this is just a list of one.
|
|
104
|
+
This is used for two reasons.
|
|
105
|
+
- Everything in this list needs to be returned. Files are returned and folders are walked.
|
|
106
|
+
- A common source path is derived from this is, which is just the common folder that contains everything in the
|
|
107
|
+
list. For ex. if you do
|
|
108
|
+
$ pyflyte --pkgs a.b,a.c package
|
|
109
|
+
Then the common root is just the folder a/. The modules list is filtered against this root. Only files
|
|
110
|
+
representing modules under this root are included
|
|
111
|
+
|
|
112
|
+
If the copy enum is set to loaded_modules, then the loaded sys modules will be used.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
# Unlike the below, the value error here is useful and should be returned to the user, like if absolute and
|
|
116
|
+
# relative paths are mixed.
|
|
117
|
+
|
|
118
|
+
# This is --copy auto
|
|
119
|
+
if copy_file_detection == "loaded_modules":
|
|
120
|
+
sys_modules = list(sys.modules.values())
|
|
121
|
+
all_files = list_imported_modules_as_files(str(source_path), sys_modules)
|
|
122
|
+
# this is --copy all (--copy none should never invoke this function)
|
|
123
|
+
else:
|
|
124
|
+
all_files = list_all_files(source_path, deref_symlinks, ignore_group)
|
|
125
|
+
|
|
126
|
+
all_files.sort()
|
|
127
|
+
hasher = hashlib.md5()
|
|
128
|
+
for abspath in all_files:
|
|
129
|
+
relpath = os.path.relpath(abspath, source_path)
|
|
130
|
+
_filehash_update(abspath, hasher)
|
|
131
|
+
_pathhash_update(relpath, hasher)
|
|
132
|
+
|
|
133
|
+
digest = hasher.hexdigest()
|
|
134
|
+
|
|
135
|
+
return all_files, digest
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _filehash_update(path: Union[os.PathLike, str], hasher: hashlib._Hash) -> None:
|
|
139
|
+
blocksize = 65536
|
|
140
|
+
with open(path, "rb") as f:
|
|
141
|
+
bytes = f.read(blocksize)
|
|
142
|
+
while bytes:
|
|
143
|
+
hasher.update(bytes)
|
|
144
|
+
bytes = f.read(blocksize)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _pathhash_update(path: Union[os.PathLike, str], hasher: hashlib._Hash) -> None:
|
|
148
|
+
path_list = str(path).split(os.sep)
|
|
149
|
+
hasher.update("".join(path_list).encode("utf-8"))
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
EXCLUDE_DIRS = {".git"}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def list_all_files(source_path: pathlib.Path, deref_symlinks, ignore_group: Optional[IgnoreGroup] = None) -> List[str]:
|
|
156
|
+
all_files = []
|
|
157
|
+
|
|
158
|
+
# This is needed to prevent infinite recursion when walking with followlinks
|
|
159
|
+
visited_inodes = set()
|
|
160
|
+
for root, dirnames, files in source_path.walk(top_down=True, follow_symlinks=deref_symlinks):
|
|
161
|
+
dirnames[:] = [d for d in dirnames if d not in EXCLUDE_DIRS]
|
|
162
|
+
if deref_symlinks:
|
|
163
|
+
inode = os.stat(root).st_ino
|
|
164
|
+
if inode in visited_inodes:
|
|
165
|
+
continue
|
|
166
|
+
visited_inodes.add(inode)
|
|
167
|
+
|
|
168
|
+
ff = []
|
|
169
|
+
files.sort()
|
|
170
|
+
for fname in files:
|
|
171
|
+
abspath = (root / fname).absolute()
|
|
172
|
+
# Only consider files that exist (e.g. disregard symlinks that point to non-existent files)
|
|
173
|
+
if not os.path.exists(abspath):
|
|
174
|
+
logger.info(f"Skipping non-existent file {abspath}")
|
|
175
|
+
continue
|
|
176
|
+
# Skip socket files
|
|
177
|
+
if stat.S_ISSOCK(os.stat(abspath).st_mode):
|
|
178
|
+
logger.info(f"Skip socket file {abspath}")
|
|
179
|
+
continue
|
|
180
|
+
if ignore_group:
|
|
181
|
+
if ignore_group.is_ignored(abspath):
|
|
182
|
+
continue
|
|
183
|
+
|
|
184
|
+
ff.append(str(abspath))
|
|
185
|
+
all_files.extend(ff)
|
|
186
|
+
|
|
187
|
+
# Remove directories that we've already visited from dirnames
|
|
188
|
+
if deref_symlinks:
|
|
189
|
+
dirnames[:] = [d for d in dirnames if os.stat(os.path.join(root, d)).st_ino not in visited_inodes]
|
|
190
|
+
|
|
191
|
+
return all_files
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _file_is_in_directory(file: str, directory: str) -> bool:
|
|
195
|
+
"""Return True if file is in directory and in its children."""
|
|
196
|
+
try:
|
|
197
|
+
return os.path.commonpath([file, directory]) == directory
|
|
198
|
+
except ValueError as e:
|
|
199
|
+
# ValueError is raised by windows if the paths are not from the same drive
|
|
200
|
+
logger.debug(f"{file} and {directory} are not in the same drive: {e!s}")
|
|
201
|
+
return False
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def list_imported_modules_as_files(source_path: str, modules: List[ModuleType]) -> List[str]:
|
|
205
|
+
"""Copies modules into destination that are in modules. The module files are copied only if:
|
|
206
|
+
|
|
207
|
+
1. Not a site-packages. These are installed packages and not user files.
|
|
208
|
+
2. Not in the sys.base_prefix or sys.prefix. These are also installed and not user files.
|
|
209
|
+
3. Shares a common path with the source_path.
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
import flyte
|
|
213
|
+
from flyte._utils.lazy_module import is_imported
|
|
214
|
+
|
|
215
|
+
files = []
|
|
216
|
+
union_root = os.path.dirname(flyte.__file__)
|
|
217
|
+
|
|
218
|
+
# These directories contain installed packages or modules from the Python standard library.
|
|
219
|
+
# If a module is from these directories, then they are not user files.
|
|
220
|
+
invalid_directories = [union_root, sys.prefix, sys.base_prefix, site.getusersitepackages(), *site.getsitepackages()]
|
|
221
|
+
|
|
222
|
+
for mod in modules:
|
|
223
|
+
# Be careful not to import a module with the .__file__ call if not yet imported.
|
|
224
|
+
if "LazyModule" in object.__getattribute__(mod, "__class__").__name__:
|
|
225
|
+
name = object.__getattribute__(mod, "__name__")
|
|
226
|
+
if is_imported(name):
|
|
227
|
+
mod_file = mod.__file__
|
|
228
|
+
else:
|
|
229
|
+
continue
|
|
230
|
+
else:
|
|
231
|
+
try:
|
|
232
|
+
mod_file = mod.__file__
|
|
233
|
+
except AttributeError:
|
|
234
|
+
continue
|
|
235
|
+
|
|
236
|
+
if mod_file is None:
|
|
237
|
+
continue
|
|
238
|
+
|
|
239
|
+
if any(_file_is_in_directory(mod_file, directory) for directory in invalid_directories):
|
|
240
|
+
continue
|
|
241
|
+
|
|
242
|
+
if not _file_is_in_directory(mod_file, source_path):
|
|
243
|
+
# Only upload files where the module file in the source directory
|
|
244
|
+
continue
|
|
245
|
+
|
|
246
|
+
files.append(mod_file)
|
|
247
|
+
|
|
248
|
+
return files
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def add_imported_modules_from_source(source_path: str, destination: str, modules: List[ModuleType]):
|
|
252
|
+
"""Copies modules into destination that are in modules. The module files are copied only if:
|
|
253
|
+
|
|
254
|
+
1. Not a site-packages. These are installed packages and not user files.
|
|
255
|
+
2. Not in the sys.base_prefix or sys.prefix. These are also installed and not user files.
|
|
256
|
+
3. Does not share a common path with the source_path.
|
|
257
|
+
"""
|
|
258
|
+
# source path is the folder holding the main script.
|
|
259
|
+
# but in register/package case, there are multiple folders.
|
|
260
|
+
# identify a common root amongst the packages listed?
|
|
261
|
+
|
|
262
|
+
files = list_imported_modules_as_files(source_path, modules)
|
|
263
|
+
for file in files:
|
|
264
|
+
relative_path = os.path.relpath(file, start=source_path)
|
|
265
|
+
new_destination = os.path.join(destination, relative_path)
|
|
266
|
+
|
|
267
|
+
if os.path.exists(new_destination):
|
|
268
|
+
# No need to copy if it already exists
|
|
269
|
+
continue
|
|
270
|
+
|
|
271
|
+
os.makedirs(os.path.dirname(new_destination), exist_ok=True)
|
|
272
|
+
shutil.copy(file, new_destination)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def import_module_from_file(module_name, file):
|
|
276
|
+
try:
|
|
277
|
+
spec = importlib.util.spec_from_file_location(module_name, file)
|
|
278
|
+
module = importlib.util.module_from_spec(spec)
|
|
279
|
+
return module
|
|
280
|
+
except Exception as exc:
|
|
281
|
+
raise ModuleNotFoundError(f"Module from file {file} cannot be loaded") from exc
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def get_all_modules(source_path: str, module_name: Optional[str]) -> List[ModuleType]:
|
|
285
|
+
"""Import python file with module_name in source_path and return all modules."""
|
|
286
|
+
sys_modules = list(sys.modules.values())
|
|
287
|
+
if module_name is None or module_name in sys.modules:
|
|
288
|
+
# module already exists, there is no need to import it again
|
|
289
|
+
return sys_modules
|
|
290
|
+
|
|
291
|
+
full_module = os.path.join(source_path, *module_name.split("."))
|
|
292
|
+
full_module_path = f"{full_module}.py"
|
|
293
|
+
|
|
294
|
+
is_python_file = os.path.exists(full_module_path) and os.path.isfile(full_module_path)
|
|
295
|
+
if not is_python_file:
|
|
296
|
+
return sys_modules
|
|
297
|
+
|
|
298
|
+
try:
|
|
299
|
+
new_module = import_module_from_file(module_name, full_module_path)
|
|
300
|
+
return [*sys_modules, new_module]
|
|
301
|
+
except Exception as exc:
|
|
302
|
+
logger.error(f"Using system modules, failed to import {module_name} from {full_module_path}: {exc!s}")
|
|
303
|
+
# Import failed so we fallback to `sys_modules`
|
|
304
|
+
return sys_modules
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
@lru_cache
|
|
308
|
+
def hash_file(file_path: typing.Union[os.PathLike, str]) -> Tuple[bytes, str, int]:
|
|
309
|
+
"""
|
|
310
|
+
Hash a file and produce a digest to be used as a version
|
|
311
|
+
"""
|
|
312
|
+
h = hashlib.md5()
|
|
313
|
+
size = 0
|
|
314
|
+
|
|
315
|
+
with open(file_path, "rb") as file:
|
|
316
|
+
while True:
|
|
317
|
+
# Reading is buffered, so we can read smaller chunks.
|
|
318
|
+
chunk = file.read(h.block_size)
|
|
319
|
+
if not chunk:
|
|
320
|
+
break
|
|
321
|
+
h.update(chunk)
|
|
322
|
+
size += len(chunk)
|
|
323
|
+
|
|
324
|
+
return h.digest(), h.hexdigest(), size
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _find_project_root(source_path) -> str:
|
|
328
|
+
"""
|
|
329
|
+
Find the root of the project.
|
|
330
|
+
The root of the project is considered to be the first ancestor from source_path that does
|
|
331
|
+
not contain a __init__.py file.
|
|
332
|
+
|
|
333
|
+
N.B.: This assumption only holds for regular packages (as opposed to namespace packages)
|
|
334
|
+
"""
|
|
335
|
+
# Start from the directory right above source_path
|
|
336
|
+
path = Path(source_path).parent.resolve()
|
|
337
|
+
while os.path.exists(os.path.join(path, "__init__.py")):
|
|
338
|
+
path = path.parent
|
|
339
|
+
return str(path)
|