trendify2 2.0.4__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.
Files changed (76) hide show
  1. trendify/__about__.py +14 -0
  2. trendify/__init__.py +250 -0
  3. trendify/base/__init__.py +36 -0
  4. trendify/base/helpers.py +76 -0
  5. trendify/base/pen.py +161 -0
  6. trendify/base/record.py +148 -0
  7. trendify/cli.py +403 -0
  8. trendify/color.py +260 -0
  9. trendify/examples.py +474 -0
  10. trendify/formats/__init__.py +28 -0
  11. trendify/formats/format2d.py +128 -0
  12. trendify/formats/table.py +55 -0
  13. trendify/generator/__init__.py +38 -0
  14. trendify/generator/generate.py +167 -0
  15. trendify/generator/histogrammer.py +62 -0
  16. trendify/generator/render.py +195 -0
  17. trendify/generator/table_builder.py +194 -0
  18. trendify/generator/xy_data_plotter.py +195 -0
  19. trendify/log.py +195 -0
  20. trendify/pipeline.py +136 -0
  21. trendify/plotting/__init__.py +48 -0
  22. trendify/plotting/axline.py +122 -0
  23. trendify/plotting/figure.py +389 -0
  24. trendify/plotting/histogram.py +282 -0
  25. trendify/plotting/point.py +89 -0
  26. trendify/plotting/scatter.py +100 -0
  27. trendify/plotting/trace.py +129 -0
  28. trendify/progress.py +43 -0
  29. trendify/store/__init__.py +30 -0
  30. trendify/store/db.py +119 -0
  31. trendify/store/record_store.py +448 -0
  32. trendify/store/tags.py +65 -0
  33. trendify/styling/__init__.py +30 -0
  34. trendify/styling/grid.py +125 -0
  35. trendify/styling/legend.py +217 -0
  36. trendify/styling/marker.py +168 -0
  37. trendify/typing.py +26 -0
  38. trendify/viewer/__init__.py +41 -0
  39. trendify/viewer/app.py +208 -0
  40. trendify/viewer/hydration.py +181 -0
  41. trendify/viewer/plot_config.py +102 -0
  42. trendify/viewer/routes/__init__.py +6 -0
  43. trendify/viewer/routes/api.py +360 -0
  44. trendify/viewer/routes/pages.py +39 -0
  45. trendify/viewer/tag_tree.py +148 -0
  46. trendify/viewer/templates/base.html +63 -0
  47. trendify/viewer/templates/index.html +131 -0
  48. trendify/viewer/templates/partials/__init__.py +1 -0
  49. trendify/viewer/templates/partials/footer.html +15 -0
  50. trendify/viewer/templates/partials/nav.html +181 -0
  51. trendify/viewer/templates/partials/plot_view.html +221 -0
  52. trendify/viewer/templates/partials/sidebar.html +163 -0
  53. trendify/viewer/templates/partials/table_view.html +43 -0
  54. trendify/viewer/templates/static/__init__.py +1 -0
  55. trendify/viewer/templates/static/assets/__init__.py +1 -0
  56. trendify/viewer/templates/static/assets/main-logo-white-bg.svg +75 -0
  57. trendify/viewer/templates/static/assets/main-logo.svg +66 -0
  58. trendify/viewer/templates/static/css/app.css +75 -0
  59. trendify/viewer/templates/static/js/main.js +2 -0
  60. trendify/viewer/templates/static/vendored/__init__.py +1 -0
  61. trendify/viewer/templates/static/vendored/alpine-3.15.12.min.js +5 -0
  62. trendify/viewer/templates/static/vendored/alpine-LICENSE.md +21 -0
  63. trendify/viewer/templates/static/vendored/dataTables-LICENSE.txt +3 -0
  64. trendify/viewer/templates/static/vendored/dataTables.dataTables.min.css +1 -0
  65. trendify/viewer/templates/static/vendored/dataTables.min.js +4 -0
  66. trendify/viewer/templates/static/vendored/jquery-3.7.1.min.js +2 -0
  67. trendify/viewer/templates/static/vendored/jquery-LICENSE.txt +3 -0
  68. trendify/viewer/templates/static/vendored/plotly-3.6.0.min.js +3882 -0
  69. trendify/viewer/templates/static/vendored/plotly-LICENSE.txt +21 -0
  70. trendify/viewer/templates/static/vendored/tailwind-4.3.2.global.js +947 -0
  71. trendify/viewer/templates/static/vendored/tailwind-LICENSE +21 -0
  72. trendify2-2.0.4.dist-info/METADATA +107 -0
  73. trendify2-2.0.4.dist-info/RECORD +76 -0
  74. trendify2-2.0.4.dist-info/WHEEL +4 -0
  75. trendify2-2.0.4.dist-info/entry_points.txt +2 -0
  76. trendify2-2.0.4.dist-info/licenses/LICENSE.txt +21 -0
