syrtis-python-dataflow 1.0.1__py3-none-any.whl → 2.0.0__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.
- syrtis_python_dataflow/__init__.py +31 -0
- syrtis_python_dataflow/common/__init__.py +0 -0
- syrtis_python_dataflow/common/abstract_operator.py +146 -0
- syrtis_python_dataflow/common/abstract_projection_storage.py +57 -0
- syrtis_python_dataflow/common/element.py +34 -0
- syrtis_python_dataflow/common/executor.py +121 -0
- syrtis_python_dataflow/common/file_projection_storage.py +59 -0
- syrtis_python_dataflow/common/memory_projection_storage.py +41 -0
- syrtis_python_dataflow/common/operator_context.py +26 -0
- syrtis_python_dataflow/common/pipeline.py +108 -0
- syrtis_python_dataflow/common/port.py +24 -0
- syrtis_python_dataflow/common/projection_index.py +27 -0
- syrtis_python_dataflow/common/run_report.py +27 -0
- syrtis_python_dataflow/common/stage.py +31 -0
- syrtis_python_dataflow/common/stage_input.py +15 -0
- syrtis_python_dataflow/common/stage_report.py +29 -0
- syrtis_python_dataflow/common/stored_element.py +35 -0
- syrtis_python_dataflow/const/__init__.py +0 -0
- syrtis_python_dataflow/const/config.py +4 -0
- syrtis_python_dataflow/const/port.py +4 -0
- syrtis_python_dataflow/exception/__init__.py +0 -0
- syrtis_python_dataflow/exception/pipeline_definition_exception.py +23 -0
- syrtis_python_dataflow/helper/__init__.py +0 -0
- syrtis_python_dataflow/helper/content.py +36 -0
- syrtis_python_dataflow/helper/fingerprint.py +23 -0
- syrtis_python_dataflow/py.typed +0 -0
- {syrtis_python_dataflow-1.0.1.dist-info → syrtis_python_dataflow-2.0.0.dist-info}/METADATA +11 -2
- syrtis_python_dataflow-2.0.0.dist-info/RECORD +32 -0
- syrtis_python_dataflow-1.0.1.dist-info/RECORD +0 -6
- {syrtis_python_dataflow-1.0.1.dist-info → syrtis_python_dataflow-2.0.0.dist-info}/WHEEL +0 -0
- {syrtis_python_dataflow-1.0.1.dist-info → syrtis_python_dataflow-2.0.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from syrtis_python_dataflow.common.abstract_operator import AbstractOperator
|
|
4
|
+
from syrtis_python_dataflow.common.abstract_projection_storage import AbstractProjectionStorage
|
|
5
|
+
from syrtis_python_dataflow.common.element import Element
|
|
6
|
+
from syrtis_python_dataflow.common.executor import Executor
|
|
7
|
+
from syrtis_python_dataflow.common.file_projection_storage import FileProjectionStorage
|
|
8
|
+
from syrtis_python_dataflow.common.memory_projection_storage import MemoryProjectionStorage
|
|
9
|
+
from syrtis_python_dataflow.common.operator_context import OperatorContext
|
|
10
|
+
from syrtis_python_dataflow.common.pipeline import Pipeline
|
|
11
|
+
from syrtis_python_dataflow.common.port import Port
|
|
12
|
+
from syrtis_python_dataflow.common.run_report import RunReport
|
|
13
|
+
from syrtis_python_dataflow.common.stage import Stage
|
|
14
|
+
from syrtis_python_dataflow.common.stage_input import StageInput
|
|
15
|
+
from syrtis_python_dataflow.common.stage_report import StageReport
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"AbstractOperator",
|
|
19
|
+
"AbstractProjectionStorage",
|
|
20
|
+
"Element",
|
|
21
|
+
"Executor",
|
|
22
|
+
"FileProjectionStorage",
|
|
23
|
+
"MemoryProjectionStorage",
|
|
24
|
+
"OperatorContext",
|
|
25
|
+
"Pipeline",
|
|
26
|
+
"Port",
|
|
27
|
+
"RunReport",
|
|
28
|
+
"Stage",
|
|
29
|
+
"StageInput",
|
|
30
|
+
"StageReport",
|
|
31
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
4
|
+
|
|
5
|
+
from wexample_helpers.classes.abstract_method import abstract_method
|
|
6
|
+
from wexample_helpers.classes.base_class import BaseClass
|
|
7
|
+
from wexample_helpers.decorator.base_class import base_class
|
|
8
|
+
|
|
9
|
+
from syrtis_python_dataflow.const.config import CONFIG_TRANSMIT_CONTENT_TYPE
|
|
10
|
+
from syrtis_python_dataflow.const.port import PORT_NAME_DEFAULT
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from syrtis_python_dataflow.common.element import Element
|
|
14
|
+
from syrtis_python_dataflow.common.operator_context import OperatorContext
|
|
15
|
+
from syrtis_python_dataflow.common.port import Port
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@base_class
|
|
19
|
+
class AbstractOperator(BaseClass):
|
|
20
|
+
"""What a stage runs: named, with its ports. Given, for each input port,
|
|
21
|
+
the whole of what reaches it — the elements of every stage linked to the
|
|
22
|
+
port, in the links' order —, it gives back the elements of its
|
|
23
|
+
projection. Whether it gives one per element read, ten, or one for the
|
|
24
|
+
lot is its own business. A stage whose operator has no input port is
|
|
25
|
+
where a pipe starts: what it reads lives outside the pipe.
|
|
26
|
+
|
|
27
|
+
An element keeps its key through what derives from it (`derive`): what
|
|
28
|
+
two branches made of the same thing is told apart by it, and put back
|
|
29
|
+
together by it.
|
|
30
|
+
|
|
31
|
+
An operator that passes through (`passes_through`, or a stage's config
|
|
32
|
+
saying so: `transmit_content_type`) — a text in capitals is still the
|
|
33
|
+
text it was — gives each element the type it came with,
|
|
34
|
+
whatever its output port declares; a stage reading it may then name a
|
|
35
|
+
type in place of a port, and read the elements of that type alone."""
|
|
36
|
+
|
|
37
|
+
name: str = ""
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def passes_through(cls) -> bool:
|
|
41
|
+
"""Whether what it gives keeps the type of what it read."""
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def transmits(cls, config: dict[str, Any] | None) -> bool:
|
|
46
|
+
"""Whether a stage set with this config keeps the type of what it
|
|
47
|
+
reads: as set (`transmit_content_type`), else as the operator does
|
|
48
|
+
by default."""
|
|
49
|
+
said = (config or {}).get(CONFIG_TRANSMIT_CONTENT_TYPE)
|
|
50
|
+
return cls.passes_through() if said is None else bool(said)
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def get_ports_input(cls) -> list[Port]:
|
|
54
|
+
from syrtis_python_dataflow.common.port import Port
|
|
55
|
+
|
|
56
|
+
return [Port(name=PORT_NAME_DEFAULT)]
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def get_ports_output(cls) -> list[Port]:
|
|
60
|
+
from syrtis_python_dataflow.common.port import Port
|
|
61
|
+
|
|
62
|
+
return [Port(name=PORT_NAME_DEFAULT)]
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def get_port_input(cls, name: str) -> Port | None:
|
|
66
|
+
return next((port for port in cls.get_ports_input() if port.name == name), None)
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def get_port_output(cls, name: str) -> Port | None:
|
|
70
|
+
return next((port for port in cls.get_ports_output() if port.name == name), None)
|
|
71
|
+
|
|
72
|
+
@abstract_method
|
|
73
|
+
def run(self, inputs: dict[str, list[Element]], config: dict[str, Any], context: OperatorContext) -> list[Element]:
|
|
74
|
+
"""The elements of the projection, from what each input port reads."""
|
|
75
|
+
|
|
76
|
+
def each(self, elements: list[Element], make: Callable[[Element], Element | list[Element] | None], context: OperatorContext) -> list[Element]:
|
|
77
|
+
"""What `make` gives of each element — one, several, none —, an
|
|
78
|
+
element it fails on said in the stage's report (`context.fail`) and
|
|
79
|
+
left out: one page out of reach costs that page, not the stage."""
|
|
80
|
+
produced: list[Element] = []
|
|
81
|
+
|
|
82
|
+
for element in elements:
|
|
83
|
+
try:
|
|
84
|
+
made = make(element)
|
|
85
|
+
except Exception as exception: # noqa: BLE001 — whatever it failed with is the element's report.
|
|
86
|
+
context.fail(element, exception)
|
|
87
|
+
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
if isinstance(made, list):
|
|
91
|
+
produced.extend(made)
|
|
92
|
+
elif made is not None:
|
|
93
|
+
produced.append(made)
|
|
94
|
+
|
|
95
|
+
return produced
|
|
96
|
+
|
|
97
|
+
def get_output_content_type(self, config: dict[str, Any], port: str = PORT_NAME_DEFAULT) -> str | None:
|
|
98
|
+
"""The content type the elements of an output port carry; an
|
|
99
|
+
operator whose type depends on its config says it here. None when
|
|
100
|
+
it depends on what is read: the element keeps the type it came with."""
|
|
101
|
+
found = self.get_port_output(port)
|
|
102
|
+
return found.get_content_type() if found else None
|
|
103
|
+
|
|
104
|
+
def element(
|
|
105
|
+
self,
|
|
106
|
+
key: str,
|
|
107
|
+
data: Any,
|
|
108
|
+
content_type: str | None = None,
|
|
109
|
+
parents: list[str] | None = None,
|
|
110
|
+
metadata: dict[str, Any] | None = None,
|
|
111
|
+
config: dict[str, Any] | None = None,
|
|
112
|
+
) -> Element:
|
|
113
|
+
"""A new element, its type the default output port's unless given."""
|
|
114
|
+
from syrtis_python_dataflow.common.element import Element
|
|
115
|
+
|
|
116
|
+
return Element(
|
|
117
|
+
content_type=content_type or self.get_output_content_type(config or {}) or "content/text",
|
|
118
|
+
data=data,
|
|
119
|
+
key=key,
|
|
120
|
+
metadata=metadata or {},
|
|
121
|
+
parents=parents or [],
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def derive(
|
|
125
|
+
self,
|
|
126
|
+
source: Element,
|
|
127
|
+
data: Any,
|
|
128
|
+
content_type: str | None = None,
|
|
129
|
+
metadata: dict[str, Any] | None = None,
|
|
130
|
+
key: str | None = None,
|
|
131
|
+
config: dict[str, Any] | None = None,
|
|
132
|
+
) -> Element:
|
|
133
|
+
"""An element made from another: the same key unless given — a
|
|
134
|
+
paragraph of `moby-dick` is `moby-dick/3` —, descending from it, its
|
|
135
|
+
metadata carried over, its type the source's own when the operator
|
|
136
|
+
passes through, else the output port's, else the source's."""
|
|
137
|
+
kept = source.content_type if self.transmits(config) else None
|
|
138
|
+
|
|
139
|
+
return self.element(
|
|
140
|
+
key=key if key is not None else source.key,
|
|
141
|
+
data=data,
|
|
142
|
+
content_type=content_type or kept or self.get_output_content_type(config or {}) or source.content_type,
|
|
143
|
+
parents=[source.key],
|
|
144
|
+
metadata={**source.metadata, **(metadata or {})},
|
|
145
|
+
config=config,
|
|
146
|
+
)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from wexample_helpers.classes.abstract_method import abstract_method
|
|
6
|
+
from wexample_helpers.classes.base_class import BaseClass
|
|
7
|
+
from wexample_helpers.decorator.base_class import base_class
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from syrtis_python_dataflow.common.element import Element
|
|
11
|
+
from syrtis_python_dataflow.common.projection_index import ProjectionIndex
|
|
12
|
+
from syrtis_python_dataflow.common.stored_element import StoredElement
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@base_class
|
|
16
|
+
class AbstractProjectionStorage(BaseClass):
|
|
17
|
+
"""Where a pipeline's projections are kept between runs: an index per
|
|
18
|
+
projection, and each element's content by key. What a run computes is
|
|
19
|
+
written here; what it finds unchanged is read back from here."""
|
|
20
|
+
|
|
21
|
+
@abstract_method
|
|
22
|
+
def read_index(self, pipeline: str, projection: str) -> ProjectionIndex:
|
|
23
|
+
"""The projection as it stands; empty before any run."""
|
|
24
|
+
|
|
25
|
+
@abstract_method
|
|
26
|
+
def write_index(self, pipeline: str, projection: str, index: ProjectionIndex) -> None:
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
@abstract_method
|
|
30
|
+
def read_content(self, pipeline: str, projection: str, key: str) -> bytes:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
@abstract_method
|
|
34
|
+
def write_content(self, pipeline: str, projection: str, key: str, content: bytes) -> None:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
@abstract_method
|
|
38
|
+
def delete_content(self, pipeline: str, projection: str, key: str) -> None:
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
def read_element(self, pipeline: str, projection: str, stored: StoredElement) -> Element:
|
|
42
|
+
"""An element read back whole from what the index keeps of it."""
|
|
43
|
+
from syrtis_python_dataflow.common.element import Element
|
|
44
|
+
from syrtis_python_dataflow.helper.content import content_from_bytes
|
|
45
|
+
|
|
46
|
+
return Element(
|
|
47
|
+
content_type=stored.content_type,
|
|
48
|
+
data=content_from_bytes(self.read_content(pipeline, projection, stored.key), stored.content_type),
|
|
49
|
+
key=stored.key,
|
|
50
|
+
metadata=dict(stored.metadata),
|
|
51
|
+
parents=list(stored.parents),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def read_elements(self, pipeline: str, projection: str) -> list[Element]:
|
|
55
|
+
"""Every element of a projection, in its order."""
|
|
56
|
+
index = self.read_index(pipeline, projection)
|
|
57
|
+
return [self.read_element(pipeline, projection, stored) for stored in index.elements.values()]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from wexample_helpers.classes.base_class import BaseClass
|
|
6
|
+
from wexample_helpers.classes.field import public_field
|
|
7
|
+
from wexample_helpers.decorator.base_class import base_class
|
|
8
|
+
|
|
9
|
+
from syrtis_python_dataflow.helper.content import ElementData, content_to_bytes
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@base_class
|
|
13
|
+
class Element(BaseClass):
|
|
14
|
+
"""One thing of a projection: a book, a paragraph, a summary. Its key is
|
|
15
|
+
its identity, posed by the operator that first makes it and kept by what
|
|
16
|
+
derives from it — a paragraph of `moby-dick` is `moby-dick/12`, its
|
|
17
|
+
summary keeps that key —; its parents say which elements it was made
|
|
18
|
+
from."""
|
|
19
|
+
|
|
20
|
+
content_type: str = public_field(description="The Syrtis content type its data follows, e.g. `content/text`.")
|
|
21
|
+
data: ElementData = public_field(description="What it holds: text, bytes, or a JSON structure.")
|
|
22
|
+
key: str = public_field(description="Its name in the projection, unique there.")
|
|
23
|
+
metadata: dict[str, Any] = public_field(
|
|
24
|
+
factory=dict, description="What goes with it without being it: a chapter number, a source url."
|
|
25
|
+
)
|
|
26
|
+
parents: list[str] = public_field(
|
|
27
|
+
factory=list, description="The keys of the elements it was made from, in the projections upstream."
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def to_bytes(self) -> bytes:
|
|
31
|
+
return content_to_bytes(self.data)
|
|
32
|
+
|
|
33
|
+
def __repr__(self) -> str:
|
|
34
|
+
return f"Element(key={self.key!r}, content_type={self.content_type!r})"
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
4
|
+
|
|
5
|
+
from wexample_helpers.classes.base_class import BaseClass
|
|
6
|
+
from wexample_helpers.classes.field import public_field
|
|
7
|
+
from wexample_helpers.decorator.base_class import base_class
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from syrtis_python_dataflow.common.abstract_projection_storage import AbstractProjectionStorage
|
|
11
|
+
from syrtis_python_dataflow.common.element import Element
|
|
12
|
+
from syrtis_python_dataflow.common.pipeline import Pipeline
|
|
13
|
+
from syrtis_python_dataflow.common.run_report import RunReport
|
|
14
|
+
from syrtis_python_dataflow.common.stage import Stage
|
|
15
|
+
from syrtis_python_dataflow.common.stage_input import StageInput
|
|
16
|
+
from syrtis_python_dataflow.common.stage_report import StageReport
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@base_class
|
|
20
|
+
class Executor(BaseClass):
|
|
21
|
+
"""Runs a pipeline, stage after stage, from the start down: each
|
|
22
|
+
operator given the whole of what its ports read, its elements written
|
|
23
|
+
in the stage's place of the run before. An element the operator fails
|
|
24
|
+
on (`each`, `context.fail`) is reported and left out, the others kept;
|
|
25
|
+
a stage whose operator fails whole is reported and left empty, the
|
|
26
|
+
stages reading it read nothing. The run goes on."""
|
|
27
|
+
|
|
28
|
+
host: Any = public_field(default=None, description="What every operator is handed in its context: a kernel, a wallet.")
|
|
29
|
+
storage: AbstractProjectionStorage = public_field(description="Where the projections are kept between runs.")
|
|
30
|
+
listener: Callable[[str, Stage, StageReport | None], None] | None = public_field(
|
|
31
|
+
default=None,
|
|
32
|
+
description="Told of each stage as it starts (`stage.start`, no report yet) and ends (`stage.done`, its report): whoever follows the run.",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def run(self, pipeline: Pipeline) -> RunReport:
|
|
36
|
+
from syrtis_python_dataflow.common.run_report import RunReport
|
|
37
|
+
|
|
38
|
+
report = RunReport(pipeline=pipeline.name)
|
|
39
|
+
for stage in pipeline.get_stages_ordered():
|
|
40
|
+
self._tell("stage.start", stage, None)
|
|
41
|
+
stage_report = self._run_stage(pipeline, stage)
|
|
42
|
+
report.stages.append(stage_report)
|
|
43
|
+
self._tell("stage.done", stage, stage_report)
|
|
44
|
+
|
|
45
|
+
return report
|
|
46
|
+
|
|
47
|
+
def _tell(self, event: str, stage: Stage, report: StageReport | None) -> None:
|
|
48
|
+
if self.listener is not None:
|
|
49
|
+
self.listener(event, stage, report)
|
|
50
|
+
|
|
51
|
+
def _run_stage(self, pipeline: Pipeline, stage: Stage) -> StageReport:
|
|
52
|
+
from syrtis_python_dataflow.common.operator_context import OperatorContext
|
|
53
|
+
from syrtis_python_dataflow.common.stage_report import StageReport
|
|
54
|
+
|
|
55
|
+
report = StageReport(name=stage.name)
|
|
56
|
+
# Each port the union of what its links bring, in the links' order; a
|
|
57
|
+
# link naming a type in place of a port brings the elements of that
|
|
58
|
+
# type alone.
|
|
59
|
+
inputs = {
|
|
60
|
+
port.name: [
|
|
61
|
+
element
|
|
62
|
+
for read in stage.inputs.get(port.name, [])
|
|
63
|
+
for element in self._read(pipeline, read)
|
|
64
|
+
]
|
|
65
|
+
for port in stage.operator.get_ports_input()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
context = OperatorContext(host=self.host, stage_name=stage.name)
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
produced = list(stage.operator.run(inputs, stage.config, context))
|
|
72
|
+
# What it made, and the elements it failed on.
|
|
73
|
+
report.errors.extend(context.errors)
|
|
74
|
+
report.failed = len(context.errors)
|
|
75
|
+
except Exception as exception: # noqa: BLE001 — whatever the operator failed with is the stage's report.
|
|
76
|
+
report.failed = 1
|
|
77
|
+
report.errors.append({"key": None, "reason": f"{type(exception).__name__}: {exception}"})
|
|
78
|
+
produced = []
|
|
79
|
+
|
|
80
|
+
for position, element in enumerate(produced):
|
|
81
|
+
if not element.key:
|
|
82
|
+
element.key = str(position)
|
|
83
|
+
|
|
84
|
+
self._write(pipeline, stage, produced)
|
|
85
|
+
report.computed = len(produced)
|
|
86
|
+
report.total = len({element.key for element in produced})
|
|
87
|
+
|
|
88
|
+
return report
|
|
89
|
+
|
|
90
|
+
def _read(self, pipeline: Pipeline, read: StageInput) -> list[Element]:
|
|
91
|
+
elements = self.storage.read_elements(pipeline.name, read.stage)
|
|
92
|
+
upstream = pipeline.get_stage(read.stage)
|
|
93
|
+
|
|
94
|
+
if upstream and upstream.operator.get_port_output(read.port) is None:
|
|
95
|
+
return [element for element in elements if element.content_type == read.port]
|
|
96
|
+
|
|
97
|
+
return elements
|
|
98
|
+
|
|
99
|
+
def _write(self, pipeline: Pipeline, stage: Stage, elements: list[Element]) -> None:
|
|
100
|
+
"""The stage's elements written in place of the run before's: two
|
|
101
|
+
under one key, the last one stands."""
|
|
102
|
+
from syrtis_python_dataflow.common.projection_index import ProjectionIndex
|
|
103
|
+
from syrtis_python_dataflow.common.stored_element import StoredElement
|
|
104
|
+
|
|
105
|
+
before = self.storage.read_index(pipeline.name, stage.name)
|
|
106
|
+
index = ProjectionIndex()
|
|
107
|
+
|
|
108
|
+
for element in elements:
|
|
109
|
+
self.storage.write_content(pipeline.name, stage.name, element.key, element.to_bytes())
|
|
110
|
+
index.elements[element.key] = StoredElement(
|
|
111
|
+
content_type=element.content_type,
|
|
112
|
+
key=element.key,
|
|
113
|
+
metadata=dict(element.metadata),
|
|
114
|
+
parents=list(element.parents),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
for key in before.elements:
|
|
118
|
+
if key not in index.elements:
|
|
119
|
+
self.storage.delete_content(pipeline.name, stage.name, key)
|
|
120
|
+
|
|
121
|
+
self.storage.write_index(pipeline.name, stage.name, index)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from wexample_helpers.classes.field import public_field
|
|
7
|
+
from wexample_helpers.decorator.base_class import base_class
|
|
8
|
+
|
|
9
|
+
from syrtis_python_dataflow.common.abstract_projection_storage import AbstractProjectionStorage
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from syrtis_python_storage.common.abstract_file_storage import AbstractFileStorage
|
|
13
|
+
|
|
14
|
+
from syrtis_python_dataflow.common.projection_index import ProjectionIndex
|
|
15
|
+
|
|
16
|
+
# Where a projection's index stands among its contents.
|
|
17
|
+
FILE_PROJECTION_STORAGE_INDEX_NAME: str = "index.json"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@base_class
|
|
21
|
+
class FileProjectionStorage(AbstractProjectionStorage):
|
|
22
|
+
"""Projections kept in a syrtis-python-storage file storage — a directory,
|
|
23
|
+
a bucket —: `<pipeline>/<projection>/index.json` and one file per
|
|
24
|
+
element, named by a digest of its key so that `a` and `a/b` never
|
|
25
|
+
collide as a file and a directory would."""
|
|
26
|
+
|
|
27
|
+
storage: AbstractFileStorage = public_field(description="The file storage the projections are written to.")
|
|
28
|
+
|
|
29
|
+
def read_index(self, pipeline: str, projection: str) -> ProjectionIndex:
|
|
30
|
+
from syrtis_python_dataflow.common.projection_index import ProjectionIndex
|
|
31
|
+
|
|
32
|
+
path = self._index_path(pipeline, projection)
|
|
33
|
+
if not self.storage.exists(path):
|
|
34
|
+
return ProjectionIndex()
|
|
35
|
+
|
|
36
|
+
return ProjectionIndex.from_dict(json.loads(self.storage.read(path).decode("utf-8")))
|
|
37
|
+
|
|
38
|
+
def write_index(self, pipeline: str, projection: str, index: ProjectionIndex) -> None:
|
|
39
|
+
self.storage.write(
|
|
40
|
+
self._index_path(pipeline, projection),
|
|
41
|
+
json.dumps(index.to_dict(), ensure_ascii=False, indent=2).encode("utf-8"),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def read_content(self, pipeline: str, projection: str, key: str) -> bytes:
|
|
45
|
+
return self.storage.read(self._content_path(pipeline, projection, key))
|
|
46
|
+
|
|
47
|
+
def write_content(self, pipeline: str, projection: str, key: str, content: bytes) -> None:
|
|
48
|
+
self.storage.write(self._content_path(pipeline, projection, key), content)
|
|
49
|
+
|
|
50
|
+
def delete_content(self, pipeline: str, projection: str, key: str) -> None:
|
|
51
|
+
self.storage.delete(self._content_path(pipeline, projection, key))
|
|
52
|
+
|
|
53
|
+
def _index_path(self, pipeline: str, projection: str) -> str:
|
|
54
|
+
return f"{pipeline}/{projection}/{FILE_PROJECTION_STORAGE_INDEX_NAME}"
|
|
55
|
+
|
|
56
|
+
def _content_path(self, pipeline: str, projection: str, key: str) -> str:
|
|
57
|
+
from syrtis_python_dataflow.helper.fingerprint import fingerprint_bytes
|
|
58
|
+
|
|
59
|
+
return f"{pipeline}/{projection}/{fingerprint_bytes(key.encode('utf-8'))[:32]}"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from wexample_helpers.classes.field import public_field
|
|
6
|
+
from wexample_helpers.decorator.base_class import base_class
|
|
7
|
+
|
|
8
|
+
from syrtis_python_dataflow.common.abstract_projection_storage import AbstractProjectionStorage
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from syrtis_python_dataflow.common.projection_index import ProjectionIndex
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@base_class
|
|
15
|
+
class MemoryProjectionStorage(AbstractProjectionStorage):
|
|
16
|
+
"""Projections kept in memory: for tests, and for a run whose result
|
|
17
|
+
is read at once and never again."""
|
|
18
|
+
|
|
19
|
+
contents: dict[str, bytes] = public_field(factory=dict, description="Contents by pipeline, projection and key.")
|
|
20
|
+
indexes: dict[str, ProjectionIndex] = public_field(factory=dict, description="Indexes by pipeline and projection.")
|
|
21
|
+
|
|
22
|
+
def read_index(self, pipeline: str, projection: str) -> ProjectionIndex:
|
|
23
|
+
from syrtis_python_dataflow.common.projection_index import ProjectionIndex
|
|
24
|
+
|
|
25
|
+
index = self.indexes.get(f"{pipeline}/{projection}")
|
|
26
|
+
# Read back as written: a run's own edits never reach the storage before it writes.
|
|
27
|
+
return ProjectionIndex.from_dict(index.to_dict()) if index else ProjectionIndex()
|
|
28
|
+
|
|
29
|
+
def write_index(self, pipeline: str, projection: str, index: ProjectionIndex) -> None:
|
|
30
|
+
from syrtis_python_dataflow.common.projection_index import ProjectionIndex
|
|
31
|
+
|
|
32
|
+
self.indexes[f"{pipeline}/{projection}"] = ProjectionIndex.from_dict(index.to_dict())
|
|
33
|
+
|
|
34
|
+
def read_content(self, pipeline: str, projection: str, key: str) -> bytes:
|
|
35
|
+
return self.contents[f"{pipeline}/{projection}/{key}"]
|
|
36
|
+
|
|
37
|
+
def write_content(self, pipeline: str, projection: str, key: str, content: bytes) -> None:
|
|
38
|
+
self.contents[f"{pipeline}/{projection}/{key}"] = content
|
|
39
|
+
|
|
40
|
+
def delete_content(self, pipeline: str, projection: str, key: str) -> None:
|
|
41
|
+
self.contents.pop(f"{pipeline}/{projection}/{key}", None)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from wexample_helpers.classes.base_class import BaseClass
|
|
6
|
+
from wexample_helpers.classes.field import public_field
|
|
7
|
+
from wexample_helpers.decorator.base_class import base_class
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from syrtis_python_dataflow.common.element import Element
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@base_class
|
|
14
|
+
class OperatorContext(BaseClass):
|
|
15
|
+
"""What an operator is handed besides its inputs and its config: the
|
|
16
|
+
stage it runs for, whatever the host gives every operator — a kernel, a
|
|
17
|
+
wallet —, and where it says which elements it failed on."""
|
|
18
|
+
|
|
19
|
+
errors: list[dict[str, str | None]] = public_field(factory=list, description="The elements the operator could not make anything of, each its key and why: the stage's report.")
|
|
20
|
+
host: Any = public_field(default=None, description="What the host lends its operators; nothing in a bare run.")
|
|
21
|
+
stage_name: str = public_field(description="The stage the operator runs for.")
|
|
22
|
+
|
|
23
|
+
def fail(self, element: Element, exception: BaseException) -> None:
|
|
24
|
+
"""An element the operator could not make anything of: said in the
|
|
25
|
+
stage's report, the others going on."""
|
|
26
|
+
self.errors.append({"key": element.key, "reason": f"{type(exception).__name__}: {exception}"})
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from wexample_helpers.classes.base_class import BaseClass
|
|
6
|
+
from wexample_helpers.classes.field import public_field
|
|
7
|
+
from wexample_helpers.decorator.base_class import base_class
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from syrtis_python_dataflow.common.stage import Stage
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@base_class
|
|
14
|
+
class Pipeline(BaseClass):
|
|
15
|
+
"""Stages reading one another: one pipe, branches allowed, no loop.
|
|
16
|
+
Validated once, so that a run never meets a stage reading a port that
|
|
17
|
+
does not exist or a type its operator refuses. A stage reading nothing
|
|
18
|
+
is where the pipe starts — or a leak the caller chose to leave in."""
|
|
19
|
+
|
|
20
|
+
name: str = public_field(description="Its name, under which its projections are kept.")
|
|
21
|
+
stages: list[Stage] = public_field(factory=list, description="Its stages, in any order.")
|
|
22
|
+
|
|
23
|
+
def get_stage(self, name: str) -> Stage | None:
|
|
24
|
+
return next((stage for stage in self.stages if stage.name == name), None)
|
|
25
|
+
|
|
26
|
+
def get_stages_ordered(self) -> list[Stage]:
|
|
27
|
+
"""The stages from the start down, each after every stage it reads;
|
|
28
|
+
validated first."""
|
|
29
|
+
self.validate()
|
|
30
|
+
|
|
31
|
+
ordered: list[Stage] = []
|
|
32
|
+
remaining = {stage.name: stage for stage in self.stages}
|
|
33
|
+
while remaining:
|
|
34
|
+
ready = [
|
|
35
|
+
stage for stage in remaining.values() if all(read.stage not in remaining for read in stage.get_reads())
|
|
36
|
+
]
|
|
37
|
+
# Validation refused cycles: something is always ready.
|
|
38
|
+
ready.sort(key=lambda stage: stage.name)
|
|
39
|
+
for stage in ready:
|
|
40
|
+
ordered.append(stage)
|
|
41
|
+
del remaining[stage.name]
|
|
42
|
+
|
|
43
|
+
return ordered
|
|
44
|
+
|
|
45
|
+
def get_readers(self, name: str) -> list[Stage]:
|
|
46
|
+
"""The stages reading the one named."""
|
|
47
|
+
return [stage for stage in self.stages if any(read.stage == name for read in stage.get_reads())]
|
|
48
|
+
|
|
49
|
+
def validate(self) -> None:
|
|
50
|
+
from syrtis_python_dataflow.exception.pipeline_definition_exception import (
|
|
51
|
+
PipelineDefinitionException,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
names = [stage.name for stage in self.stages]
|
|
55
|
+
if len(names) != len(set(names)):
|
|
56
|
+
raise PipelineDefinitionException(pipeline_name=self.name, problem="two stages share a name")
|
|
57
|
+
|
|
58
|
+
for stage in self.stages:
|
|
59
|
+
operator = stage.operator
|
|
60
|
+
ports_input = {port.name: port for port in operator.get_ports_input()}
|
|
61
|
+
|
|
62
|
+
for port_name, reads in stage.inputs.items():
|
|
63
|
+
port = ports_input.get(port_name)
|
|
64
|
+
if port is None:
|
|
65
|
+
self._refuse(stage, f"its operator '{operator.name}' has no input port '{port_name}'")
|
|
66
|
+
|
|
67
|
+
for read in reads:
|
|
68
|
+
upstream = self.get_stage(read.stage)
|
|
69
|
+
if upstream is None:
|
|
70
|
+
self._refuse(stage, f"port '{port_name}' reads stage '{read.stage}', which does not exist")
|
|
71
|
+
if upstream is stage:
|
|
72
|
+
self._refuse(stage, "it reads itself")
|
|
73
|
+
# A type named in place of a port, on an operator passing
|
|
74
|
+
# through: the elements of that type alone.
|
|
75
|
+
if upstream.operator.get_port_output(read.port) is None and not upstream.operator.transmits(upstream.config):
|
|
76
|
+
self._refuse(stage, f"port '{port_name}' reads port '{read.port}' of stage '{read.stage}', which its operator does not have")
|
|
77
|
+
given = read.port if upstream.operator.get_port_output(read.port) is None else upstream.operator.get_output_content_type(upstream.config, read.port)
|
|
78
|
+
if given is not None and not port.accepts(given):
|
|
79
|
+
self._refuse(
|
|
80
|
+
stage,
|
|
81
|
+
f"port '{port_name}' takes {', '.join(port.content_types)} but stage '{read.stage}' gives {given}",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
self._refuse_cycles()
|
|
85
|
+
|
|
86
|
+
def _refuse(self, stage: Stage, problem: str) -> None:
|
|
87
|
+
from syrtis_python_dataflow.exception.pipeline_definition_exception import (
|
|
88
|
+
PipelineDefinitionException,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
raise PipelineDefinitionException(pipeline_name=self.name, problem=problem, stage_name=stage.name)
|
|
92
|
+
|
|
93
|
+
def _refuse_cycles(self) -> None:
|
|
94
|
+
from syrtis_python_dataflow.exception.pipeline_definition_exception import (
|
|
95
|
+
PipelineDefinitionException,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Kahn: what cannot be ordered is in a cycle.
|
|
99
|
+
remaining = {stage.name: {read.stage for read in stage.get_reads()} for stage in self.stages}
|
|
100
|
+
while remaining:
|
|
101
|
+
ready = [name for name, reads in remaining.items() if not reads & set(remaining)]
|
|
102
|
+
if not ready:
|
|
103
|
+
raise PipelineDefinitionException(
|
|
104
|
+
pipeline_name=self.name,
|
|
105
|
+
problem=f"stages {', '.join(sorted(remaining))} read one another in a cycle",
|
|
106
|
+
)
|
|
107
|
+
for name in ready:
|
|
108
|
+
del remaining[name]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from wexample_helpers.classes.base_class import BaseClass
|
|
4
|
+
from wexample_helpers.classes.field import public_field
|
|
5
|
+
from wexample_helpers.decorator.base_class import base_class
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@base_class
|
|
9
|
+
class Port(BaseClass):
|
|
10
|
+
"""Where elements enter or leave an operator. An input port says the
|
|
11
|
+
content types it takes — none, any —; an output port the one it gives."""
|
|
12
|
+
|
|
13
|
+
content_types: list[str] = public_field(
|
|
14
|
+
factory=list, description="The content types an input takes, or the single one an output gives."
|
|
15
|
+
)
|
|
16
|
+
name: str = public_field(description="Its name, unique among the operator's ports of its direction.")
|
|
17
|
+
title: str | None = public_field(default=None, description="Its name as a person reads it.")
|
|
18
|
+
|
|
19
|
+
def accepts(self, content_type: str) -> bool:
|
|
20
|
+
return not self.content_types or content_type in self.content_types
|
|
21
|
+
|
|
22
|
+
def get_content_type(self) -> str | None:
|
|
23
|
+
"""What an output port gives; None when it does not say."""
|
|
24
|
+
return self.content_types[0] if self.content_types else None
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from wexample_helpers.classes.base_class import BaseClass
|
|
6
|
+
from wexample_helpers.classes.field import public_field
|
|
7
|
+
from wexample_helpers.decorator.base_class import base_class
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from syrtis_python_dataflow.common.stored_element import StoredElement
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@base_class
|
|
14
|
+
class ProjectionIndex(BaseClass):
|
|
15
|
+
"""What a projection holds, as its storage keeps it: its elements in order."""
|
|
16
|
+
|
|
17
|
+
elements: dict[str, StoredElement] = public_field(factory=dict, description="Its elements by key, in order.")
|
|
18
|
+
|
|
19
|
+
def to_dict(self) -> dict[str, Any]:
|
|
20
|
+
return {"elements": [element.to_dict() for element in self.elements.values()]}
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def from_dict(cls, data: dict[str, Any]) -> ProjectionIndex:
|
|
24
|
+
from syrtis_python_dataflow.common.stored_element import StoredElement
|
|
25
|
+
|
|
26
|
+
elements = [StoredElement.from_dict(element) for element in data.get("elements") or []]
|
|
27
|
+
return cls(elements={element.key: element for element in elements})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from wexample_helpers.classes.base_class import BaseClass
|
|
6
|
+
from wexample_helpers.classes.field import public_field
|
|
7
|
+
from wexample_helpers.decorator.base_class import base_class
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from syrtis_python_dataflow.common.stage_report import StageReport
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@base_class
|
|
14
|
+
class RunReport(BaseClass):
|
|
15
|
+
"""What a run of a pipeline did, stage by stage."""
|
|
16
|
+
|
|
17
|
+
pipeline: str = public_field(description="The pipeline run.")
|
|
18
|
+
stages: list[StageReport] = public_field(factory=list, description="One report per stage, in the order run.")
|
|
19
|
+
|
|
20
|
+
def get_stage(self, name: str) -> StageReport | None:
|
|
21
|
+
return next((stage for stage in self.stages if stage.name == name), None)
|
|
22
|
+
|
|
23
|
+
def has_failures(self) -> bool:
|
|
24
|
+
return any(stage.failed for stage in self.stages)
|
|
25
|
+
|
|
26
|
+
def to_dict(self) -> dict[str, Any]:
|
|
27
|
+
return {"pipeline": self.pipeline, "stages": [stage.to_dict() for stage in self.stages]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from wexample_helpers.classes.base_class import BaseClass
|
|
6
|
+
from wexample_helpers.classes.field import public_field
|
|
7
|
+
from wexample_helpers.decorator.base_class import base_class
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from syrtis_python_dataflow.common.abstract_operator import AbstractOperator
|
|
11
|
+
from syrtis_python_dataflow.common.stage_input import StageInput
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@base_class
|
|
15
|
+
class Stage(BaseClass):
|
|
16
|
+
"""One projection of a pipeline: the elements an operator computes, with
|
|
17
|
+
its config, from the stages it reads — any number per input port of the
|
|
18
|
+
operator, their elements read together. Keyed by its name, under which
|
|
19
|
+
its elements are kept."""
|
|
20
|
+
|
|
21
|
+
config: dict[str, Any] = public_field(factory=dict, description="What the operator is configured with here.")
|
|
22
|
+
inputs: dict[str, list[StageInput]] = public_field(
|
|
23
|
+
factory=dict, description="What each input port of the operator reads, by port name: the links reaching it, in order."
|
|
24
|
+
)
|
|
25
|
+
name: str = public_field(description="Its name, unique in the pipeline.")
|
|
26
|
+
operator: AbstractOperator = public_field(description="What computes its elements.")
|
|
27
|
+
title: str | None = public_field(default=None, description="Its name as a person reads it.")
|
|
28
|
+
|
|
29
|
+
def get_reads(self) -> list[StageInput]:
|
|
30
|
+
"""Every link reaching it, whatever the port."""
|
|
31
|
+
return [read for reads in self.inputs.values() for read in reads]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from wexample_helpers.classes.base_class import BaseClass
|
|
4
|
+
from wexample_helpers.classes.field import public_field
|
|
5
|
+
from wexample_helpers.decorator.base_class import base_class
|
|
6
|
+
|
|
7
|
+
from syrtis_python_dataflow.const.port import PORT_NAME_DEFAULT
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@base_class
|
|
11
|
+
class StageInput(BaseClass):
|
|
12
|
+
"""Where a stage reads from: a port of a stage upstream."""
|
|
13
|
+
|
|
14
|
+
port: str = public_field(default=PORT_NAME_DEFAULT, description="The output port of the stage read.")
|
|
15
|
+
stage: str = public_field(description="The stage read.")
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from wexample_helpers.classes.base_class import BaseClass
|
|
6
|
+
from wexample_helpers.classes.field import public_field
|
|
7
|
+
from wexample_helpers.decorator.base_class import base_class
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@base_class
|
|
11
|
+
class StageReport(BaseClass):
|
|
12
|
+
"""What a run did on one stage: how many elements it made, and what it
|
|
13
|
+
failed on — elements, each its key and why, the others kept; or the
|
|
14
|
+
stage whole, its key none, the stage then holding nothing."""
|
|
15
|
+
|
|
16
|
+
computed: int = public_field(default=0, description="Elements made in this run.")
|
|
17
|
+
errors: list[dict[str, str | None]] = public_field(factory=list, description="What it failed on: an element each — its key, why —, or the stage whole, its key none.")
|
|
18
|
+
failed: int = public_field(default=0, description="The elements it failed on; 1, its key none, when the operator failed whole and the stage was left empty.")
|
|
19
|
+
name: str = public_field(description="The stage.")
|
|
20
|
+
total: int = public_field(default=0, description="Elements the projection holds after the run.")
|
|
21
|
+
|
|
22
|
+
def to_dict(self) -> dict[str, Any]:
|
|
23
|
+
return {
|
|
24
|
+
"name": self.name,
|
|
25
|
+
"computed": self.computed,
|
|
26
|
+
"failed": self.failed,
|
|
27
|
+
"total": self.total,
|
|
28
|
+
"errors": list(self.errors),
|
|
29
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from wexample_helpers.classes.base_class import BaseClass
|
|
6
|
+
from wexample_helpers.classes.field import public_field
|
|
7
|
+
from wexample_helpers.decorator.base_class import base_class
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@base_class
|
|
11
|
+
class StoredElement(BaseClass):
|
|
12
|
+
"""What a projection's index keeps of an element: enough to read it back
|
|
13
|
+
without the run that made it."""
|
|
14
|
+
|
|
15
|
+
content_type: str = public_field(description="The content type of its data.")
|
|
16
|
+
key: str = public_field(description="Its key in the projection.")
|
|
17
|
+
metadata: dict[str, Any] = public_field(factory=dict, description="What goes with it.")
|
|
18
|
+
parents: list[str] = public_field(factory=list, description="The keys it was made from, upstream.")
|
|
19
|
+
|
|
20
|
+
def to_dict(self) -> dict[str, Any]:
|
|
21
|
+
return {
|
|
22
|
+
"content_type": self.content_type,
|
|
23
|
+
"key": self.key,
|
|
24
|
+
"metadata": self.metadata,
|
|
25
|
+
"parents": self.parents,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def from_dict(cls, data: dict[str, Any]) -> StoredElement:
|
|
30
|
+
return cls(
|
|
31
|
+
content_type=data["content_type"],
|
|
32
|
+
key=data["key"],
|
|
33
|
+
metadata=data.get("metadata") or {},
|
|
34
|
+
parents=list(data.get("parents") or []),
|
|
35
|
+
)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import ClassVar
|
|
4
|
+
|
|
5
|
+
from wexample_helpers.classes.field import public_field
|
|
6
|
+
from wexample_helpers.decorator.base_class import base_class
|
|
7
|
+
from wexample_helpers.exception.undefined_exception import UndefinedException
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@base_class
|
|
11
|
+
class PipelineDefinitionException(UndefinedException):
|
|
12
|
+
"""The pipeline cannot run as defined: a stage names an input that does
|
|
13
|
+
not exist, a port its operator lacks, a content type the next operator
|
|
14
|
+
refuses, or the stages form a cycle."""
|
|
15
|
+
|
|
16
|
+
error_code: ClassVar[str] = "DATAFLOW_PIPELINE_DEFINITION"
|
|
17
|
+
pipeline_name: str = public_field(description="The pipeline at fault.")
|
|
18
|
+
problem: str = public_field(description="What is wrong with it.")
|
|
19
|
+
stage_name: str | None = public_field(default=None, description="The stage at fault, when one is.")
|
|
20
|
+
|
|
21
|
+
def _build_message(self) -> str:
|
|
22
|
+
where = f"stage '{self.stage_name}' of pipeline '{self.pipeline_name}'" if self.stage_name else f"pipeline '{self.pipeline_name}'"
|
|
23
|
+
return f"In {where}: {self.problem}"
|
|
File without changes
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
ElementData = bytes | str | dict[str, Any] | list[Any]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def content_to_bytes(data: ElementData) -> bytes:
|
|
10
|
+
"""What an element holds, as it is kept: text as utf-8, bytes as they
|
|
11
|
+
are, a structure as JSON."""
|
|
12
|
+
if isinstance(data, bytes):
|
|
13
|
+
return data
|
|
14
|
+
if isinstance(data, str):
|
|
15
|
+
return data.encode("utf-8")
|
|
16
|
+
return json.dumps(data, ensure_ascii=False, sort_keys=True).encode("utf-8")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def content_from_bytes(content: bytes, content_type: str) -> ElementData:
|
|
20
|
+
"""What was kept, read back as the content type says: JSON for a
|
|
21
|
+
structured type, text for a textual one, bytes otherwise."""
|
|
22
|
+
if content_type_is_structured(content_type):
|
|
23
|
+
return json.loads(content.decode("utf-8"))
|
|
24
|
+
if content_type_is_text(content_type):
|
|
25
|
+
return content.decode("utf-8")
|
|
26
|
+
return content
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def content_type_is_structured(content_type: str) -> bool:
|
|
30
|
+
return content_type.endswith(("/json", "/object", "/list"))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def content_type_is_text(content_type: str) -> bool:
|
|
34
|
+
"""A type whose content is read as text: Syrtis' shapes of text and
|
|
35
|
+
whatever names itself so."""
|
|
36
|
+
return not content_type.endswith(("/binary", "/image", "/file", "/bytes"))
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def fingerprint_bytes(content: bytes) -> str:
|
|
9
|
+
return hashlib.sha256(content).hexdigest()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def fingerprint_json(value: Any) -> str:
|
|
13
|
+
"""The same value written the same way gives the same fingerprint,
|
|
14
|
+
whatever the order its keys were given in."""
|
|
15
|
+
return fingerprint_bytes(
|
|
16
|
+
json.dumps(value, sort_keys=True, separators=(",", ":"), default=str).encode("utf-8")
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def fingerprint_parts(*parts: str) -> str:
|
|
21
|
+
"""One fingerprint of several, each kept apart so that `ab` + `c` and
|
|
22
|
+
`a` + `bc` do not meet."""
|
|
23
|
+
return fingerprint_bytes("\x1f".join(parts).encode("utf-8"))
|
|
File without changes
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: syrtis-python-dataflow
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Author-Email: weeger <contact@syrtis.ai>
|
|
5
5
|
License: MIT
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
7
7
|
Classifier: License :: OSI Approved :: MIT License
|
|
8
8
|
Classifier: Operating System :: OS Independent
|
|
9
9
|
Requires-Python: >=3.10
|
|
10
|
+
Requires-Dist: syrtis-python-storage>=2.0.0
|
|
11
|
+
Requires-Dist: wexample-helpers>=20.6.0
|
|
10
12
|
Provides-Extra: dev
|
|
11
13
|
Requires-Dist: pytest; extra == "dev"
|
|
12
14
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
@@ -14,16 +16,23 @@ Description-Content-Type: text/markdown
|
|
|
14
16
|
|
|
15
17
|
# python-dataflow
|
|
16
18
|
|
|
17
|
-
Version:
|
|
19
|
+
Version: 2.0.0
|
|
18
20
|
|
|
19
21
|
## Table of Contents
|
|
20
22
|
|
|
23
|
+
- [Introduction](#introduction)
|
|
21
24
|
- [Install](#install)
|
|
22
25
|
- [Tests](#tests)
|
|
23
26
|
- [Roadmap](#roadmap)
|
|
24
27
|
- [Status Compatibility](#status-compatibility)
|
|
25
28
|
- [Useful Links](#useful-links)
|
|
26
29
|
|
|
30
|
+
`syrtis-python-dataflow` runs a pipeline: stages reading one another from the sources down, each a projection of elements an operator computes — a book, its paragraphs, their summaries, the chapters', the book's. It is the engine of the data side of Syrtis, where the scenarios' engine holds a conversation: here nothing reasons, and no session is kept; collections are read, transformed and kept, in a storage.
|
|
31
|
+
|
|
32
|
+
What it does that the scenarios' engine does not: it works on collections, element by element; it remembers what every element was computed from, so that a run computes only what changed — a stage's config, a source refreshed — and keeps the rest; it runs a stage's elements on several threads; an element an operator fails on is reported and left out, the stage going on.
|
|
33
|
+
|
|
34
|
+
It knows nothing of Syrtis' database: the host — the core — builds the pipeline from what it keeps, hands the operators what they need, and reads the report back.
|
|
35
|
+
|
|
27
36
|
## Installation
|
|
28
37
|
|
|
29
38
|
```bash
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
syrtis_python_dataflow-2.0.0.dist-info/METADATA,sha256=hyHAbYp-PFuNQRy1sUiAsgWTZvc2gRZUprRNEAuvq7I,3885
|
|
2
|
+
syrtis_python_dataflow-2.0.0.dist-info/WHEEL,sha256=IuIk3wbaEnFH1cx75iM2R1CmliRI3dFLXTbRakgvgFQ,90
|
|
3
|
+
syrtis_python_dataflow-2.0.0.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
|
|
4
|
+
syrtis_python_dataflow/__init__.py,sha256=JMYsyGKsdT609sZ36Os7-HXsedKCtTMpoPo1gUJsDTw,1224
|
|
5
|
+
syrtis_python_dataflow/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
syrtis_python_dataflow/common/abstract_operator.py,sha256=axeia69wb0KbJhx6kE--ocoAEVAkMOvhoUlHAAmhMy8,6028
|
|
7
|
+
syrtis_python_dataflow/common/abstract_projection_storage.py,sha256=GXUsG2S8iMcMVgBDMrWgV43qs4hUtgUq4PhNexlqqNs,2330
|
|
8
|
+
syrtis_python_dataflow/common/element.py,sha256=ht5E39dfqRGMGRXtPwDZBAJaR7RXUbNmkygkiVzOEuI,1471
|
|
9
|
+
syrtis_python_dataflow/common/executor.py,sha256=Nf4wdKxbuTxzamz3UPAYEMeFizGeeeM9wYiaqFl6t4s,5474
|
|
10
|
+
syrtis_python_dataflow/common/file_projection_storage.py,sha256=DR8YVQCuyJ2T1y-zf7nmO-ho4021Vd1q3ut2G8n5uRY,2596
|
|
11
|
+
syrtis_python_dataflow/common/memory_projection_storage.py,sha256=zuaZbMyLJNuJf1Jexu8s_wX9tezOfdAIuSHCuCysFOQ,1932
|
|
12
|
+
syrtis_python_dataflow/common/operator_context.py,sha256=ODVem5xrpCkiwwwkiHJq7rAm4jXdlGe_j6WfKaa9pnY,1275
|
|
13
|
+
syrtis_python_dataflow/common/pipeline.py,sha256=LgCQ1-L3Yr5sJraL6oKCuq-Fi8P0Tw8cTJ44oUzTSvU,5017
|
|
14
|
+
syrtis_python_dataflow/common/port.py,sha256=75oxG_6CQcTZWM4vcOAgujfHt8AM9Jk2YZYq50g1_Uc,1074
|
|
15
|
+
syrtis_python_dataflow/common/projection_index.py,sha256=ojCaS9av6uhMiD-UQgJwX2oTuypks9W3dJjidg4AheU,1054
|
|
16
|
+
syrtis_python_dataflow/common/run_report.py,sha256=ONDVDwJHpMejzj5IW0m7e2mso11ZhbZzUMFk42UUaX0,994
|
|
17
|
+
syrtis_python_dataflow/common/stage.py,sha256=MPT0EWjrzCBikWFKLHyBe8-E-kCkHNVbLI4MLwRxRfA,1462
|
|
18
|
+
syrtis_python_dataflow/common/stage_input.py,sha256=xWPPZDoGjGfkZiTQ5kyzpYAvPNkKsLR5Y-cdlLJjA2E,551
|
|
19
|
+
syrtis_python_dataflow/common/stage_report.py,sha256=ipsZ06MHFBfGDRzc1QpncFfDG0u_STMjGzx3girajTw,1322
|
|
20
|
+
syrtis_python_dataflow/common/stored_element.py,sha256=0YgcmQ5XijVE18CFS8XZ7RVDwQb0DbW-6u88chFZOk0,1254
|
|
21
|
+
syrtis_python_dataflow/const/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
|
+
syrtis_python_dataflow/const/config.py,sha256=3QV_JJWJtTilAOU5oN0jh4vUPfdRNY42idBn8eM3bRU,176
|
|
23
|
+
syrtis_python_dataflow/const/port.py,sha256=4U_sfjIda6XfBMFaQQDbUl3cV8uYMhbNFDRmv_v2m5Y,136
|
|
24
|
+
syrtis_python_dataflow/exception/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
25
|
+
syrtis_python_dataflow/exception/pipeline_definition_exception.py,sha256=ZXdn7bTk4a6ltAw50fmBfUE7nSs35kAf-XL69oCUbtc,1060
|
|
26
|
+
syrtis_python_dataflow/helper/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
27
|
+
syrtis_python_dataflow/helper/content.py,sha256=JBDeEgA1C-YC3uULlQwOwd7joIgp3YqD7vXpUbqRkzs,1259
|
|
28
|
+
syrtis_python_dataflow/helper/fingerprint.py,sha256=5giH4XC8g0c9Psr0joXpBUpDF8rd7C6yejcKOLJ_X-A,690
|
|
29
|
+
syrtis_python_dataflow/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
30
|
+
syrtis_syrtis_python_dataflow/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
31
|
+
syrtis_syrtis_python_dataflow/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
32
|
+
syrtis_python_dataflow-2.0.0.dist-info/RECORD,,
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
syrtis_python_dataflow-1.0.1.dist-info/METADATA,sha256=m1XeStBE0eRvCKhVEk9uo-W70CtnYZAiytl8q7EEnYg,2808
|
|
2
|
-
syrtis_python_dataflow-1.0.1.dist-info/WHEEL,sha256=IuIk3wbaEnFH1cx75iM2R1CmliRI3dFLXTbRakgvgFQ,90
|
|
3
|
-
syrtis_python_dataflow-1.0.1.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
|
|
4
|
-
syrtis_syrtis_python_dataflow/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
-
syrtis_syrtis_python_dataflow/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
-
syrtis_python_dataflow-1.0.1.dist-info/RECORD,,
|
|
File without changes
|
{syrtis_python_dataflow-1.0.1.dist-info → syrtis_python_dataflow-2.0.0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|