formed 0.0.1__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.
- formed/__init__.py +3 -0
- formed/__main__.py +20 -0
- formed/commands/__init__.py +21 -0
- formed/commands/subcommand.py +117 -0
- formed/commands/workflow.py +92 -0
- formed/common/astutils.py +63 -0
- formed/common/base58.py +30 -0
- formed/common/dag.py +111 -0
- formed/common/git.py +76 -0
- formed/common/hashutils.py +70 -0
- formed/common/iterutils.py +158 -0
- formed/common/jsonnet.py +112 -0
- formed/common/logutils.py +60 -0
- formed/common/pkgutils.py +13 -0
- formed/common/typeutils.py +14 -0
- formed/constants.py +9 -0
- formed/integrations/__init__.py +0 -0
- formed/integrations/mlflow/__init__.py +7 -0
- formed/integrations/mlflow/constants.py +6 -0
- formed/integrations/mlflow/utils.py +470 -0
- formed/integrations/mlflow/workflow.py +533 -0
- formed/py.typed +0 -0
- formed/settings.py +48 -0
- formed/types.py +30 -0
- formed/workflow/__init__.py +31 -0
- formed/workflow/cache.py +99 -0
- formed/workflow/callback.py +76 -0
- formed/workflow/colt.py +156 -0
- formed/workflow/constants.py +9 -0
- formed/workflow/executor.py +204 -0
- formed/workflow/format.py +176 -0
- formed/workflow/graph.py +111 -0
- formed/workflow/organizer.py +288 -0
- formed/workflow/settings.py +18 -0
- formed/workflow/step.py +288 -0
- formed/workflow/types.py +6 -0
- formed/workflow/utils.py +42 -0
- formed-0.0.1.dist-info/LICENSE +21 -0
- formed-0.0.1.dist-info/METADATA +42 -0
- formed-0.0.1.dist-info/RECORD +42 -0
- formed-0.0.1.dist-info/WHEEL +4 -0
- formed-0.0.1.dist-info/entry_points.txt +3 -0
formed/__init__.py
ADDED
formed/__main__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from formed.commands import main
|
|
5
|
+
|
|
6
|
+
if os.environ.get("FORMED_DEBUG"):
|
|
7
|
+
LEVEL = logging.DEBUG
|
|
8
|
+
else:
|
|
9
|
+
level_name = os.environ.get("FORMED_LOG_LEVEL", "INFO")
|
|
10
|
+
LEVEL = logging._nameToLevel.get(level_name, logging.INFO)
|
|
11
|
+
|
|
12
|
+
logging.basicConfig(format="%(asctime)s - %(levelname)s - %(name)s - %(message)s", level=LEVEL)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def run() -> None:
|
|
16
|
+
main(prog="formed")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
if __name__ == "__main__":
|
|
20
|
+
run()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from formed import __version__
|
|
5
|
+
from formed.commands import workflow # noqa: F401
|
|
6
|
+
from formed.commands.subcommand import Subcommand
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def create_subcommand(prog: Optional[str] = None) -> Subcommand:
|
|
10
|
+
parser = argparse.ArgumentParser(usage="%(prog)s", prog=prog)
|
|
11
|
+
parser.add_argument(
|
|
12
|
+
"--version",
|
|
13
|
+
action="version",
|
|
14
|
+
version="%(prog)s " + __version__,
|
|
15
|
+
)
|
|
16
|
+
return Subcommand(parser)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main(prog: Optional[str] = None) -> None:
|
|
20
|
+
app = create_subcommand(prog)
|
|
21
|
+
app()
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import re
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import ClassVar, NamedTuple, Optional, TypeVar, cast
|
|
8
|
+
|
|
9
|
+
Subclass = TypeVar("Subclass", bound="Subcommand")
|
|
10
|
+
Registry = dict[type["Subcommand"], dict[str, type["Subcommand"]]]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SubcommandInfo(NamedTuple):
|
|
14
|
+
name: str
|
|
15
|
+
usage: str | None = None
|
|
16
|
+
description: str | None = None
|
|
17
|
+
epilog: str | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Subcommand:
|
|
21
|
+
_registry: ClassVar[Registry] = defaultdict(dict)
|
|
22
|
+
_func_key: ClassVar[str] = "__func"
|
|
23
|
+
_cmd_info: ClassVar[SubcommandInfo]
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def register(
|
|
27
|
+
cls,
|
|
28
|
+
name: str | None = None,
|
|
29
|
+
usage: str | None = None,
|
|
30
|
+
description: str | None = None,
|
|
31
|
+
epilog: str | None = None,
|
|
32
|
+
exist_ok: bool = False,
|
|
33
|
+
) -> Callable[[type[Subclass]], type[Subclass]]:
|
|
34
|
+
registry = Subcommand._registry[cls]
|
|
35
|
+
|
|
36
|
+
def wrapper(subclass: type[Subclass]) -> type[Subclass]:
|
|
37
|
+
info = SubcommandInfo(
|
|
38
|
+
name=name or cls.camel_to_snake(subclass.__name__),
|
|
39
|
+
usage=usage,
|
|
40
|
+
description=description or subclass.__doc__,
|
|
41
|
+
epilog=epilog,
|
|
42
|
+
)
|
|
43
|
+
subclass._cmd_info = info
|
|
44
|
+
|
|
45
|
+
if not exist_ok and name in registry:
|
|
46
|
+
raise ValueError(f"Subcommand '{name}' was already registered.")
|
|
47
|
+
|
|
48
|
+
registry[info.name] = subclass
|
|
49
|
+
return subclass
|
|
50
|
+
|
|
51
|
+
return wrapper
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def get_info(cls) -> SubcommandInfo:
|
|
55
|
+
if not hasattr(cls, "_cmd_info"):
|
|
56
|
+
cls._cmd_info = SubcommandInfo(
|
|
57
|
+
name=cls.camel_to_snake(cls.__name__),
|
|
58
|
+
description=cls.__doc__,
|
|
59
|
+
)
|
|
60
|
+
return cls._cmd_info
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def camel_to_snake(text: str) -> str:
|
|
64
|
+
underscored = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", text)
|
|
65
|
+
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", underscored).lower()
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
parser_or_subparsers: argparse.ArgumentParser | argparse._SubParsersAction | None = None,
|
|
70
|
+
subcommand_info: SubcommandInfo | None = None,
|
|
71
|
+
) -> None:
|
|
72
|
+
cls = type(self)
|
|
73
|
+
info = subcommand_info or self.get_info()
|
|
74
|
+
|
|
75
|
+
if isinstance(parser_or_subparsers, argparse.ArgumentParser):
|
|
76
|
+
self._parser = parser_or_subparsers
|
|
77
|
+
elif isinstance(parser_or_subparsers, argparse._SubParsersAction):
|
|
78
|
+
self._parser = parser_or_subparsers.add_parser(
|
|
79
|
+
name=info.name,
|
|
80
|
+
usage=info.usage,
|
|
81
|
+
description=info.description,
|
|
82
|
+
epilog=info.epilog,
|
|
83
|
+
)
|
|
84
|
+
else:
|
|
85
|
+
self._parser = argparse.ArgumentParser()
|
|
86
|
+
|
|
87
|
+
self.setup()
|
|
88
|
+
self._parser.set_defaults(**{self._func_key: self.run})
|
|
89
|
+
|
|
90
|
+
registry = Subcommand._registry[cls]
|
|
91
|
+
if registry:
|
|
92
|
+
subparsers = self.parser.add_subparsers()
|
|
93
|
+
for subclass in registry.values():
|
|
94
|
+
subclass(subparsers)
|
|
95
|
+
|
|
96
|
+
def __call__(self, args: argparse.Namespace | None = None) -> None:
|
|
97
|
+
if not args:
|
|
98
|
+
args = self.parser.parse_args()
|
|
99
|
+
|
|
100
|
+
func = cast(
|
|
101
|
+
Optional[Callable[[argparse.Namespace], None]],
|
|
102
|
+
getattr(args, self._func_key, None),
|
|
103
|
+
)
|
|
104
|
+
if func:
|
|
105
|
+
func(args)
|
|
106
|
+
else:
|
|
107
|
+
self.parser.print_help()
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def parser(self) -> argparse.ArgumentParser:
|
|
111
|
+
return self._parser
|
|
112
|
+
|
|
113
|
+
def setup(self) -> None:
|
|
114
|
+
"""setup parser"""
|
|
115
|
+
|
|
116
|
+
def run(self, args: argparse.Namespace) -> None:
|
|
117
|
+
self.parser.print_help()
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import pathlib
|
|
3
|
+
import sys
|
|
4
|
+
from logging import getLogger
|
|
5
|
+
|
|
6
|
+
from formed.settings import load_formed_settings
|
|
7
|
+
from formed.workflow import WorkflowExecutionID, WorkflowExecutionInfo, WorkflowGraph
|
|
8
|
+
|
|
9
|
+
from .subcommand import Subcommand
|
|
10
|
+
|
|
11
|
+
logger = getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@Subcommand.register("workflow")
|
|
15
|
+
class WorkflowCommand(Subcommand):
|
|
16
|
+
def setup(self) -> None:
|
|
17
|
+
self.parser.add_argument(
|
|
18
|
+
"--settings",
|
|
19
|
+
type=str,
|
|
20
|
+
default=None,
|
|
21
|
+
help="workflow environment settings file path",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@WorkflowCommand.register("run")
|
|
26
|
+
class WorkflowRunCommand(Subcommand):
|
|
27
|
+
def setup(self) -> None:
|
|
28
|
+
self.parser.add_argument(
|
|
29
|
+
"config",
|
|
30
|
+
type=str,
|
|
31
|
+
help="config file path",
|
|
32
|
+
)
|
|
33
|
+
self.parser.add_argument(
|
|
34
|
+
"--execution-id",
|
|
35
|
+
type=WorkflowExecutionID,
|
|
36
|
+
default=None,
|
|
37
|
+
help="execution id",
|
|
38
|
+
)
|
|
39
|
+
self.parser.add_argument(
|
|
40
|
+
"--step",
|
|
41
|
+
type=str,
|
|
42
|
+
default=None,
|
|
43
|
+
help="step name to execute",
|
|
44
|
+
)
|
|
45
|
+
self.parser.add_argument(
|
|
46
|
+
"--overrides",
|
|
47
|
+
type=str,
|
|
48
|
+
default=None,
|
|
49
|
+
help="overrides jsonnet file path",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def run(self, args: argparse.Namespace) -> None:
|
|
53
|
+
formed_settings = load_formed_settings(args.settings)
|
|
54
|
+
settings = formed_settings.workflow
|
|
55
|
+
config_path = pathlib.Path(args.config)
|
|
56
|
+
|
|
57
|
+
logger.info(f"Load workflow from {config_path}")
|
|
58
|
+
graph = WorkflowGraph.from_jsonnet(config_path, overrides=args.overrides)
|
|
59
|
+
if args.step is not None:
|
|
60
|
+
graph = graph.get_subgraph(args.step)
|
|
61
|
+
|
|
62
|
+
execution = WorkflowExecutionInfo(graph, id=args.execution_id)
|
|
63
|
+
|
|
64
|
+
organizer = settings.organizer
|
|
65
|
+
executor = settings.executor
|
|
66
|
+
|
|
67
|
+
if execution.id is not None and organizer.exists(execution.id):
|
|
68
|
+
logger.error(f"Execution {execution.id} already exists")
|
|
69
|
+
sys.exit(1)
|
|
70
|
+
|
|
71
|
+
organizer.run(executor, execution)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@WorkflowCommand.register("remove")
|
|
75
|
+
class WorkflowRemoveCommand(Subcommand):
|
|
76
|
+
def setup(self) -> None:
|
|
77
|
+
self.parser.add_argument(
|
|
78
|
+
"execution_id",
|
|
79
|
+
type=WorkflowExecutionID,
|
|
80
|
+
help="execution id",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def run(self, args: argparse.Namespace) -> None:
|
|
84
|
+
formed_settings = load_formed_settings(args.settings)
|
|
85
|
+
settings = formed_settings.workflow
|
|
86
|
+
organizer = settings.organizer
|
|
87
|
+
|
|
88
|
+
if not organizer.exists(args.execution_id):
|
|
89
|
+
logger.error(f"Execution {args.execution_id} does not exist")
|
|
90
|
+
sys.exit(1)
|
|
91
|
+
|
|
92
|
+
organizer.remove(args.execution_id)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import itertools
|
|
5
|
+
import textwrap
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ASTNormalizer(ast.NodeTransformer):
|
|
9
|
+
def __init__(self) -> None:
|
|
10
|
+
self.global_name_counter = itertools.count()
|
|
11
|
+
self.scoped_name_counter = itertools.count()
|
|
12
|
+
self.global_name_mapping: dict[str, str] = {}
|
|
13
|
+
self.scope_stack: list[dict[str, str]] = [{}]
|
|
14
|
+
|
|
15
|
+
def find_name_in_scopes(self, name: str) -> str | None:
|
|
16
|
+
for scope in reversed(self.scope_stack):
|
|
17
|
+
if name in scope:
|
|
18
|
+
return scope[name]
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
def visit_Name(self, node: ast.Name) -> ast.Name:
|
|
22
|
+
new_name = self.find_name_in_scopes(node.id)
|
|
23
|
+
if new_name is not None:
|
|
24
|
+
node.id = new_name
|
|
25
|
+
return node
|
|
26
|
+
|
|
27
|
+
def visit_arg(self, node: ast.arg) -> ast.arg:
|
|
28
|
+
new_name = self.find_name_in_scopes(node.arg)
|
|
29
|
+
if new_name is not None:
|
|
30
|
+
node.arg = new_name
|
|
31
|
+
return node
|
|
32
|
+
|
|
33
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef:
|
|
34
|
+
if node.name not in self.global_name_mapping:
|
|
35
|
+
self.global_name_mapping[node.name] = f"var{next(self.global_name_counter)}"
|
|
36
|
+
node.name = self.global_name_mapping[node.name]
|
|
37
|
+
self.scope_stack.append({})
|
|
38
|
+
self.scoped_name_counter = itertools.count()
|
|
39
|
+
for arg in node.args.args:
|
|
40
|
+
new_name = f"var{next(self.scoped_name_counter)}"
|
|
41
|
+
self.scope_stack[-1][arg.arg] = new_name
|
|
42
|
+
self.generic_visit(node)
|
|
43
|
+
self.scope_stack.pop()
|
|
44
|
+
return node
|
|
45
|
+
|
|
46
|
+
def visit_Assign(self, node: ast.Assign) -> ast.Assign:
|
|
47
|
+
for target in node.targets:
|
|
48
|
+
if isinstance(target, ast.Name):
|
|
49
|
+
new_name = self.find_name_in_scopes(target.id)
|
|
50
|
+
if new_name is None:
|
|
51
|
+
new_name = f"var{next(self.scoped_name_counter)}"
|
|
52
|
+
self.scope_stack[-1][target.id] = new_name
|
|
53
|
+
target.id = new_name
|
|
54
|
+
self.generic_visit(node)
|
|
55
|
+
return node
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def normalize_source(source: str) -> str:
|
|
59
|
+
source = textwrap.dedent(source)
|
|
60
|
+
tree = ast.parse(source)
|
|
61
|
+
normalizer = ASTNormalizer()
|
|
62
|
+
tree = normalizer.visit(tree)
|
|
63
|
+
return ast.dump(tree)
|
formed/common/base58.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from typing import Final, Union
|
|
2
|
+
|
|
3
|
+
_ALPHABET: Final[bytes] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def b58encode(s: Union[str, bytes]) -> bytes:
|
|
7
|
+
if isinstance(s, str):
|
|
8
|
+
s = s.encode("ascii")
|
|
9
|
+
original_length = len(s)
|
|
10
|
+
s = s.lstrip(b"\0")
|
|
11
|
+
stripped_length = len(s)
|
|
12
|
+
n = int.from_bytes(s, "big")
|
|
13
|
+
res = bytearray()
|
|
14
|
+
while n:
|
|
15
|
+
n, r = divmod(n, 58)
|
|
16
|
+
res.append(_ALPHABET[r])
|
|
17
|
+
return _ALPHABET[0:1] * (original_length - stripped_length) + bytes(reversed(res))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def b58decode(v: Union[str, bytes]) -> bytes:
|
|
21
|
+
v = v.rstrip()
|
|
22
|
+
if isinstance(v, str):
|
|
23
|
+
v = v.encode("ascii")
|
|
24
|
+
original_length = len(v)
|
|
25
|
+
v = v.lstrip(_ALPHABET[0:1])
|
|
26
|
+
stripped_length = len(v)
|
|
27
|
+
n = 0
|
|
28
|
+
for c in v:
|
|
29
|
+
n = n * 58 + _ALPHABET.index(c)
|
|
30
|
+
return n.to_bytes(original_length - stripped_length + (n.bit_length() + 7) // 8, "big")
|
formed/common/dag.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from collections.abc import Callable, Hashable
|
|
3
|
+
from typing import Generic, TextIO, TypeVar
|
|
4
|
+
|
|
5
|
+
T_Node = TypeVar("T_Node", bound=Hashable)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DAG(Generic[T_Node]):
|
|
9
|
+
def __init__(self, dependencies: dict[T_Node, set[T_Node]]) -> None:
|
|
10
|
+
nodes = set(dependencies.keys() | set(node for deps in dependencies.values() for node in deps))
|
|
11
|
+
empty_dependencies: dict[T_Node, set[T_Node]] = {node: set() for node in nodes}
|
|
12
|
+
|
|
13
|
+
self._dependencies = {**empty_dependencies, **dependencies}
|
|
14
|
+
|
|
15
|
+
def __len__(self) -> int:
|
|
16
|
+
return len(self._dependencies)
|
|
17
|
+
|
|
18
|
+
def __eq__(self, other: object) -> bool:
|
|
19
|
+
if not isinstance(other, DAG):
|
|
20
|
+
return NotImplemented
|
|
21
|
+
return self._dependencies == other._dependencies
|
|
22
|
+
|
|
23
|
+
def __hash__(self) -> int:
|
|
24
|
+
return hash(frozenset((node, frozenset(deps)) for node, deps in self._dependencies.items()))
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def nodes(self) -> set[T_Node]:
|
|
28
|
+
return set(self._dependencies.keys())
|
|
29
|
+
|
|
30
|
+
def subgraph(self, nodes: set[T_Node]) -> "DAG":
|
|
31
|
+
return DAG({node: nodes & deps for node, deps in self._dependencies.items() if node in nodes})
|
|
32
|
+
|
|
33
|
+
def in_degree(self, node: T_Node) -> int:
|
|
34
|
+
return len(self._dependencies[node])
|
|
35
|
+
|
|
36
|
+
def successors(self, node: T_Node) -> set[T_Node]:
|
|
37
|
+
return {successor for successor, deps in self._dependencies.items() if node in deps}
|
|
38
|
+
|
|
39
|
+
def weekly_connected_components(self) -> set["DAG"]:
|
|
40
|
+
groups: list[set[T_Node]] = []
|
|
41
|
+
|
|
42
|
+
for node, deps in self._dependencies.items():
|
|
43
|
+
current_group = {node} | deps
|
|
44
|
+
for group in groups:
|
|
45
|
+
if group & current_group:
|
|
46
|
+
group.update(current_group)
|
|
47
|
+
break
|
|
48
|
+
else:
|
|
49
|
+
groups.append(current_group)
|
|
50
|
+
|
|
51
|
+
for i, group in enumerate(groups):
|
|
52
|
+
for other_group in groups[i + 1 :]:
|
|
53
|
+
if group & other_group:
|
|
54
|
+
group.update(other_group)
|
|
55
|
+
other_group.clear()
|
|
56
|
+
|
|
57
|
+
groups = [group for group in groups if group]
|
|
58
|
+
|
|
59
|
+
return {DAG({node: self._dependencies[node] for node in group}) for group in groups}
|
|
60
|
+
|
|
61
|
+
def visualize(
|
|
62
|
+
self,
|
|
63
|
+
*,
|
|
64
|
+
indent: int = 2,
|
|
65
|
+
output: TextIO = sys.stdout,
|
|
66
|
+
rename: Callable[[T_Node], str] = str,
|
|
67
|
+
) -> None:
|
|
68
|
+
locations: dict[T_Node, tuple[int, int]] = {}
|
|
69
|
+
|
|
70
|
+
def _process_dag(dag: DAG, level: int) -> None:
|
|
71
|
+
for subdag in sorted(dag.weekly_connected_components(), key=lambda g: sorted(g.nodes)):
|
|
72
|
+
_process_component(subdag, level)
|
|
73
|
+
|
|
74
|
+
def _process_component(dag: DAG, level: int) -> None:
|
|
75
|
+
sources = set(node for node in dag.nodes if dag.in_degree(node) == 0)
|
|
76
|
+
for i, node in enumerate(sorted(sources)):
|
|
77
|
+
if node in locations:
|
|
78
|
+
raise ValueError(f"Node {node} already placed")
|
|
79
|
+
locations[node] = (len(locations), level + i)
|
|
80
|
+
|
|
81
|
+
subdag = dag.subgraph(dag.nodes - sources)
|
|
82
|
+
_process_dag(subdag, level + len(sources))
|
|
83
|
+
|
|
84
|
+
_process_dag(self, 0)
|
|
85
|
+
|
|
86
|
+
a = [[" "] * level * indent for _, level in sorted(locations.values())]
|
|
87
|
+
for node, (position, level) in sorted(locations.items(), key=lambda x: x[1][0]):
|
|
88
|
+
successors = sorted(self.successors(node), key=lambda v: locations[v][0])
|
|
89
|
+
if not successors:
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
last_successor_position, last_successor_level = locations[successors[-1]]
|
|
93
|
+
for successor_position in range(position + 1, last_successor_position):
|
|
94
|
+
a[successor_position][level * indent] = "│"
|
|
95
|
+
for successor in successors[:-1]:
|
|
96
|
+
successor_position, successor_level = locations[successor]
|
|
97
|
+
a[successor_position][level * indent] = (
|
|
98
|
+
"┼" if level > 0 and a[successor_position][level * indent - 1] == "─" else "├"
|
|
99
|
+
)
|
|
100
|
+
for offset in range(level * indent + 1, successor_level * indent):
|
|
101
|
+
a[successor_position][offset] = "─"
|
|
102
|
+
a[last_successor_position][level * indent] = (
|
|
103
|
+
"┴" if level > 0 and a[last_successor_position][level * indent - 1] == "─" else "╰"
|
|
104
|
+
)
|
|
105
|
+
for offset in range(level * indent + 1, last_successor_level * indent):
|
|
106
|
+
a[last_successor_position][offset] = "─"
|
|
107
|
+
|
|
108
|
+
output.writelines(
|
|
109
|
+
"".join(a[position]) + "• " + rename(node) + "\n"
|
|
110
|
+
for node, (position, _) in sorted(locations.items(), key=lambda x: x[1][0])
|
|
111
|
+
)
|
formed/common/git.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import os
|
|
3
|
+
import subprocess
|
|
4
|
+
from os import PathLike
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional, Union
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclasses.dataclass(frozen=True)
|
|
10
|
+
class GitInfo:
|
|
11
|
+
commit: str
|
|
12
|
+
branch: str
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class GitClient:
|
|
16
|
+
@staticmethod
|
|
17
|
+
def is_available() -> bool:
|
|
18
|
+
return (
|
|
19
|
+
subprocess.run(["git", "--version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
def __init__(self, directory: Union[str, PathLike]) -> None:
|
|
23
|
+
if not self.is_available():
|
|
24
|
+
raise RuntimeError("Git is not available")
|
|
25
|
+
|
|
26
|
+
self._directory = Path(directory)
|
|
27
|
+
|
|
28
|
+
def root(self) -> Path:
|
|
29
|
+
return Path(os.popen(f"git -C {self._directory} rev-parse --show-toplevel").read().strip())
|
|
30
|
+
|
|
31
|
+
def is_initialized(self) -> bool:
|
|
32
|
+
return (
|
|
33
|
+
subprocess.run(
|
|
34
|
+
["git", "status"], cwd=self._directory, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
|
|
35
|
+
).returncode
|
|
36
|
+
== 0
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def diff(self) -> str:
|
|
40
|
+
return os.popen(f"git -C {self._directory} diff").read()
|
|
41
|
+
|
|
42
|
+
def show(
|
|
43
|
+
self,
|
|
44
|
+
commit: Optional[str] = None,
|
|
45
|
+
format: Optional[str] = None,
|
|
46
|
+
no_patch: bool = False,
|
|
47
|
+
) -> str:
|
|
48
|
+
command = f"git -C {self._directory} show"
|
|
49
|
+
if commit:
|
|
50
|
+
command += f" {commit}"
|
|
51
|
+
if format:
|
|
52
|
+
command += f" --format='{format}'"
|
|
53
|
+
if no_patch:
|
|
54
|
+
command += " --no-patch"
|
|
55
|
+
return os.popen(command).read()
|
|
56
|
+
|
|
57
|
+
def get_commit(self) -> str:
|
|
58
|
+
return self.show(no_patch=True, format="%H").strip()
|
|
59
|
+
|
|
60
|
+
def get_branch(self) -> str:
|
|
61
|
+
return os.popen(f"git -C {self._directory} rev-parse --abbrev-ref HEAD").read().strip()
|
|
62
|
+
|
|
63
|
+
def get_info(self) -> GitInfo:
|
|
64
|
+
commit = self.get_commit()
|
|
65
|
+
branch = self.get_branch()
|
|
66
|
+
return GitInfo(commit, branch)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_git_info(directory: Optional[Union[str, PathLike]] = None) -> Optional[GitInfo]:
|
|
70
|
+
if not GitClient.is_available():
|
|
71
|
+
return None
|
|
72
|
+
directory = directory or Path.cwd()
|
|
73
|
+
client = GitClient(directory)
|
|
74
|
+
if not client.is_available() or not client.is_initialized():
|
|
75
|
+
return None
|
|
76
|
+
return client.get_info()
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import io
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import dill
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def hash_object_bytes(o: Any) -> bytes:
|
|
9
|
+
m = hashlib.blake2b()
|
|
10
|
+
with io.BytesIO() as buffer:
|
|
11
|
+
dill.dump(o, buffer)
|
|
12
|
+
m.update(buffer.getbuffer())
|
|
13
|
+
return m.digest()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def hash_object(o: Any) -> int:
|
|
17
|
+
return int.from_bytes(hash_object_bytes(o), "big")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def murmurhash3(key: str, seed: int = 0) -> int:
|
|
21
|
+
length = len(key)
|
|
22
|
+
|
|
23
|
+
remainder = length & 3
|
|
24
|
+
n = length - remainder
|
|
25
|
+
h1 = seed
|
|
26
|
+
c1 = 0xCC9E2D51
|
|
27
|
+
c2 = 0x1B873593
|
|
28
|
+
i = 0
|
|
29
|
+
|
|
30
|
+
while i < n:
|
|
31
|
+
k1 = (
|
|
32
|
+
(ord(key[i]) & 0xFF)
|
|
33
|
+
| ((ord(key[i + 1]) & 0xFF) << 8)
|
|
34
|
+
| (ord(key[i + 2]) & 0xFF) << 16
|
|
35
|
+
| (ord(key[i + 3]) & 0xFF) << 24
|
|
36
|
+
)
|
|
37
|
+
i += 4
|
|
38
|
+
|
|
39
|
+
k1 = ((((k1 & 0xFFFF) * c1) + ((((k1 >> 16) * c1) & 0xFFFF) << 16))) & 0xFFFFFFFF
|
|
40
|
+
k1 = (k1 << 15) | (k1 >> 17)
|
|
41
|
+
k1 = ((((k1 & 0xFFFF) * c2) + ((((k1 >> 16) * c2) & 0xFFFF) << 16))) & 0xFFFFFFFF
|
|
42
|
+
|
|
43
|
+
h1 ^= k1
|
|
44
|
+
h1 = (h1 << 13) | (h1 >> 19)
|
|
45
|
+
h1b = ((((h1 & 0xFFFF) * 5) + ((((h1 >> 16) * 5) & 0xFFFF) << 16))) & 0xFFFFFFFF
|
|
46
|
+
h1 = ((h1b & 0xFFFF) + 0x6B64) + ((((h1b >> 16) + 0xE654) & 0xFFFF) << 16)
|
|
47
|
+
|
|
48
|
+
k1 = 0
|
|
49
|
+
if remainder == 3:
|
|
50
|
+
k1 ^= (ord(key[i + 2]) & 0xFF) << 16
|
|
51
|
+
k1 ^= (ord(key[i + 1]) & 0xFF) << 8
|
|
52
|
+
k1 ^= ord(key[i]) & 0xFF
|
|
53
|
+
elif remainder == 2:
|
|
54
|
+
k1 ^= (ord(key[i + 1]) & 0xFF) << 8
|
|
55
|
+
k1 ^= ord(key[i]) & 0xFF
|
|
56
|
+
elif remainder == 1:
|
|
57
|
+
k1 ^= ord(key[i]) & 0xFF
|
|
58
|
+
k1 = ((((k1 & 0xFFFF) * c1) + ((((k1 >> 16) * c1) & 0xFFFF) << 16))) & 0xFFFFFFFF
|
|
59
|
+
k1 = (k1 << 15) | (k1 >> 17)
|
|
60
|
+
k1 = ((((k1 & 0xFFFF) * c2) + ((((k1 >> 16) * c2) & 0xFFFF) << 16))) & 0xFFFFFFFF
|
|
61
|
+
h1 ^= k1
|
|
62
|
+
|
|
63
|
+
h1 ^= length
|
|
64
|
+
h1 ^= h1 >> 16
|
|
65
|
+
h1 = ((((h1 & 0xFFFF) * 0x85EBCA6B) + ((((h1 >> 16) * 0x85EBCA6B) & 0xFFFF) << 16))) & 0xFFFFFFFF
|
|
66
|
+
h1 ^= h1 >> 13
|
|
67
|
+
h1 = ((((h1 & 0xFFFF) * 0xC2B2AE35) + ((((h1 >> 16) * 0xC2B2AE35) & 0xFFFF) << 16))) & 0xFFFFFFFF
|
|
68
|
+
h1 ^= h1 >> 16
|
|
69
|
+
|
|
70
|
+
return h1 >> 0
|