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.
Files changed (35) hide show
  1. {syrtis_python_dataflow-1.0.1 → syrtis_python_dataflow-2.0.0}/PKG-INFO +11 -2
  2. {syrtis_python_dataflow-1.0.1 → syrtis_python_dataflow-2.0.0}/README.md +8 -1
  3. {syrtis_python_dataflow-1.0.1 → syrtis_python_dataflow-2.0.0}/pyproject.toml +5 -2
  4. syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/__init__.py +31 -0
  5. syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/common/abstract_operator.py +146 -0
  6. syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/common/abstract_projection_storage.py +57 -0
  7. syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/common/element.py +34 -0
  8. syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/common/executor.py +121 -0
  9. syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/common/file_projection_storage.py +59 -0
  10. syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/common/memory_projection_storage.py +41 -0
  11. syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/common/operator_context.py +26 -0
  12. syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/common/pipeline.py +108 -0
  13. syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/common/port.py +24 -0
  14. syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/common/projection_index.py +27 -0
  15. syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/common/run_report.py +27 -0
  16. syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/common/stage.py +31 -0
  17. syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/common/stage_input.py +15 -0
  18. syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/common/stage_report.py +29 -0
  19. syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/common/stored_element.py +35 -0
  20. syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/const/config.py +4 -0
  21. syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/const/port.py +4 -0
  22. syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/exception/pipeline_definition_exception.py +23 -0
  23. syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/helper/__init__.py +0 -0
  24. syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/helper/content.py +36 -0
  25. syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/helper/fingerprint.py +23 -0
  26. syrtis_python_dataflow-2.0.0/src/syrtis_python_dataflow/py.typed +0 -0
  27. syrtis_python_dataflow-2.0.0/src/syrtis_syrtis_python_dataflow/__init__.py +0 -0
  28. syrtis_python_dataflow-2.0.0/src/syrtis_syrtis_python_dataflow/py.typed +0 -0
  29. syrtis_python_dataflow-2.0.0/tests/.gitkeep +0 -0
  30. syrtis_python_dataflow-2.0.0/tests/__init__.py +0 -0
  31. syrtis_python_dataflow-2.0.0/tests/book_operators.py +128 -0
  32. syrtis_python_dataflow-2.0.0/tests/test_executor.py +367 -0
  33. {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
  34. /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
  35. /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: 1.0.1
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: 1.0.1
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: 1.0.1
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 = "1.0.1"
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}"})