syrtis-python-dataflow 1.0.1__tar.gz → 2.0.0__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.
- {syrtis_python_dataflow-1.0.1 → syrtis_python_dataflow-2.0.0}/PKG-INFO +11 -2
- {syrtis_python_dataflow-1.0.1 → syrtis_python_dataflow-2.0.0}/README.md +8 -1
- {syrtis_python_dataflow-1.0.1 → syrtis_python_dataflow-2.0.0}/pyproject.toml +5 -2
- syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/__init__.py +31 -0
- syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/common/abstract_operator.py +146 -0
- syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/common/abstract_projection_storage.py +57 -0
- syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/common/element.py +34 -0
- syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/common/executor.py +121 -0
- syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/common/file_projection_storage.py +59 -0
- syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/common/memory_projection_storage.py +41 -0
- syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/common/operator_context.py +26 -0
- syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/common/pipeline.py +108 -0
- syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/common/port.py +24 -0
- syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/common/projection_index.py +27 -0
- syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/common/run_report.py +27 -0
- syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/common/stage.py +31 -0
- syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/common/stage_input.py +15 -0
- syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/common/stage_report.py +29 -0
- syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/common/stored_element.py +35 -0
- syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/const/config.py +4 -0
- syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/const/port.py +4 -0
- syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/exception/pipeline_definition_exception.py +23 -0
- syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/helper/__init__.py +0 -0
- syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/helper/content.py +36 -0
- syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/helper/fingerprint.py +23 -0
- syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/py.typed +0 -0
- syrtis_python_dataflow-2.0.0/src/syrtis_syrtis_python_dataflow/__init__.py +0 -0
- syrtis_python_dataflow-2.0.0/src/syrtis_syrtis_python_dataflow/py.typed +0 -0
- syrtis_python_dataflow-2.0.0/tests/.gitkeep +0 -0
- syrtis_python_dataflow-2.0.0/tests/__init__.py +0 -0
- syrtis_python_dataflow-2.0.0/tests/book_operators.py +128 -0
- syrtis_python_dataflow-2.0.0/tests/test_executor.py +367 -0
- {syrtis_python_dataflow-1.0.1/src/syrtis_syrtis_python_dataflow → syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/common}/__init__.py +0 -0
- /syrtis_python_dataflow-1.0.1/src/syrtis_syrtis_python_dataflow/py.typed → /syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/const/__init__.py +0 -0
- /syrtis_python_dataflow-1.0.1/tests/.gitkeep → /syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/exception/__init__.py +0 -0
|
@@ -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
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
# python-dataflow
|
|
2
2
|
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
|
|
5
5
|
## Table of Contents
|
|
6
6
|
|
|
7
|
+
- [Introduction](#introduction)
|
|
7
8
|
- [Install](#install)
|
|
8
9
|
- [Tests](#tests)
|
|
9
10
|
- [Roadmap](#roadmap)
|
|
10
11
|
- [Status Compatibility](#status-compatibility)
|
|
11
12
|
- [Useful Links](#useful-links)
|
|
12
13
|
|
|
14
|
+
`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.
|
|
15
|
+
|
|
16
|
+
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.
|
|
17
|
+
|
|
18
|
+
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.
|
|
19
|
+
|
|
13
20
|
## Installation
|
|
14
21
|
|
|
15
22
|
```bash
|
|
@@ -6,7 +6,7 @@ build-backend = "pdm.backend"
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "syrtis-python-dataflow"
|
|
9
|
-
version = "
|
|
9
|
+
version = "2.0.0"
|
|
10
10
|
authors = [
|
|
11
11
|
{ name = "weeger", email = "contact@syrtis.ai" },
|
|
12
12
|
]
|
|
@@ -16,7 +16,10 @@ classifiers = [
|
|
|
16
16
|
"License :: OSI Approved :: MIT License",
|
|
17
17
|
"Operating System :: OS Independent",
|
|
18
18
|
]
|
|
19
|
-
dependencies = [
|
|
19
|
+
dependencies = [
|
|
20
|
+
"syrtis-python-storage>=2.0.0",
|
|
21
|
+
"wexample-helpers>=20.6.0",
|
|
22
|
+
]
|
|
20
23
|
|
|
21
24
|
[project.readme]
|
|
22
25
|
file = "README.md"
|
|
@@ -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
|
+
]
|
|
@@ -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}"})
|