auto-html-report 0.1.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.
- auto_html_report/__init__.py +141 -0
- auto_html_report/_naming.py +35 -0
- auto_html_report/_version.py +29 -0
- auto_html_report/application/__init__.py +1 -0
- auto_html_report/application/collect_blocks.py +161 -0
- auto_html_report/application/context.py +65 -0
- auto_html_report/application/discovery.py +319 -0
- auto_html_report/application/execution.py +533 -0
- auto_html_report/application/ports.py +199 -0
- auto_html_report/blocks/__init__.py +14 -0
- auto_html_report/blocks/maintenance_history.py +76 -0
- auto_html_report/blocks/wafer_map.py +64 -0
- auto_html_report/domain/__init__.py +6 -0
- auto_html_report/domain/block.py +98 -0
- auto_html_report/domain/errors.py +46 -0
- auto_html_report/domain/event.py +39 -0
- auto_html_report/domain/report.py +126 -0
- auto_html_report/exit_codes.py +69 -0
- auto_html_report/generate.py +151 -0
- auto_html_report/infrastructure/__init__.py +1 -0
- auto_html_report/infrastructure/clock.py +27 -0
- auto_html_report/infrastructure/factories.py +148 -0
- auto_html_report/infrastructure/logging_setup.py +161 -0
- auto_html_report/infrastructure/settings.py +265 -0
- auto_html_report/infrastructure/stores/__init__.py +7 -0
- auto_html_report/infrastructure/stores/postgres_store.py +426 -0
- auto_html_report/infrastructure/stores/sqlite_store.py +408 -0
- auto_html_report/infrastructure/writers/__init__.py +1 -0
- auto_html_report/infrastructure/writers/_names.py +30 -0
- auto_html_report/infrastructure/writers/local_writer.py +100 -0
- auto_html_report/infrastructure/writers/s3_writer.py +136 -0
- auto_html_report/processing.py +469 -0
- auto_html_report/py.typed +0 -0
- auto_html_report/registry.py +164 -0
- auto_html_report/rendering/__init__.py +5 -0
- auto_html_report/rendering/cells.py +170 -0
- auto_html_report/rendering/escaping.py +64 -0
- auto_html_report/rendering/figures.py +116 -0
- auto_html_report/rendering/shell.py +121 -0
- auto_html_report/rendering/templates/report.css +197 -0
- auto_html_report/rendering/templates/report.html.j2 +54 -0
- auto_html_report/reports/__init__.py +6 -0
- auto_html_report/reports/manufacturing.py +67 -0
- auto_html_report/testing.py +93 -0
- auto_html_report-0.1.0.dist-info/METADATA +387 -0
- auto_html_report-0.1.0.dist-info/RECORD +48 -0
- auto_html_report-0.1.0.dist-info/WHEEL +4 -0
- auto_html_report-0.1.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""auto_html_report -- dynamic, self-contained HTML report generation.
|
|
2
|
+
|
|
3
|
+
This module is the public surface: everything a block author, a report factory or a job script should
|
|
4
|
+
import. It grew in stages -- the author-facing names in stage 3, the report model and entry point in
|
|
5
|
+
stages 5 and 6, the configuration factories in stage 7, the event store and discovery job in stage 8,
|
|
6
|
+
the processing job and exit codes in stage 9, and the report family and examples in stage 10.
|
|
7
|
+
Stage 11 added the optional PostgreSQL store and S3 writer, which import their drivers only when
|
|
8
|
+
constructed. ``auto_html_report.testing`` is public too, but is not re-exported here, because production
|
|
9
|
+
code has no use for it.
|
|
10
|
+
|
|
11
|
+
Nothing here imports matplotlib or pyplot eagerly: a report built only from HTML tables never pays for
|
|
12
|
+
them, and a test in ``tests/unit/test_rendering_figures.py`` pins that in a subprocess.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from auto_html_report._version import get_version
|
|
18
|
+
from auto_html_report.application.collect_blocks import collect_blocks
|
|
19
|
+
from auto_html_report.application.context import DiscoveryContext, RenderContext, ReportContext
|
|
20
|
+
from auto_html_report.application.discovery import (
|
|
21
|
+
Discoverer,
|
|
22
|
+
DiscoveryOutcome,
|
|
23
|
+
DiscoveryRun,
|
|
24
|
+
run_discovery,
|
|
25
|
+
)
|
|
26
|
+
from auto_html_report.application.ports import (
|
|
27
|
+
Clock,
|
|
28
|
+
EventStore,
|
|
29
|
+
FailureRecord,
|
|
30
|
+
HttpClient,
|
|
31
|
+
OutputWriter,
|
|
32
|
+
SaveResult,
|
|
33
|
+
)
|
|
34
|
+
from auto_html_report.domain.block import Block, BlockOutcome, BlockStatus
|
|
35
|
+
from auto_html_report.domain.errors import (
|
|
36
|
+
AutoHtmlReportError,
|
|
37
|
+
BlockError,
|
|
38
|
+
ConfigurationError,
|
|
39
|
+
OutputError,
|
|
40
|
+
PermanentBlockError,
|
|
41
|
+
StoreError,
|
|
42
|
+
TransientBlockError,
|
|
43
|
+
)
|
|
44
|
+
from auto_html_report.domain.event import Event, EventStatus
|
|
45
|
+
from auto_html_report.domain.report import (
|
|
46
|
+
OutputLocation,
|
|
47
|
+
Report,
|
|
48
|
+
ReportDefinition,
|
|
49
|
+
ReportResult,
|
|
50
|
+
ReportRow,
|
|
51
|
+
)
|
|
52
|
+
from auto_html_report.exit_codes import ExitCode, exit_code_for
|
|
53
|
+
from auto_html_report.generate import generate_report, report_filename
|
|
54
|
+
from auto_html_report.infrastructure.clock import SystemClock
|
|
55
|
+
from auto_html_report.infrastructure.factories import (
|
|
56
|
+
build_output_writer,
|
|
57
|
+
new_discovery_context,
|
|
58
|
+
new_report_context,
|
|
59
|
+
)
|
|
60
|
+
from auto_html_report.infrastructure.logging_setup import configure_logging
|
|
61
|
+
from auto_html_report.infrastructure.settings import Settings, load_settings
|
|
62
|
+
from auto_html_report.infrastructure.stores.postgres_store import PostgresEventStore
|
|
63
|
+
from auto_html_report.infrastructure.stores.sqlite_store import SqliteEventStore
|
|
64
|
+
from auto_html_report.infrastructure.writers.local_writer import LocalDirectoryWriter
|
|
65
|
+
from auto_html_report.infrastructure.writers.s3_writer import S3Writer
|
|
66
|
+
from auto_html_report.processing import (
|
|
67
|
+
EventProcessingOutcome,
|
|
68
|
+
ProcessingRun,
|
|
69
|
+
run_processing,
|
|
70
|
+
)
|
|
71
|
+
from auto_html_report.registry import register_report
|
|
72
|
+
from auto_html_report.rendering.escaping import escape, html_table
|
|
73
|
+
from auto_html_report.rendering.figures import figure_lock, figure_to_data_uri
|
|
74
|
+
from auto_html_report.rendering.shell import render_report
|
|
75
|
+
|
|
76
|
+
# Deliberately flat and alphabetical. The names fall into groups -- writing a block, building a report,
|
|
77
|
+
# producing the document, configuring it, and classifying a failure -- but the ordering carries no
|
|
78
|
+
# meaning, because an unsorted __all__ is a lint error in this project.
|
|
79
|
+
#: The installed version, computed once at import from the distribution metadata. ``get_version()``
|
|
80
|
+
#: remains the source (``docs/08-packaging.md`` §7); this is the attribute a consumer reaches for first.
|
|
81
|
+
__version__ = get_version()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
__all__ = [
|
|
85
|
+
"AutoHtmlReportError",
|
|
86
|
+
"Block",
|
|
87
|
+
"BlockError",
|
|
88
|
+
"BlockOutcome",
|
|
89
|
+
"BlockStatus",
|
|
90
|
+
"Clock",
|
|
91
|
+
"ConfigurationError",
|
|
92
|
+
"Discoverer",
|
|
93
|
+
"DiscoveryContext",
|
|
94
|
+
"DiscoveryOutcome",
|
|
95
|
+
"DiscoveryRun",
|
|
96
|
+
"Event",
|
|
97
|
+
"EventProcessingOutcome",
|
|
98
|
+
"EventStatus",
|
|
99
|
+
"EventStore",
|
|
100
|
+
"ExitCode",
|
|
101
|
+
"FailureRecord",
|
|
102
|
+
"HttpClient",
|
|
103
|
+
"LocalDirectoryWriter",
|
|
104
|
+
"OutputError",
|
|
105
|
+
"OutputLocation",
|
|
106
|
+
"OutputWriter",
|
|
107
|
+
"PermanentBlockError",
|
|
108
|
+
"PostgresEventStore",
|
|
109
|
+
"ProcessingRun",
|
|
110
|
+
"RenderContext",
|
|
111
|
+
"Report",
|
|
112
|
+
"ReportContext",
|
|
113
|
+
"ReportDefinition",
|
|
114
|
+
"ReportResult",
|
|
115
|
+
"ReportRow",
|
|
116
|
+
"S3Writer",
|
|
117
|
+
"SaveResult",
|
|
118
|
+
"Settings",
|
|
119
|
+
"SqliteEventStore",
|
|
120
|
+
"StoreError",
|
|
121
|
+
"SystemClock",
|
|
122
|
+
"TransientBlockError",
|
|
123
|
+
"__version__",
|
|
124
|
+
"build_output_writer",
|
|
125
|
+
"collect_blocks",
|
|
126
|
+
"configure_logging",
|
|
127
|
+
"escape",
|
|
128
|
+
"exit_code_for",
|
|
129
|
+
"figure_lock",
|
|
130
|
+
"figure_to_data_uri",
|
|
131
|
+
"generate_report",
|
|
132
|
+
"html_table",
|
|
133
|
+
"load_settings",
|
|
134
|
+
"new_discovery_context",
|
|
135
|
+
"new_report_context",
|
|
136
|
+
"register_report",
|
|
137
|
+
"render_report",
|
|
138
|
+
"report_filename",
|
|
139
|
+
"run_discovery",
|
|
140
|
+
"run_processing",
|
|
141
|
+
]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Slug generation, shared by the things that need a filesystem- and attribute-safe token.
|
|
2
|
+
|
|
3
|
+
Two callers want this and they must not disagree: the row's ``data-block`` attribute
|
|
4
|
+
(``rendering/cells.py``) and the report filename (stage 6, ``docs/06-output-html.md`` §2). One
|
|
5
|
+
implementation, so a title that slugs cleanly for a filename slugs identically in the document.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
#: Matches runs of anything that is not a lowercase letter or digit.
|
|
13
|
+
_NON_SLUG_CHARACTERS = re.compile(r"[^a-z0-9]+")
|
|
14
|
+
|
|
15
|
+
#: The documented cap, chosen so that a filename built from a title, an event id and a timestamp
|
|
16
|
+
#: still fits comfortably inside filesystem and S3 key limits.
|
|
17
|
+
DEFAULT_MAX_LENGTH = 60
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def slug(text: str, *, max_length: int = DEFAULT_MAX_LENGTH, fallback: str = "") -> str:
|
|
21
|
+
"""Return ``text`` as a lowercase, hyphen-separated token of at most ``max_length`` characters.
|
|
22
|
+
|
|
23
|
+
``fallback`` is returned when nothing survives -- a title consisting only of emoji or punctuation
|
|
24
|
+
slugs to the empty string, and every caller needs *something* rather than a silently blank
|
|
25
|
+
identifier. Callers pass their own fallback (``"block"``, ``"report"``) rather than each inventing
|
|
26
|
+
a check.
|
|
27
|
+
"""
|
|
28
|
+
if max_length < 1:
|
|
29
|
+
raise ValueError(f"max_length must be at least 1, got {max_length}")
|
|
30
|
+
|
|
31
|
+
slugified = _NON_SLUG_CHARACTERS.sub("-", text.lower()).strip("-")
|
|
32
|
+
if len(slugified) > max_length:
|
|
33
|
+
# Truncate, then strip again: cutting mid-word can leave a trailing hyphen.
|
|
34
|
+
slugified = slugified[:max_length].rstrip("-")
|
|
35
|
+
return slugified or fallback
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Resolves the installed package version.
|
|
2
|
+
|
|
3
|
+
``pyproject.toml`` is the single source of truth. Nothing here stores a copy of it, because a second
|
|
4
|
+
copy is a copy that drifts -- the same reasoning that removed ``requirements.txt`` (ADR-0021).
|
|
5
|
+
|
|
6
|
+
Resolution is at runtime, and the not-installed path is real rather than hypothetical: running from
|
|
7
|
+
a source tree with the package not installed raises ``PackageNotFoundError``, which this module
|
|
8
|
+
converts into a placeholder rather than a guess. See ``docs/08-packaging.md`` §7 and ADR-0023.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
14
|
+
|
|
15
|
+
#: Distribution name, matching ``[project] name`` in pyproject.toml. Both the hyphen and underscore
|
|
16
|
+
#: spellings resolve, so this constant is not a portability trap whichever one a reader expects.
|
|
17
|
+
_DISTRIBUTION_NAME = "auto-html-report"
|
|
18
|
+
|
|
19
|
+
#: Reported when the package is not installed. Deliberately not a plausible release number: a report
|
|
20
|
+
#: footer should never claim a version that was never built.
|
|
21
|
+
UNKNOWN_VERSION = "0+unknown"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_version() -> str:
|
|
25
|
+
"""Return the installed distribution's version, or :data:`UNKNOWN_VERSION` if not installed."""
|
|
26
|
+
try:
|
|
27
|
+
return version(_DISTRIBUTION_NAME)
|
|
28
|
+
except PackageNotFoundError:
|
|
29
|
+
return UNKNOWN_VERSION
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""The application layer: use cases that compose domain types and injected ports."""
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Concurrent collection: every block in a report, assembled into ordered rows.
|
|
2
|
+
|
|
3
|
+
**Collection produces data and outcomes; rendering produces HTML.** A failed block's row carries an
|
|
4
|
+
empty ``html`` and a populated ``outcome``, and the error cell is built by the renderer from that
|
|
5
|
+
outcome. That is not an accident of layering (application/ may not import rendering/) — it is the right
|
|
6
|
+
split: this module knows *what happened*, the renderer knows *how it looks*, and the error detail level
|
|
7
|
+
is a presentation setting.
|
|
8
|
+
|
|
9
|
+
**The execution rules are not here.** Timeout accounting, the retry loop, backoff and the hang guard
|
|
10
|
+
live in ``application/execution.py``, because discoverers obey the same rules (``docs/05`` §1) and two
|
|
11
|
+
copies of that logic would drift. This module supplies the block-specific parts: which callables to
|
|
12
|
+
run, the duplicate-name check, and the mapping from an execution result to a :class:`ReportRow`. The
|
|
13
|
+
one thing it keeps in the worker is the success log line, so ``block.finish`` is emitted when the block
|
|
14
|
+
finishes rather than when the slowest sibling does.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from collections.abc import Callable, Sequence
|
|
20
|
+
from datetime import datetime
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from auto_html_report.application.context import RenderContext, ReportContext
|
|
24
|
+
from auto_html_report.application.execution import (
|
|
25
|
+
BLOCK_EXECUTION,
|
|
26
|
+
ExecutionResult,
|
|
27
|
+
ExecutionStatus,
|
|
28
|
+
Task,
|
|
29
|
+
execute_tasks,
|
|
30
|
+
)
|
|
31
|
+
from auto_html_report.domain.block import Block, BlockOutcome, BlockStatus
|
|
32
|
+
from auto_html_report.domain.errors import ConfigurationError
|
|
33
|
+
from auto_html_report.domain.report import ReportRow
|
|
34
|
+
|
|
35
|
+
#: Execution results speak about tasks; a report speaks about blocks. One mapping, so the two cannot
|
|
36
|
+
#: drift and neither module has to know the other's enum.
|
|
37
|
+
_STATUS: dict[ExecutionStatus, BlockStatus] = {
|
|
38
|
+
ExecutionStatus.OK: BlockStatus.OK,
|
|
39
|
+
ExecutionStatus.FAILED: BlockStatus.FAILED,
|
|
40
|
+
ExecutionStatus.TIMED_OUT: BlockStatus.TIMED_OUT,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def collect_blocks(
|
|
45
|
+
blocks: Sequence[Block[Any]],
|
|
46
|
+
*,
|
|
47
|
+
context: ReportContext,
|
|
48
|
+
report_title: str = "",
|
|
49
|
+
generated_at: datetime | None = None,
|
|
50
|
+
) -> tuple[ReportRow, ...]:
|
|
51
|
+
"""Collect and render every block concurrently; return rows in argument order.
|
|
52
|
+
|
|
53
|
+
Never raises for a block-level failure: such a row carries a ``FAILED`` or ``TIMED_OUT`` outcome and
|
|
54
|
+
an empty ``html``, which the renderer turns into an error cell. Raises :class:`ConfigurationError`
|
|
55
|
+
for an invalid block set, because that is a programming error rather than a data condition.
|
|
56
|
+
|
|
57
|
+
``report_title`` is offered to blocks through their ``RenderContext``. It defaults to empty because
|
|
58
|
+
collection is also usable outside a report definition, but a report should always pass it.
|
|
59
|
+
|
|
60
|
+
``generated_at`` is the instant the report is stamped with, and it reaches every block's
|
|
61
|
+
``RenderContext``. Pass the same value the :class:`Report` will carry so that a block rendering it
|
|
62
|
+
agrees with the document header; it defaults to the clock's current reading for standalone use.
|
|
63
|
+
"""
|
|
64
|
+
_validate(blocks)
|
|
65
|
+
|
|
66
|
+
render_context = RenderContext(
|
|
67
|
+
generated_at=generated_at if generated_at is not None else context.clock.now(),
|
|
68
|
+
report_title=report_title,
|
|
69
|
+
)
|
|
70
|
+
results = execute_tasks(
|
|
71
|
+
[_task(block, context, render_context) for block in blocks],
|
|
72
|
+
settings=context.settings,
|
|
73
|
+
clock=context.clock,
|
|
74
|
+
logger=context.logger,
|
|
75
|
+
run_id=context.run.run_id,
|
|
76
|
+
vocabulary=BLOCK_EXECUTION,
|
|
77
|
+
on_success=_finish_logger(context),
|
|
78
|
+
)
|
|
79
|
+
return tuple(_row(block, result) for block, result in zip(blocks, results, strict=True))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _task(
|
|
83
|
+
block: Block[Any], context: ReportContext, render_context: RenderContext
|
|
84
|
+
) -> Task[Any, str]:
|
|
85
|
+
"""One block as a task: ``collect()`` per attempt, ``render()`` once on success.
|
|
86
|
+
|
|
87
|
+
Named closures rather than lambdas: mypy cannot infer the callable types through ``Task``'s two type
|
|
88
|
+
parameters, and a function with annotations is clearer than a lambda with a cast. Each call creates
|
|
89
|
+
its own closures over ``block``, so the late-binding trap a comprehension would set does not exist.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def attempt(_attempt_number: int) -> Any:
|
|
93
|
+
return block.collect(context)
|
|
94
|
+
|
|
95
|
+
def finalise(data: Any, _attempt_number: int) -> str:
|
|
96
|
+
return block.render(data, render_context)
|
|
97
|
+
|
|
98
|
+
return Task(
|
|
99
|
+
name=block.name,
|
|
100
|
+
attempt=attempt,
|
|
101
|
+
finalise=finalise,
|
|
102
|
+
timeout_seconds=block.timeout_seconds,
|
|
103
|
+
max_retries=block.max_retries,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _finish_logger(
|
|
108
|
+
context: ReportContext,
|
|
109
|
+
) -> Callable[[str, ExecutionResult[Any]], None]:
|
|
110
|
+
"""The success log line, emitted from the worker the moment a block finishes."""
|
|
111
|
+
|
|
112
|
+
def log_finish(name: str, result: ExecutionResult[Any]) -> None:
|
|
113
|
+
context.logger.info(
|
|
114
|
+
"block.finish",
|
|
115
|
+
extra={
|
|
116
|
+
"run_id": context.run.run_id,
|
|
117
|
+
"block": name,
|
|
118
|
+
"status": BlockStatus.OK.value,
|
|
119
|
+
"duration_ms": round(result.duration_seconds * 1000, 1),
|
|
120
|
+
"attempts": result.attempts,
|
|
121
|
+
},
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
return log_finish
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _row(block: Block[Any], result: ExecutionResult[Any]) -> ReportRow:
|
|
128
|
+
"""One execution result as a row. A failed block's cell is the renderer's job."""
|
|
129
|
+
outcome = BlockOutcome(
|
|
130
|
+
name=block.name,
|
|
131
|
+
status=_STATUS[result.status],
|
|
132
|
+
duration_seconds=result.duration_seconds,
|
|
133
|
+
attempts=result.attempts,
|
|
134
|
+
error_type=result.error_type,
|
|
135
|
+
error_message=result.error_message,
|
|
136
|
+
)
|
|
137
|
+
html = result.value if result.value is not None else ""
|
|
138
|
+
return ReportRow(item=block.name, html=html, outcome=outcome)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _validate(blocks: Sequence[Block[Any]]) -> None:
|
|
142
|
+
"""Reject the block sets that cannot produce a sensible report.
|
|
143
|
+
|
|
144
|
+
The per-block overrides (``timeout_seconds``, ``max_retries``) and the settings bounds are checked
|
|
145
|
+
by :func:`~auto_html_report.application.execution.execute_tasks`, which is where those values are used.
|
|
146
|
+
"""
|
|
147
|
+
if not blocks:
|
|
148
|
+
raise ConfigurationError("a report must contain at least one block")
|
|
149
|
+
|
|
150
|
+
seen: set[str] = set()
|
|
151
|
+
duplicates: set[str] = set()
|
|
152
|
+
for block in blocks:
|
|
153
|
+
if block.name in seen:
|
|
154
|
+
duplicates.add(block.name)
|
|
155
|
+
seen.add(block.name)
|
|
156
|
+
|
|
157
|
+
if duplicates:
|
|
158
|
+
raise ConfigurationError(
|
|
159
|
+
f"duplicate block names: {sorted(duplicates)}. Names fill the Item column, so a reader "
|
|
160
|
+
"cannot tell duplicate rows apart; pass name=... to distinguish repeated blocks."
|
|
161
|
+
)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""The frozen contexts handed to blocks, discoverers and jobs.
|
|
2
|
+
|
|
3
|
+
These live here rather than in ``domain/`` because they bundle injected dependencies -- ``Settings``
|
|
4
|
+
from the infrastructure layer, and the ``HttpClient`` and ``Clock`` ports. ``domain/block.py`` names
|
|
5
|
+
two of them in its method signatures, importing them under ``TYPE_CHECKING`` only, so the domain
|
|
6
|
+
layer keeps no *runtime* dependency on this module. See ``docs/01-overview.md`` §4 rule 1 and
|
|
7
|
+
``docs/03-report-api.md`` §5.
|
|
8
|
+
|
|
9
|
+
All four are frozen, and therefore safe to share with every worker thread in a report. That is a
|
|
10
|
+
contract, not an accident: anything mutable added here would need to be thread-safe and documented
|
|
11
|
+
as such. The v1 answer is "don't" -- use per-instance state on the block, which the framework
|
|
12
|
+
guarantees is used by one thread at a time.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from datetime import datetime
|
|
20
|
+
|
|
21
|
+
from auto_html_report.application.ports import Clock, HttpClient
|
|
22
|
+
from auto_html_report.infrastructure.settings import Settings
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True, slots=True)
|
|
26
|
+
class RunMetadata:
|
|
27
|
+
"""Identity of one job run, generated once per context."""
|
|
28
|
+
|
|
29
|
+
run_id: str
|
|
30
|
+
started_at: datetime
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True, slots=True)
|
|
34
|
+
class ReportContext:
|
|
35
|
+
"""Everything a block's ``collect()`` is allowed to depend on."""
|
|
36
|
+
|
|
37
|
+
settings: Settings
|
|
38
|
+
http: HttpClient
|
|
39
|
+
logger: logging.Logger
|
|
40
|
+
clock: Clock
|
|
41
|
+
run: RunMetadata
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True, slots=True)
|
|
45
|
+
class RenderContext:
|
|
46
|
+
"""What ``render()`` is allowed to touch.
|
|
47
|
+
|
|
48
|
+
Slimmer than :class:`ReportContext` on purpose: no HTTP, no store, and **no logger**, because
|
|
49
|
+
``render()`` is pure and logging is a side effect. A block that needs to log does it in
|
|
50
|
+
``collect()``.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
generated_at: datetime
|
|
54
|
+
report_title: str
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True, slots=True)
|
|
58
|
+
class DiscoveryContext:
|
|
59
|
+
"""Everything a discoverer's ``discover()`` is allowed to depend on."""
|
|
60
|
+
|
|
61
|
+
settings: Settings
|
|
62
|
+
http: HttpClient
|
|
63
|
+
logger: logging.Logger
|
|
64
|
+
clock: Clock
|
|
65
|
+
run: RunMetadata
|