trendify/__about__.py ADDED
@@ -0,0 +1,14 @@
1
+ """Looks up the installed version of this package."""
2
+
3
+ from datetime import date
4
+ from importlib.metadata import PackageNotFoundError, version
5
+
6
+ try:
7
+ __version__ = version("trendify2")
8
+ except PackageNotFoundError:
9
+ __version__ = "0.0.0-dev"
10
+
11
+ __license__ = "MIT"
12
+ __summary__ = "Tools for generating data products, storing, and interacting with them."
13
+ __copyright__ = f"© {date.today().year} Talbot Knighton and David Gable"
14
+ __url__ = "https://github.com/Hydrowelder/trendify"
trendify/__init__.py ADDED
@@ -0,0 +1,250 @@
1
+ """
2
+ Tools for generating, storing, and interacting with tagged records (points, traces, tables,
3
+ histograms) produced by batch-processing many run directories.
4
+
5
+ Write a `RecordGenerator` that returns records for one run directory, run it over many runs
6
+ with [`TrendifyPipeline`][trendify.pipeline.TrendifyPipeline] or the `trendify` CLI, then browse
7
+ the results in the viewer or render them as plots and tables.
8
+ """
9
+
10
+ from trendify import (
11
+ base,
12
+ cli,
13
+ color,
14
+ examples,
15
+ formats,
16
+ generator,
17
+ log,
18
+ pipeline,
19
+ plotting,
20
+ store,
21
+ styling,
22
+ typing,
23
+ viewer,
24
+ )
25
+ from trendify.base import (
26
+ HashableBase,
27
+ Pen,
28
+ R,
29
+ Record,
30
+ RecordGenerator,
31
+ RecordList,
32
+ RecordType,
33
+ Tag,
34
+ Tags,
35
+ helpers,
36
+ pen,
37
+ record,
38
+ )
39
+ from trendify.cli import (
40
+ app as cli_app,
41
+ )
42
+ from trendify.color import (
43
+ Color,
44
+ )
45
+ from trendify.examples import (
46
+ example_record_generator,
47
+ make_example_data,
48
+ )
49
+ from trendify.formats import (
50
+ AxisScale,
51
+ Format2D,
52
+ PlottableData2D,
53
+ Rastered,
54
+ TableEntry,
55
+ Vector,
56
+ XYData,
57
+ format2d,
58
+ table,
59
+ )
60
+ from trendify.generator import (
61
+ Histogrammer,
62
+ TableBuilder,
63
+ XYDataPlotter,
64
+ generate,
65
+ generate_records,
66
+ get_sorted_dirs,
67
+ histogrammer,
68
+ render,
69
+ render_assets,
70
+ table_builder,
71
+ xy_data_plotter,
72
+ )
73
+ from trendify.log import (
74
+ create_queue_listener,
75
+ get_logger,
76
+ set_log_level,
77
+ setup_logger,
78
+ worker_init,
79
+ )
80
+ from trendify.pipeline import (
81
+ TrendifyPipeline,
82
+ )
83
+ from trendify.plotting import (
84
+ AxLine,
85
+ HistogramEntry,
86
+ HistogramStyle,
87
+ LineOrientation,
88
+ PlotlyFigure,
89
+ Point2D,
90
+ Scatter2D,
91
+ SingleAxisFigure,
92
+ Trace2D,
93
+ axline,
94
+ figure,
95
+ histogram,
96
+ point,
97
+ scatter,
98
+ trace,
99
+ )
100
+ from trendify.store import (
101
+ SCHEMA_VERSION,
102
+ RecordStore,
103
+ connect,
104
+ db,
105
+ decode_tag,
106
+ encode_tag,
107
+ record_store,
108
+ tag_to_path_parts,
109
+ tags,
110
+ )
111
+ from trendify.styling import (
112
+ Grid,
113
+ GridAxis,
114
+ GridTheme,
115
+ Legend,
116
+ LegendLocation,
117
+ Marker,
118
+ grid,
119
+ legend,
120
+ marker,
121
+ )
122
+ from trendify.typing import (
123
+ MatN,
124
+ VecN,
125
+ )
126
+ from trendify.viewer import (
127
+ HoverMode,
128
+ InterpMode,
129
+ LineMode,
130
+ PlotConfig,
131
+ TagNode,
132
+ api,
133
+ build_tag_tree,
134
+ camel_case_dict,
135
+ create_app,
136
+ create_app_from_env,
137
+ pages,
138
+ plot_config,
139
+ routes,
140
+ tag_tree,
141
+ viewer_app,
142
+ )
143
+
144
+ __all__ = [
145
+ "SCHEMA_VERSION",
146
+ "AxLine",
147
+ "AxisScale",
148
+ "Color",
149
+ "Format2D",
150
+ "Grid",
151
+ "GridAxis",
152
+ "GridTheme",
153
+ "HashableBase",
154
+ "HistogramEntry",
155
+ "HistogramStyle",
156
+ "Histogrammer",
157
+ "HoverMode",
158
+ "InterpMode",
159
+ "Legend",
160
+ "LegendLocation",
161
+ "LineMode",
162
+ "LineOrientation",
163
+ "Marker",
164
+ "MatN",
165
+ "Pen",
166
+ "PlotConfig",
167
+ "PlotlyFigure",
168
+ "PlottableData2D",
169
+ "Point2D",
170
+ "R",
171
+ "Rastered",
172
+ "Record",
173
+ "RecordGenerator",
174
+ "RecordList",
175
+ "RecordStore",
176
+ "RecordType",
177
+ "Scatter2D",
178
+ "SingleAxisFigure",
179
+ "TableBuilder",
180
+ "TableEntry",
181
+ "Tag",
182
+ "TagNode",
183
+ "Tags",
184
+ "Trace2D",
185
+ "TrendifyPipeline",
186
+ "VecN",
187
+ "Vector",
188
+ "XYData",
189
+ "XYDataPlotter",
190
+ "api",
191
+ "axline",
192
+ "base",
193
+ "build_tag_tree",
194
+ "camel_case_dict",
195
+ "cli",
196
+ "cli_app",
197
+ "color",
198
+ "connect",
199
+ "create_app",
200
+ "create_app_from_env",
201
+ "create_queue_listener",
202
+ "db",
203
+ "decode_tag",
204
+ "encode_tag",
205
+ "example_record_generator",
206
+ "examples",
207
+ "figure",
208
+ "format2d",
209
+ "formats",
210
+ "generate",
211
+ "generate_records",
212
+ "generator",
213
+ "get_logger",
214
+ "get_sorted_dirs",
215
+ "grid",
216
+ "helpers",
217
+ "histogram",
218
+ "histogrammer",
219
+ "legend",
220
+ "log",
221
+ "make_example_data",
222
+ "marker",
223
+ "pages",
224
+ "pen",
225
+ "pipeline",
226
+ "plot_config",
227
+ "plotting",
228
+ "point",
229
+ "record",
230
+ "record_store",
231
+ "render",
232
+ "render_assets",
233
+ "routes",
234
+ "scatter",
235
+ "set_log_level",
236
+ "setup_logger",
237
+ "store",
238
+ "styling",
239
+ "table",
240
+ "table_builder",
241
+ "tag_to_path_parts",
242
+ "tag_tree",
243
+ "tags",
244
+ "trace",
245
+ "typing",
246
+ "viewer",
247
+ "viewer_app",
248
+ "worker_init",
249
+ "xy_data_plotter",
250
+ ]
@@ -0,0 +1,36 @@
1
+ """Core building blocks: the `Record` base class, `Pen` styling, and shared type helpers."""
2
+
3
+ from trendify.base import helpers
4
+ from trendify.base import pen
5
+ from trendify.base import record
6
+
7
+ from trendify.base.helpers import (
8
+ HashableBase,
9
+ R,
10
+ RecordType,
11
+ Tag,
12
+ Tags,
13
+ )
14
+ from trendify.base.pen import (
15
+ Pen,
16
+ )
17
+ from trendify.base.record import (
18
+ Record,
19
+ RecordGenerator,
20
+ RecordList,
21
+ )
22
+
23
+ __all__ = [
24
+ "HashableBase",
25
+ "Pen",
26
+ "R",
27
+ "Record",
28
+ "RecordGenerator",
29
+ "RecordList",
30
+ "RecordType",
31
+ "Tag",
32
+ "Tags",
33
+ "helpers",
34
+ "pen",
35
+ "record",
36
+ ]
@@ -0,0 +1,76 @@
1
+ """
2
+ Small shared building blocks used across the rest of the package: the `Tag`/`Tags` type
3
+ aliases every `Record` and the store's tag-encoding logic build on, `HashableBase` for
4
+ pydantic models that need to go in a `set`, and the `RecordType` enum.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from enum import StrEnum
11
+ from typing import TYPE_CHECKING, TypeVar
12
+
13
+ from pydantic import BaseModel
14
+
15
+ if TYPE_CHECKING:
16
+ from trendify.base.record import Record
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ __all__ = [
21
+ "HashableBase",
22
+ "R",
23
+ "RecordType",
24
+ "Tag",
25
+ "Tags",
26
+ ]
27
+
28
+ R = TypeVar("R", bound="Record")
29
+ """Bound to `Record`: used by `RecordStore.get_records`/`get_records_of_type` so a
30
+ call like `get_records_of_type(Point2D)` returns `list[Point2D]` rather than `list[Record]`.
31
+ """
32
+
33
+ Tag = str | int | tuple[str | int, ...]
34
+ """
35
+ Determines what types can be used to define a tag.
36
+
37
+ Tags must be reliably JSON-encodable (str/int, or tuples thereof) because the store
38
+ canonicalizes a tag into a `tag_key` via `json.dumps` for indexed lookup (see
39
+ `trendify.store.tags`).
40
+ """
41
+
42
+ Tags = list[Tag]
43
+ """
44
+ List of tags
45
+ """
46
+
47
+
48
+ class HashableBase(BaseModel):
49
+ """
50
+ Defines a base for hashable pydantic data classes so that they can be reduced to a minimal set through type-casting.
51
+ """
52
+
53
+ def __hash__(self):
54
+ """
55
+ Defines hash function
56
+ """
57
+ return hash((type(self), *tuple(self.__dict__.values())))
58
+
59
+
60
+ class RecordType(StrEnum):
61
+ """Defines all record types. Used to type-cast URL info in server to validate."""
62
+
63
+ XY_DATA = "xy_data"
64
+ """XY_DATA name"""
65
+
66
+ TRACE_2D = "trace_2d"
67
+ """TRACE_2D name"""
68
+
69
+ POINT_2D = "point_2d"
70
+ """POINT_2D name"""
71
+
72
+ TABLE_ENTRY = "table_entry"
73
+ """TABLE_ENTRY name"""
74
+
75
+ HISTOGRAM_ENTRY = "histogram_entry"
76
+ """HISTOGRAM_ENTRY name"""
trendify/base/pen.py ADDED
@@ -0,0 +1,161 @@
1
+ """
2
+ `Pen`: shared line/marker styling (color, size, alpha, linestyle, zorder, label) used by
3
+ `Trace2D`, `AxLine`, and anything else that draws a line to matplotlib or Plotly.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any
9
+
10
+ from matplotlib.colors import to_rgba
11
+ from pydantic import ConfigDict
12
+
13
+ from trendify.base.helpers import HashableBase
14
+
15
+ __all__ = ["Pen"]
16
+
17
+
18
+ class Pen(HashableBase):
19
+ """Defines the pen drawing style."""
20
+
21
+ color: tuple[float, float, float] | tuple[float, float, float, float] | str = "k"
22
+ """Color of line"""
23
+
24
+ size: float = 1
25
+ """Line width"""
26
+
27
+ alpha: float = 1
28
+ """Opacity from 0 to 1 (inclusive)"""
29
+
30
+ zorder: float = 0
31
+ """Prioritization of trace line relative to other plotted data"""
32
+
33
+ linestyle: str | tuple[int, tuple[int, ...]] | None = "-"
34
+ """Linestyle to plot. Supports `str` or `tuple` definition ([matplotlib documentation](https://matplotlib.org/stable/gallery/lines_bars_and_markers/linestyles.html)).
35
+ Use `None` to draw no line at all, for a markers-only trace."""
36
+
37
+ label: str | None = None
38
+ """Legend label"""
39
+
40
+ model_config = ConfigDict(extra="forbid")
41
+
42
+ def as_scatter_plot_kwargs(self) -> dict[str, Any]:
43
+ """
44
+ Returns kwargs dictionary for passing to [matplotlib plot][matplotlib.axes.Axes.plot] method
45
+ """
46
+ return {
47
+ "color": self.color,
48
+ "linewidth": self.size,
49
+ # matplotlib's `Line2D` treats `linestyle=None` as "use the default style", not
50
+ # "no line" (only the string "none" means that), so `None` is translated here to
51
+ # keep `Pen.linestyle = None` meaning "no line" from `has_line`'s point of view.
52
+ "linestyle": self.linestyle if self.linestyle is not None else "none",
53
+ "alpha": self.alpha,
54
+ "zorder": self.zorder,
55
+ "label": self.label,
56
+ }
57
+
58
+ @property
59
+ def has_line(self) -> bool:
60
+ """
61
+ Whether this pen draws a visible line. `False` when `linestyle` is `None`, which
62
+ callers like `Trace2D.add_to_plotly` use to render markers only, with no connecting
63
+ line.
64
+ """
65
+ return self.linestyle is not None
66
+
67
+ def _convert_linestyle_to_plotly(self) -> str:
68
+ """Convert matplotlib linestyle to plotly dash style"""
69
+ # `None` means "no line" (see `has_line`); callers exclude "lines" from the Plotly
70
+ # trace's `mode` in that case, so this value is never actually rendered, but return
71
+ # something valid regardless.
72
+ if self.linestyle is None:
73
+ return "solid"
74
+
75
+ # Handle string styles
76
+ style_map = {
77
+ "-": "solid",
78
+ "--": "dash",
79
+ ":": "dot",
80
+ "-.": "dashdot",
81
+ }
82
+ if isinstance(self.linestyle, str):
83
+ return style_map.get(self.linestyle, "solid")
84
+
85
+ # Handle tuple styles - convert to 'dash' as approximation
86
+ return "dash"
87
+
88
+ @property
89
+ def rgba(self) -> str:
90
+ """
91
+ Convert the pen's color to rgba string format.
92
+
93
+ Returns:
94
+ str: Color in 'rgba(r,g,b,a)' format where r,g,b are 0-255 and a is 0-1
95
+
96
+ """
97
+ # Handle different color input formats
98
+ if isinstance(self.color, tuple):
99
+ if len(self.color) == 3: # RGB tuple
100
+ r, g, b = self.color
101
+ a = self.alpha
102
+ else: # RGBA tuple
103
+ r, g, b, a = self.color
104
+ # Convert 0-1 range to 0-255 for RGB
105
+ r, g, b = int(r * 255), int(g * 255), int(b * 255)
106
+ else: # String color (name or hex)
107
+ # Use matplotlib's color converter
108
+ rgba_vals = to_rgba(self.color, self.alpha)
109
+ # Convert 0-1 range to 0-255 for RGB
110
+ r, g, b = [int(x * 255) for x in rgba_vals[:3]]
111
+ a = rgba_vals[3]
112
+
113
+ return f"rgba({r}, {g}, {b}, {a})"
114
+
115
+ @property
116
+ def rgb(self) -> str:
117
+ return ", ".join(self.rgba.split(",")[0:-1]) + ")"
118
+
119
+ def get_contrast_color(self, background_luminance: float = 1.0) -> str:
120
+ """
121
+ Returns 'white' or 'black' to provide the best contrast against the pen's color,
122
+ taking into account the alpha (transparency) value of the line.
123
+
124
+ Args:
125
+ background_luminance (float): The luminance of the background (default is 1.0 for white).
126
+
127
+ Returns:
128
+ str: 'white' or 'black'
129
+
130
+ """
131
+ # Convert the pen's color to RGB (0-255 range) and get alpha
132
+ if isinstance(self.color, tuple):
133
+ if len(self.color) == 3: # RGB tuple
134
+ r, g, b = self.color
135
+ a = self.alpha
136
+ else: # RGBA tuple
137
+ r, g, b, a = self.color
138
+ r, g, b = int(r * 255), int(g * 255), int(b * 255)
139
+ else: # String color (name or hex)
140
+ rgba_vals = to_rgba(self.color, self.alpha)
141
+ r, g, b = [int(x * 255) for x in rgba_vals[:3]]
142
+ a = rgba_vals[3]
143
+
144
+ # Calculate relative luminance of the pen's color
145
+ def luminance(channel: float) -> float:
146
+ channel /= 255.0
147
+ return (
148
+ channel / 12.92
149
+ if channel <= 0.03928
150
+ else ((channel + 0.055) / 1.055) ** 2.4
151
+ )
152
+
153
+ color_luminance = (
154
+ 0.2126 * luminance(r) + 0.7152 * luminance(g) + 0.0722 * luminance(b)
155
+ )
156
+
157
+ # Blend the color luminance with the background luminance based on alpha
158
+ blended_luminance = (1 - a) * background_luminance + a * color_luminance
159
+
160
+ # Return white for dark blended colors, black for light blended colors
161
+ return "white" if blended_luminance < 0.5 else "black"
@@ -0,0 +1,148 @@
1
+ """
2
+ Base `Record` pydantic model, the `RecordGenerator`/`RecordList` type aliases user code
3
+ implements against, and the subclass registry `RecordStore` uses to deserialize a stored
4
+ `record_type` string back into its concrete pydantic class.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from collections.abc import Callable
11
+ from pathlib import Path
12
+ from typing import Any, Self
13
+
14
+ from pydantic import (
15
+ BaseModel,
16
+ ConfigDict,
17
+ InstanceOf,
18
+ SerializeAsAny,
19
+ computed_field,
20
+ model_validator,
21
+ )
22
+
23
+ from trendify.base.helpers import Tags
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ __all__ = ["Record", "RecordGenerator", "RecordList"]
28
+
29
+ _record_subclass_registry: dict[str, type[Record]] = {}
30
+
31
+
32
+ class Record(BaseModel):
33
+ """
34
+ Base class for records to be generated and handled.
35
+ """
36
+
37
+ tags: Tags
38
+ """Tags to be used for sorting data."""
39
+
40
+ metadata: dict[str, str] = {}
41
+ """A dictionary of metadata to be used as a tool tip for mouseover in Grafana."""
42
+
43
+ @model_validator(mode="before")
44
+ @classmethod
45
+ def _remove_computed_fields(cls, data: dict[str, Any]) -> dict[str, Any]:
46
+ """
47
+ Removes computed fields before passing data to constructor.
48
+
49
+ Args:
50
+ data (dict[str, Any]): Raw data to be validated before passing to pydantic class constructor.
51
+
52
+ Returns:
53
+ (dict[str, Any]): Sanitized data to be passed to class constructor.
54
+
55
+ """
56
+ for f in cls.model_computed_fields:
57
+ data.pop(f, None)
58
+ return data
59
+
60
+ @computed_field
61
+ @property
62
+ def record_type(self) -> str:
63
+ """
64
+ Returns:
65
+ (str): Record type should be the same as the class name.
66
+ The record type is used to search for records from the store.
67
+
68
+ """
69
+ return type(self).__name__
70
+
71
+ def __init_subclass__(cls, **kwargs: Any) -> None:
72
+ """
73
+ Registers child subclasses to be able to parse them from JSON payloads using the
74
+ [deserialize][trendify.base.record.Record.deserialize] method
75
+ """
76
+ super().__init_subclass__(**kwargs)
77
+ _record_subclass_registry[cls.__name__] = cls
78
+ logger.debug(f"Registered Record subclass {cls.__name__!r}")
79
+
80
+ model_config = ConfigDict(extra="allow")
81
+
82
+ def append_to_list(self, to_add_to: list) -> Self:
83
+ """
84
+ Appends self to list.
85
+
86
+ Args:
87
+ to_add_to (List): list to which `self` will be appended
88
+
89
+ Returns:
90
+ (Self): returns instance of `self`
91
+
92
+ """
93
+ to_add_to.append(self)
94
+ return self
95
+
96
+ @classmethod
97
+ def registry(cls) -> dict[str, type[Record]]:
98
+ """
99
+ Returns:
100
+ (dict[str, type[Record]]): a copy of the record_type -> class registry,
101
+ used by the store to resolve which leaf `record_type` names correspond to a
102
+ given (possibly non-leaf) type when filtering by `object_type`.
103
+
104
+ """
105
+ return dict(_record_subclass_registry)
106
+
107
+ @classmethod
108
+ def deserialize(cls, record_type: str, payload: str) -> Record:
109
+ """
110
+ Loads a stored JSON payload into the pydantic dataclass registered under `record_type`.
111
+
112
+ Args:
113
+ record_type (str): the `record_type` discriminator string (same as the class name)
114
+ payload (str): the raw JSON text (e.g. from the store's `records.payload` column)
115
+
116
+ Returns:
117
+ (Record): the reconstructed, correctly-typed record instance
118
+
119
+ """
120
+ try:
121
+ duck_type = _record_subclass_registry[record_type]
122
+ except KeyError:
123
+ logger.error(
124
+ f"No registered Record subclass named {record_type!r}. Known types: "
125
+ f"{sorted(_record_subclass_registry)}. This usually means the module "
126
+ f"defining that subclass hasn't been imported in this process."
127
+ )
128
+ raise
129
+ return duck_type.model_validate_json(payload)
130
+
131
+ def set_metadata(self, new: dict[str, str]) -> Self:
132
+ self.metadata = new
133
+ return self
134
+
135
+
136
+ RecordList = list[SerializeAsAny[InstanceOf[Record]]]
137
+ """List of serializable [Record][trendify.base.record.Record] or child classes thereof"""
138
+
139
+ RecordGenerator = Callable[[Path], RecordList]
140
+ """
141
+ Callable method type. Users must provide a `RecordGenerator` to map over raw data.
142
+
143
+ Args:
144
+ path (Path): Workdir holding raw data (Should be one per run from a batch)
145
+
146
+ Returns:
147
+ (RecordList): List of records to be sorted and used to produce assets
148
+ """