1"""Named Telegram destination metadata store.
5- oc_connect.audit (timestamp, utc_now - the created_at_utc stamp of a destination)
6- oc_connect.workspace (state_dir - the workspace-anchored directory of the destination store)
7- pydantic (BaseModel, ConfigDict - the frozen strict TelegramDestination model)
9from __future__
import annotations
13import secrets
as _secrets
14from collections.abc
import Callable
15from datetime
import datetime
16from pathlib
import Path
18from pydantic
import BaseModel, ConfigDict
20from ...audit
import timestamp, utc_now
21from ...workspace
import state_dir
23DESTINATION_FILENAME =
"destinations.json"
27 """Return the workspace-anchored destination store path (CONNECT-02 ยง7.1)."""
28 return state_dir(root) / DESTINATION_FILENAME
32 """Pinned operator identity and chat for one named Telegram target."""
34 model_config = ConfigDict(frozen=
True, extra=
"forbid", strict=
True)
39 operator_account_id: str
45 """The destination store is unreadable or malformed."""
49 """Atomic JSON store for non-secret destination metadata."""
53 path: str | Path |
None =
None,
55 now: Callable[[], datetime] = utc_now,
56 root: str | Path |
None =
None,
61 def _load(self) -> dict[str, TelegramDestination]:
62 if not self.
path.is_file():
65 payload = json.loads(self.
path.read_text(encoding=
"utf-8"))
67 destination.name: destination
69 TelegramDestination.model_validate(raw)
70 for raw
in payload.get(
"destinations", [])
73 except (OSError, UnicodeDecodeError, json.JSONDecodeError, TypeError, ValueError)
as exc:
75 f
"could not parse Telegram destinations at {self.path} "
76 f
"({type(exc).__name__})"
79 def _write(self, destinations: dict[str, TelegramDestination]) ->
None:
80 self.
path.parent.mkdir(parents=
True, exist_ok=
True)
81 temporary = self.
path.with_name(
82 f
".tmp-{self.path.name}-{_secrets.token_hex(8)}"
87 destinations[key].model_dump(mode=
"json")
88 for key
in sorted(destinations)
92 with temporary.open(
"w", encoding=
"utf-8", newline=
"\n")
as handle:
93 json.dump(payload, handle, indent=2, sort_keys=
True)
96 os.fsync(handle.fileno())
97 os.replace(temporary, self.
path)
99 if temporary.exists():
110 operator_account_id: str,
111 credential_name: str,
112 ) -> TelegramDestination:
113 """Create or replace one named destination."""
114 fields = (name, chat_id, operator_account_id, credential_name)
115 if not all(value.strip()
for value
in fields):
116 raise ValueError(
"destination fields must not be blank")
119 connector=
"telegram",
121 operator_account_id=operator_account_id,
122 credential_name=credential_name,
123 created_at_utc=timestamp(self.
_now()),
125 destinations = self.
_load()
126 destinations[name] = destination
130 def get(self, name: str) -> TelegramDestination:
131 """Return a named destination or fail with its public name."""
133 return self.
_load()[name]
135 raise KeyError(f
"Telegram destination {name!r} is not configured")
from None
138 """Return destinations in stable name order."""
139 destinations = self.
_load()
140 return tuple(destinations[key]
for key
in sorted(destinations))
144 "DESTINATION_FILENAME",
145 "DestinationFileError",
147 "TelegramDestination",
148 "default_destination_path",
TelegramDestination record(self, *, str name, str chat_id, str operator_account_id, str credential_name)
None __init__(self, str|Path|None path=None, *, Callable[[], datetime] now=utc_now, str|Path|None root=None)
TelegramDestination get(self, str name)
None _write(self, dict[str, TelegramDestination] destinations)
tuple[TelegramDestination,...] list_destinations(self)
dict[str, TelegramDestination] _load(self)
Path default_destination_path(str|Path|None root=None)