formed 0.0.1__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.
Files changed (41) hide show
  1. formed-0.0.1/LICENSE +21 -0
  2. formed-0.0.1/PKG-INFO +42 -0
  3. formed-0.0.1/README.md +8 -0
  4. formed-0.0.1/formed/__init__.py +3 -0
  5. formed-0.0.1/formed/__main__.py +20 -0
  6. formed-0.0.1/formed/commands/__init__.py +21 -0
  7. formed-0.0.1/formed/commands/subcommand.py +117 -0
  8. formed-0.0.1/formed/commands/workflow.py +92 -0
  9. formed-0.0.1/formed/common/astutils.py +63 -0
  10. formed-0.0.1/formed/common/base58.py +30 -0
  11. formed-0.0.1/formed/common/dag.py +111 -0
  12. formed-0.0.1/formed/common/git.py +76 -0
  13. formed-0.0.1/formed/common/hashutils.py +70 -0
  14. formed-0.0.1/formed/common/iterutils.py +158 -0
  15. formed-0.0.1/formed/common/jsonnet.py +112 -0
  16. formed-0.0.1/formed/common/logutils.py +60 -0
  17. formed-0.0.1/formed/common/pkgutils.py +13 -0
  18. formed-0.0.1/formed/common/typeutils.py +14 -0
  19. formed-0.0.1/formed/constants.py +9 -0
  20. formed-0.0.1/formed/integrations/__init__.py +0 -0
  21. formed-0.0.1/formed/integrations/mlflow/__init__.py +7 -0
  22. formed-0.0.1/formed/integrations/mlflow/constants.py +6 -0
  23. formed-0.0.1/formed/integrations/mlflow/utils.py +470 -0
  24. formed-0.0.1/formed/integrations/mlflow/workflow.py +533 -0
  25. formed-0.0.1/formed/py.typed +0 -0
  26. formed-0.0.1/formed/settings.py +48 -0
  27. formed-0.0.1/formed/types.py +30 -0
  28. formed-0.0.1/formed/workflow/__init__.py +31 -0
  29. formed-0.0.1/formed/workflow/cache.py +99 -0
  30. formed-0.0.1/formed/workflow/callback.py +76 -0
  31. formed-0.0.1/formed/workflow/colt.py +156 -0
  32. formed-0.0.1/formed/workflow/constants.py +9 -0
  33. formed-0.0.1/formed/workflow/executor.py +204 -0
  34. formed-0.0.1/formed/workflow/format.py +176 -0
  35. formed-0.0.1/formed/workflow/graph.py +111 -0
  36. formed-0.0.1/formed/workflow/organizer.py +288 -0
  37. formed-0.0.1/formed/workflow/settings.py +18 -0
  38. formed-0.0.1/formed/workflow/step.py +288 -0
  39. formed-0.0.1/formed/workflow/types.py +6 -0
  40. formed-0.0.1/formed/workflow/utils.py +42 -0
  41. formed-0.0.1/pyproject.toml +109 -0
formed-0.0.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 altescy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
formed-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,42 @@
1
+ Metadata-Version: 2.3
2
+ Name: formed
3
+ Version: 0.0.1
4
+ Summary: ⚙️ Flexible framework for organizing data/experiments/workflows
5
+ License: MIT
6
+ Keywords: python,workflow
7
+ Author: altescy
8
+ Author-email: me@altescy.jp
9
+ Requires-Python: >=3.9,<4.0
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Provides-Extra: all
18
+ Provides-Extra: mlflow
19
+ Requires-Dist: colt (>=0.16.1)
20
+ Requires-Dist: dill
21
+ Requires-Dist: filelock
22
+ Requires-Dist: mlflow (>=2.0,<3.0) ; extra == "mlflow" or extra == "all"
23
+ Requires-Dist: numpy (>=1.26.4) ; extra == "mlflow" or extra == "all"
24
+ Requires-Dist: pyarrow (>=18.0) ; (python_version >= "3.12") and (extra == "mlflow" or extra == "all")
25
+ Requires-Dist: pyarrow ; (python_version >= "3.9" and python_version < "3.12") and (extra == "mlflow" or extra == "all")
26
+ Requires-Dist: pydantic (>=2.0,<3.0)
27
+ Requires-Dist: pyyaml
28
+ Requires-Dist: rjsonnet
29
+ Requires-Dist: scipy (>=1.13) ; (python_version >= "3.9" and python_version < "3.12") and (extra == "mlflow" or extra == "all")
30
+ Requires-Dist: scipy (>=1.14) ; (python_version >= "3.12") and (extra == "mlflow" or extra == "all")
31
+ Project-URL: Homepage, https://altescy.jp/formed
32
+ Description-Content-Type: text/markdown
33
+
34
+ # ⚙️ Formed
35
+
36
+ [![CI](https://github.com/altescy/formed/actions/workflows/ci.yml/badge.svg)](https://github.com/altescy/formed/actions/workflows/ci.yml)
37
+ [![Python version](https://img.shields.io/pypi/pyversions/formed)](https://github.com/altescy/formed)
38
+ [![License](https://img.shields.io/github/license/altescy/formed)](https://github.com/altescy/formed/blob/master/LICENSE)
39
+ [![PyPI version](https://img.shields.io/pypi/v/formed)](https://pypi.org/project/formed/)
40
+
41
+ ⚙️ Flexible framework for organizing data/experiments/workflows
42
+
formed-0.0.1/README.md ADDED
@@ -0,0 +1,8 @@
1
+ # ⚙️ Formed
2
+
3
+ [![CI](https://github.com/altescy/formed/actions/workflows/ci.yml/badge.svg)](https://github.com/altescy/formed/actions/workflows/ci.yml)
4
+ [![Python version](https://img.shields.io/pypi/pyversions/formed)](https://github.com/altescy/formed)
5
+ [![License](https://img.shields.io/github/license/altescy/formed)](https://github.com/altescy/formed/blob/master/LICENSE)
6
+ [![PyPI version](https://img.shields.io/pypi/v/formed)](https://pypi.org/project/formed/)
7
+
8
+ ⚙️ Flexible framework for organizing data/experiments/workflows
@@ -0,0 +1,3 @@
1
+ from importlib.metadata import version
2
+
3
+ __version__ = version("formed")
@@ -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)
@@ -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")
@@ -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
+ )
@@ -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