Repository navigation
[07/10] refactor: introduce delivery package #95
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 2 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
ff31c5b
refactor: introduce delivery package
IceCodeNew a5dbf7c
fix: validate Telegram message split limits
IceCodeNew 0de21e2
test: patch reference loading at its owner
IceCodeNew 8af30ee
fix: preserve markup-only Telegram messages
IceCodeNew c8b323b
test: resolve delivery dependencies at their owners
IceCodeNew ef9da17
test: make public loader patches explicit
IceCodeNew File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| """Delivery contracts, renderers, and platform adapters.""" | ||
|
|
||
| from .base import DeliveryError, DeliveryProvider, RenderedTextDiagnostics | ||
| from .renderers import PlainTextRenderer, TelegramHTMLRenderer | ||
| from .stdout import StdoutPublisher | ||
| from .telegram import TelegramPublisher | ||
|
|
||
| __all__ = [ | ||
| "DeliveryError", | ||
| "DeliveryProvider", | ||
| "PlainTextRenderer", | ||
| "RenderedTextDiagnostics", | ||
| "StdoutPublisher", | ||
| "TelegramHTMLRenderer", | ||
| "TelegramPublisher", | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| """Platform-neutral message delivery composition.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import logging | ||
| import re | ||
| from dataclasses import dataclass | ||
| from typing import Protocol | ||
|
|
||
| from ..models import Article, BriefingResult, RenderedMessage, SourceDocument | ||
| from .renderers import MessageRenderer | ||
|
|
||
| _LOGGER = logging.getLogger("weather_briefing.publishers") | ||
| _SAFE_DELIVERY_REASON = re.compile(r"[a-z0-9]+(?:-[a-z0-9]+)*") | ||
|
|
||
|
|
||
| class Publisher(Protocol): | ||
| """Transport a rendered message to its destination.""" | ||
|
|
||
| async def publish( | ||
| self, | ||
| message: RenderedMessage, | ||
| *, | ||
| single_message: bool = False, | ||
| silent: bool = False, | ||
| ) -> None: | ||
| """Publish one rendered message with delivery hints.""" | ||
| ... | ||
|
|
||
|
|
||
| class RenderedTextDiagnostics(Protocol): | ||
| """Expose the runtime switch for sensitive rendered-text logging.""" | ||
|
|
||
| def rendered_text_logging_enabled(self) -> bool: | ||
| """Return whether sensitive rendered-text logging is enabled.""" | ||
| ... | ||
|
|
||
|
|
||
| @dataclass(frozen=True, slots=True) | ||
| class DeliveryProvider: | ||
| """Bind a platform renderer to its message transport.""" | ||
|
|
||
| renderer: MessageRenderer | ||
| publisher: Publisher | ||
| single_message_limit: int | None = None | ||
| diagnostics: RenderedTextDiagnostics | None = None | ||
|
|
||
| def briefing_limit(self, configured_limit: int) -> int: | ||
| """Clamp a configured briefing limit to the platform limit.""" | ||
| if self.single_message_limit is None: | ||
| return configured_limit | ||
| return min(configured_limit, self.single_message_limit) | ||
|
|
||
| def render_briefing( | ||
| self, | ||
| result: BriefingResult, | ||
| reference_articles: tuple[Article, ...], | ||
| context: tuple[SourceDocument, ...], | ||
| ) -> RenderedMessage: | ||
| """Render a briefing with the configured platform renderer.""" | ||
| return self.renderer.render_briefing(result, reference_articles, context) | ||
|
|
||
| async def publish_rendered( | ||
| self, | ||
| message: RenderedMessage, | ||
| *, | ||
| single_message: bool = False, | ||
| silent: bool = False, | ||
| ) -> None: | ||
| """Publish an already rendered message with delivery hints.""" | ||
| log_rendered_text(self.diagnostics, "briefing", message.body) | ||
| await self.publisher.publish(message, single_message=single_message, silent=silent) | ||
|
|
||
| async def publish_verbatim(self, article: Article, *, silent: bool = False) -> None: | ||
| """Render and publish one cleaned article without summarization.""" | ||
| message = self.renderer.render_verbatim(article) | ||
| _LOGGER.debug( | ||
| "Rendered verbatim message: visible_characters=%d payload_characters=%d", | ||
| message.visible_length, | ||
| len(message.body), | ||
| ) | ||
| log_rendered_text(self.diagnostics, "verbatim", message.body) | ||
| await self.publisher.publish(message, silent=silent) | ||
|
|
||
| async def publish_alert(self, title: str, body: str) -> None: | ||
| """Render and publish an operational alert.""" | ||
| message = self.renderer.render_alert(title, body) | ||
| log_rendered_text(self.diagnostics, "alert", message.body) | ||
| await self.publisher.publish(message) | ||
|
|
||
|
|
||
| class DeliveryError(RuntimeError): | ||
| """Raised without exposing private delivery endpoint details.""" | ||
|
|
||
| def __init__(self, message: str, *, reason: str, channel_unavailable: bool = False) -> None: | ||
| """Describe a delivery failure using a safe structured reason.""" | ||
| if not isinstance(reason, str) or _SAFE_DELIVERY_REASON.fullmatch(reason) is None: | ||
| raise ValueError("Delivery error reason must be a lowercase kebab-case label") | ||
| if not isinstance(channel_unavailable, bool): | ||
| raise TypeError("channel_unavailable must be a bool") | ||
| super().__init__(message) | ||
| self.reason = reason | ||
| self.channel_unavailable = channel_unavailable | ||
|
|
||
|
|
||
| def log_rendered_text(diagnostics: RenderedTextDiagnostics | None, stage: str, body: str) -> None: | ||
| """Log application-owned rendered text when its temporary switch is enabled.""" | ||
| if rendered_text_logging_enabled(diagnostics): | ||
| _LOGGER.debug("Sensitive rendered text diagnostic: stage=%s body=%r", stage, body) | ||
|
|
||
|
|
||
| def rendered_text_logging_enabled(diagnostics: RenderedTextDiagnostics | None) -> bool: | ||
| """Read the diagnostic switch without affecting delivery on failure.""" | ||
| if diagnostics is None: | ||
| return False | ||
| try: | ||
| enabled = diagnostics.rendered_text_logging_enabled() | ||
| except Exception: | ||
| _LOGGER.warning("Rendered text diagnostic state check failed", exc_info=True) | ||
| return False | ||
| return enabled and _LOGGER.isEnabledFor(logging.DEBUG) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.