GOLDED FOR PYTHONSource on GitHub ↗
CORE API REFERENCE1.2.0 WORKING SOURCES
Old formats. Shared values.

API reference for golded-ftn

This reference covers all 40 names exported by golded_ftn in the 1.2.0 working tree. Import public values and functions from golded_ftn. Core owns values, protocols and pure text helpers. Concrete format packages own files, serialization, record validation and writer sessions.

The distribution is golded-ftn, the import is golded_ftn, and Python 3.12+ is required. There are no runtime dependencies. See PHP API mapping for migration differences and the shared writer guide for concrete MSG, JAM, Squish and Hudson examples in the sibling checkout.

01

Value conventions

All dataclass constructors are keyword-only. Values are frozen and slotted. The tables below give every constructor field, its type and its default. Required means the constructor has no default. Core annotations describe the contract; they do not validate every value at runtime. Format writers check encoded lengths, numeric ranges, representable fields and date restrictions.

Strings are Unicode. Source bytes stay separate. Dates are datetime values; core does not infer timezones or convert timestamps. Unknown metadata is None. An empty string, 0, (), None and UNSET have different meanings.

MessageControlLines and OutgoingMessage convert their collection fields to tuples during construction. Other models require callers to pass their declared types; freezing a model does not recursively freeze incorrectly typed inputs.

02

Addresses and controls

FtnAddress

API signature
FtnAddress(*, zone: int, net: int, node: int,
           point: int | None = None, domain: str | None = None)
FtnAddress.from_string(value: str) -> FtnAddress
FtnAddress.try_from_string(value: str) -> FtnAddress | None
str(address) -> str
Field Type Default Meaning
zone int Required Network zone.
net int Required Network number.
node int Required Node number.
point int | None None Optional point, including explicit point 0.
domain str | None None Optional domain, with original case retained.

The parsers accept zone:net/node, optional .point and optional @domain, with surrounding whitespace stripped. Numeric parts contain ASCII digits only. Domains begin with an ASCII letter or digit and then allow letters, digits, periods, underscores and hyphens. Parsing accepts arbitrary non-negative integer sizes; it does not enforce a format's 16-bit address limits. Direct construction does not enforce this grammar.

from_string raises ValueError for invalid syntax. try_from_string returns None. str emits the full address and includes .0 when point is explicitly zero. It does not normalize domain case or remove an explicit point.

ControlLine

API signature
ControlLine(*, name: str, value: str, raw: str)
Field Type Default Meaning
name str Required Kludge name; the parser uppercases it.
value str Required Parsed value; the parser trims surrounding whitespace.
raw str Required Original decoded line, without its line separator.

raw includes any trailing nulls on the loaded line. It is decoded text, not original source bytes. For newly supplied writer controls, use raw=""; concrete serializers use structured names and values and validate them.

MessageControlLines

API signature
MessageControlLines(*, kludges=(), msgid=None, reply=None, charset=None,
                    seen_by=(), path=(), tearline=None, origin=None,
                    origin_address=None)
Field Type Default Meaning
kludges tuple[ControlLine, ...] () Valid SOH kludges, including unknown names and repetitions, in input order.
msgid str | None None First parsed MSGID value, including an empty value.
reply str | None None First parsed REPLY value; this is external metadata.
charset str | None None First CHRS or CHARSET value, including its level suffix.
seen_by tuple[str, ...] () Unexpanded SEEN-BY values.
path tuple[str, ...] () Unexpanded plain PATH values. See parser limitation below.
tearline str | None None First unquoted line beginning ---.
origin str | None None First recognized origin text, without the Origin: prefix.
origin_address FtnAddress | None None First valid parenthesized address at the end of a recognized origin line.

kludges, seen_by and path are converted to tuples. Direct construction does not reconcile convenience fields with the contents of kludges.

parse_message

API signature
parse_message(text: str) -> MessageControlLines

Splits CRLF, CR and LF without changing the supplied string. Parsing ignores empty lines and quoted lines beginning with optional whitespace followed by > or one to six ASCII letters/digits followed by >. It ignores trailing nulls when interpreting a line but retains them in ControlLine.raw.

A kludge must start at column zero with SOH (\x01) and use NAME: value. Names begin with an ASCII letter and then allow ASCII letters, digits and hyphens. Names are uppercased; first MSGID, REPLY and charset values win. Valid unknown kludges and repetitions remain in kludges. There is no conflict validation here: a writer may reject conflicting declarations.

Plain SEEN-BY: and PATH: lines are case-sensitive and have their surrounding value whitespace stripped. The current parser keeps SOH \x01PATH: in kludges but does not add it to path. Space-separated SOH controls such as \x01INTL 2:1/2 2:1/3 are not parsed by this core helper; concrete readers may extend its syntax. Routing strings are never expanded or deduplicated.

Origin recognition requires a leading whitespace character followed by * Origin:. origin keeps the first recognized origin text; a later recognized origin line can supply origin_address if earlier lines had no valid address. Ordinary body lines are not returned separately by this function.

extract_msgid

API signature
extract_msgid(text: str) -> str | None

Returns parse_message(text).msgid. It uses exactly the same quoting and syntax rules, including first-value precedence and an empty string as a valid result. It does not generate an ID or validate uniqueness.

Python · RUNNABLE
from golded_ftn import FtnAddress, extract_msgid, parse_message

address = FtnAddress.from_string(" 2:236/77.0@fidonet ")
assert str(address) == "2:236/77.0@fidonet"
assert FtnAddress.try_from_string("not an address") is None
text = "> \x01MSGID: quoted\r\x01msgid: actual\r\x01X-CUSTOM: value\0"
controls = parse_message(text)
assert extract_msgid(text) == "actual"
assert controls.kludges[-1].name == "X-CUSTOM"
assert controls.kludges[-1].raw.endswith("\0")
assert parse_message("SEEN-BY: 1/2 3\nPATH: 1/4").path == ("1/4",)
03

Decoding and text helpers

DecodeErrors

API signature
DecodeErrors = Literal["strict", "replace", "ignore"]

Type alias for to_utf8's error policy. strict raises on malformed input; replace inserts replacement characters; ignore discards undecodable bytes. Neither lossy mode retains the original bytes for you.

detect_charset

API signature
detect_charset(raw_body: bytes, fallback: str = "CP850") -> str

Finds the first case-insensitive SOH CHRS: or CHARSET: declaration anywhere in the byte string. The token ends at whitespace, null or another SOH. Detection does not apply the text parser's quoted-line rules. A recognized FTN alias is resolved; an absent or unknown declaration uses fallback. This is declaration lookup, not statistical encoding detection, and it does not reject conflicting later declarations.

The fallback is always resolved and validated through Python's codec registry, even when the message has a recognized declaration. Invalid fallbacks raise LookupError. Supported declaration aliases, matched case-insensitively:

Aliases Returned encoding
CP850, IBM850, IBMPC, IBM CP850
LATIN-1, LATIN1, 8859-1, ISO-8859-1, ISO8859-1 ISO-8859-1
ASCII, USASCII ASCII
CP866, IBM866, CP-866, +7FIDO, +7_FIDO, FIDO7, FIDO_7, RUS CP866
KOI8-R, KOI8R, KOI, KOI8, GOST, CP20866 KOI8-R
KOI8-U, KOI8U, KOU, KOI-U, CP21866 KOI8-U
CP1125, UKR CP1125
CP437, IBM437 CP437
CP1251, WIN, WIN-1251, WINDOWS-1251, CP-1251 CP1251
CP1252 CP1252
CP1250 CP1250
LATIN-2, ISO-8859-2 ISO-8859-2
UTF-8, UTF8 UTF-8

The historical Cyrillic aliases and KOI8-U/CP1125 declaration support above were added in 1.2.2. CP850 remains the default, including IBMPC. RFC Content-Type does not participate in body-charset detection.

Fallbacks additionally accept names understood by Python's codec registry. The returned spelling is an alias target or the validated fallback spelling, not necessarily the codec registry's canonical name.

to_utf8

API signature
to_utf8(value: bytes, charset: str = "CP850", *,
        errors: DecodeErrors = "strict") -> str

Removes trailing null bytes, then decodes with charset into Python Unicode. Despite its name, the result is a str, not UTF-8 bytes. Interior nulls remain. The charset is passed directly to Python's codec registry: resolve FTN aliases with detect_charset first. Unknown codec names raise LookupError; malformed bytes raise UnicodeDecodeError in strict mode. An unsupported error policy raises ValueError.

parse_body

API signature
parse_body(text: str) -> str

Removes trailing null characters and normalizes CRLF and CR to LF. It preserves interior nulls, controls, routing and ordinary text. It does not decode, extract metadata or repair mojibake.

read_null_padded_field

API signature
read_null_padded_field(raw: bytes, offset: int, length: int) -> bytes

Takes the requested byte slice and returns its bytes before the first null. Negative offset or length raises ValueError. A slice beyond the buffer is short or empty; this helper does not enforce binary record bounds. Validate record size before interpreting fixed fields.

synthetic_id

API signature
synthetic_id(from_name: str, to_name: str, subject: str,
             date: str | None, body: str) -> str

Returns hash:sha256: plus 64 lowercase hexadecimal digits. The digest covers the exact UTF-8 encoding of JSON [from_name, to_name, subject, date, body], with ensure_ascii=False and separators (",", ":"). The complete body is included. Names, text and date are not normalized. None and "" dates differ. Consumers choose their date string format.

This is a content-derived ID, separate from source record identity and revision. It does not prove that two source records are the same. Concrete writers do not automatically generate MSGID, and a synthetic ID must not be supplied as an external MSGID.

Python · RUNNABLE
from golded_ftn import (
    detect_charset,
    parse_body,
    read_null_padded_field,
    synthetic_id,
    to_utf8,
)

raw = b"\x01CHRS: IBMPC 2\rBruger m\x9bde\0\0"
charset = detect_charset(raw)
assert charset == "CP850"
assert parse_body(to_utf8(raw, charset)).endswith("Bruger møde")
assert read_null_padded_field(b"xxAlice\0padding", 2, 13) == b"Alice"
assert read_null_padded_field(b"x", 8, 2) == b""
assert parse_body("a\0b\r\n\0") == "a\0b\n"
assert to_utf8(b"\xff", "UTF-8", errors="replace") == "\ufffd"
identity = synthetic_id("Alice", "Bob", "Ping", None, "body")
assert identity.startswith("hash:sha256:") and len(identity) == 76
assert identity != synthetic_id("Alice", "Bob", "Ping", "", "body")
04

Optional text repair

MojibakeRepairResult

API signature
MojibakeRepairResult(*, text: str, changed: bool, confidence: float)
Field Type Default Meaning
text str Required Repaired text, with LF line endings.
changed bool Required At least one line was repaired.
confidence float Required Heuristic score for the returned repair.

The score is not a probability. Returned scores lie between 0 and 1; direct construction does not validate that range.

repair_mojibake

API signature
repair_mojibake(text: str, declared_charset: str | None = None,
               prefer_quoted_lines: bool = True) -> MojibakeRepairResult

Attempts RFC 2047 decoding and strict encoding round trips per line. It scores damage markers, plausible European letters and a small word list. Default visible encodings are CP850, CP437, CP865, ISO-8859-1 and Windows-1252; intended encodings are UTF-8, ISO-8859-1, Windows-1252 and CP850. A valid first token of declared_charset, including an FTN alias, adds a candidate. Unknown declarations fall back to the default candidates.

prefer_quoted_lines=True lowers the score threshold for matching quote lines; it does not mean only quoted lines are examined. Candidates that cannot encode and decode strictly are discarded. Malformed RFC 2047 words remain unchanged when decoding fails. Version 1.2.2 safeguards allow Latin-1 fallback only for MIME words declared ASCII/US-ASCII, while rejecting output that introduces C0 controls (except tab), DEL, C1 controls or replacement characters. Repair always returns a new result and never modifies source data.

Version 1.2.2 repair safeguards preserve graphic frames, block art, already-correct Danish/German words and symbol-only noise. Isolated ┼/▀ inside words can still represent damaged letters, and strict UTF-8 recovery may repair graphic-looking bytes such as m├©de; these are contextual heuristics. Supported damaged words can be repaired beside correct words. Recognizable uuencode data lines and PGP armour regions are excluded. Pass complete text to retain PGP context; separate per-line calls cannot retain that state.

Literal degree signs are protected after an ASCII digit or before uppercase C/F followed by a non-letter or end of line. Spaces and tabs are allowed between the digit/sign/unit; the context never crosses a line break. Thus m°de at 20 °C becomes møde at 20 °C, and SÃ¥dan at 10° becomes Sådan at 10°. Other degree-shaped damage, such as °l, remains repairable.

CRLF and CR are normalized to LF even when changed is false. confidence is the mean of line scores, including zero scores for unchanged lines, capped at 1. This is an opt-in heuristic tuned to European-language fixtures, not a general detector. Protected regions also undergo LF normalization, so output is not a byte-preserving copy or a signature-verification input. Readers and writers do not invoke it automatically.

Cyrillic charset support means that bytes labelled CP866, KOI8-R, CP1251, KOI8-U or CP1125 can be decoded with the selected codec. It does not establish that repair_mojibake can recover Russian or Ukrainian text already decoded with the wrong codec. The scorer is tuned to Western European text; Cyrillic repair has not been evaluated against a representative corpus. Such an evaluation must check damaged text with known correct originals, unchanged clean text, mixed scripts and graphic/payload preservation before claiming support.

Python · RUNNABLE
from golded_ftn import repair_mojibake

result = repair_mojibake("Bruger m°de")
assert result.text == "Bruger møde" and result.changed
assert 0 < result.confidence <= 1
plain = repair_mojibake("a\r\nb")
assert plain.text == "a\nb" and not plain.changed
assert plain.confidence == 0
05

Read values and options

MessageProvenance

API signature
MessageProvenance(*, source_type: str, source_path=None,
                  source_id=None, source_offset=None)
Field Type Default Meaning
source_type str Required Source format or source kind.
source_path str | None None Actual source path when known.
source_id str | None None Format-specific record identity when known.
source_offset int | None None Physical byte offset when known.

Provenance records where a value was read. It is separate from external MSGID and does not itself provide a writer revision or permission to mutate a base.

MessageSource

API signature
MessageSource(*, source_type: str, path: str, code: str, name: str,
              sort_order: int = 0, meta_key: str | None = None)
Field Type Default Meaning
source_type str Required Format selected by a concrete catalog.
path str Required Catalog source path.
code str Required Area code.
name str Required Area display name.
sort_order int 0 Catalog ordering value.
meta_key str | None None Optional consumer metadata key.

Core does not discover paths or interpret meta_key.

ParsedArea

API signature
ParsedArea(*, code: str, name: str, source_type: str,
           sort_order: int = 0, meta_key: str | None = None)
Field Type Default Meaning
code str Required Area code.
name str Required Area display name.
source_type str Required Source format or source kind.
sort_order int 0 Area ordering value.
meta_key str | None None Optional consumer metadata key.

Area metadata has no path field. Use MessageSource for a located source.

ParsedMessage

API signature
ParsedMessage(*, msgno: int, from_name: str, to_name: str, subject: str,
              body_text: str, attributes_raw: int, posted_at=None,
              external_id=None, from_address=None, to_address=None,
              reply_to_msgno=None, reply1st_msgno=None, reply_next_msgno=None,
              area_code=None, area_name=None, area_sort_order=None,
              area_meta_key=None, control_lines=None, provenance=None)
Field Type Default Meaning
msgno int Required Format-specific message number or Squish UID.
from_name str Required Decoded sender name.
to_name str Required Decoded recipient name.
subject str Required Decoded subject.
body_text str Required Decoded body; concrete formats decide whether header controls are included.
attributes_raw int Required Raw format-specific attribute bits.
posted_at datetime | None None Parsed posting time when known.
external_id str | None None Reader-supplied ID; a concrete reader may use a synthetic fallback.
from_address str | None None Parsed sender address as text.
to_address str | None None Parsed recipient address as text.
reply_to_msgno int | None None Format-specific parent message link.
reply1st_msgno int | None None Format-specific first reply link.
reply_next_msgno int | None None Format-specific next reply link.
area_code str | None None Area code when supplied by the reader.
area_name str | None None Area name when supplied by the reader.
area_sort_order int | None None Area ordering value when supplied.
area_meta_key str | None None Consumer metadata key when supplied.
control_lines MessageControlLines | None None Parsed controls and routing when supplied.
provenance MessageProvenance | None None Source location when supplied.

This decoded view does not contain all raw headers, unknown binary metadata or the complete Squish reply list. Writers update from raw records under their lock; rebuilding an existing record from ParsedMessage can lose metadata.

ReaderIssue

API signature
ReaderIssue(*, source_type: str, source_path: str,
            action: Literal["recovered", "skipped", "stopped"],
            code: str, detail: str, source_id=None, source_offset=None)
Field Type Default Meaning
source_type str Required Source format or source kind.
source_path str Required Actual source file path.
action Literal["recovered", "skipped", "stopped"] Required What the concrete reader did.
code str Required Machine-readable issue code defined by the format reader.
detail str Required Explanation without message contents.
source_id str | None None Record identity when known.
source_offset int | None None Physical byte offset when known.

recovered describes an accepted deviation. skipped excludes a record. stopped means traversal is incomplete. Several issues may describe one record; an earlier recovery does not prevent a later skip. Core does not enforce issue codes or sanitize a directly supplied detail string.

ReaderOptions

API signature
ReaderOptions(*, fallback_charset: str = "CP850", archive_mode: bool = False,
              on_issue: Callable[[ReaderIssue], None] | None = None)
Field Type Default Meaning
fallback_charset str "CP850" Charset used when no recognized declaration is available.
archive_mode bool False Explicitly permit a concrete reader's documented recoveries.
on_issue Callable[[ReaderIssue], None] | None None Callback receiving archive deviations.

archive_mode=True without on_issue raises ValueError at construction. The constructor does not validate the charset. Readers validate it when used. Strict mode is the default. Supplying a callback alone does not enable recovery. Filesystem and callback exceptions propagate; archive mode does not suppress them. Each format defines its permitted recovery steps. Archive reads do not authorize editing a damaged base.

Python · RUNNABLE
from golded_ftn import ReaderIssue, ReaderOptions

issues: list[ReaderIssue] = []
options = ReaderOptions(archive_mode=True, on_issue=issues.append)
assert options.fallback_charset == "CP850"
assert options.on_issue is not None
options.on_issue(
    ReaderIssue(
        source_type="msg",
        source_path="area/1.MSG",
        source_id="1",
        source_offset=190,
        action="skipped",
        code="example",
        detail="Record cannot be decoded.",
    )
)
assert issues[0].action == "skipped"
06

Write values, patches and revisions

OutgoingMessage

API signature
OutgoingMessage(*, from_name: str, to_name: str, subject: str, body_text: str,
                external_id=None, from_address=None, to_address=None,
                posted_at=None, attributes_raw=None, control_lines=(),
                provenance=None, reply_to_msgno=None, reply1st_msgno=None,
                reply_next_msgno=None, reply_list=(), routing_seen_by=(),
                routing_path=())
Field Type Default Meaning
from_name str Required Sender name.
to_name str Required Recipient name.
subject str Required Subject.
body_text str Required Unicode message body.
external_id str | None None Explicit external MSGID, if supplied.
from_address FtnAddress | None None Structured sender address.
to_address FtnAddress | None None Structured recipient address.
posted_at datetime | None None Posting date; concrete writers enforce precision/range/timezone rules.
attributes_raw int | None None Raw format-specific attribute bits; concrete writers choose their default.
control_lines tuple[ControlLine, ...] () Structured controls to serialize.
provenance MessageProvenance | None None Caller-supplied provenance; serialization support is format-specific.
reply_to_msgno int | None None Format-specific parent link.
reply1st_msgno int | None None Format-specific first reply link.
reply_next_msgno int | None None Format-specific next reply link.
reply_list tuple[int, ...] () Complete reply UID list for a format that represents one.
routing_seen_by tuple[str, ...] () Explicit unexpanded SEEN-BY values.
routing_path tuple[str, ...] () Explicit unexpanded PATH values.

Collection fields are converted to tuples. Core does not generate MSGID, route messages, detect duplicates or validate that replies exist. Concrete formats reject unrepresentable fields and contradictory metadata. Numeric reply links are independent of external MSGID and REPLY controls.

WriterOptions

API signature
WriterOptions(*, target_charset: str = "CP850", lock_timeout: float = 5.0,
              concurrent: bool = False)
Field Type Default Meaning
target_charset str "CP850" Encoding for values that require serialization.
lock_timeout float 5.0 Maximum lock wait in seconds, measured with a monotonic clock.
concurrent bool False Request supported concurrent GoldED access.

Negative, infinite or NaN lock_timeout raises ValueError at construction. Zero requests an immediate lock attempt. The constructor does not validate the codec; concrete writers do so during serialization. Encoding is strict, with no silent truncation or replacement characters. Conflicting charset metadata is rejected. Pure attribute changes preserve existing text bytes.

concurrent=True is a request, not permission or a safety guarantee. Current MSG, JAM, Squish and Hudson writers reject it. GoldED must remain closed during offline editing. Live support requires format, platform and build-specific competing writer, reader and refresh tests. Windows starts with offline use.

Unset

API signature
Unset()

Marker type used in patch annotations. It has no fields and is not a dataclass. Use the exported singleton UNSET rather than creating another Unset(); concrete writers can use identity comparisons with that singleton.

UNSET

API signature
UNSET: Unset

The singleton default for every patch field. It means leave the stored field untouched. It is different from None, which explicitly requests clearing.

MessagePatch

API signature
MessagePatch(*, from_name=UNSET, to_name=UNSET, subject=UNSET,
             body_text=UNSET, external_id=UNSET, from_address=UNSET,
             to_address=UNSET, posted_at=UNSET, attributes_raw=UNSET,
             control_lines=UNSET, provenance=UNSET, reply_to_msgno=UNSET,
             reply1st_msgno=UNSET, reply_next_msgno=UNSET, reply_list=UNSET,
             routing_seen_by=UNSET, routing_path=UNSET)

Every field defaults to UNSET. Each accepts None as a clear request; the concrete writer rejects clearing fields it cannot represent. Required names and body generally need a string, including "" when allowed, rather than None. Core does not validate or convert patch collections.

Field Type Default
from_name str | None | Unset UNSET
to_name str | None | Unset UNSET
subject str | None | Unset UNSET
body_text str | None | Unset UNSET
external_id str | None | Unset UNSET
from_address FtnAddress | None | Unset UNSET
to_address FtnAddress | None | Unset UNSET
posted_at datetime | None | Unset UNSET
attributes_raw int | None | Unset UNSET
control_lines tuple[ControlLine, ...] | None | Unset UNSET
provenance MessageProvenance | None | Unset UNSET
reply_to_msgno int | None | Unset UNSET
reply1st_msgno int | None | Unset UNSET
reply_next_msgno int | None | Unset UNSET
reply_list tuple[int, ...] | None | Unset UNSET
routing_seen_by tuple[str, ...] | None | Unset UNSET
routing_path tuple[str, ...] | None | Unset UNSET

Names have the same meaning as in OutgoingMessage. An omitted field preserves existing raw metadata, including reply structures the decoded model cannot fully display. Explicit control/routing replacements follow the concrete format's representation rules. Deleting a message does not rewrite other messages' reply links, and message numbers are not renumbered.

MessageIdentity

API signature
MessageIdentity(*, format: str, base: str, msgno: int,
                board: int | None = None)
Field Type Default Meaning
format str Required Concrete format tag, such as msg, jam, squish or hudson.
base str Required Base identity selected by the concrete writer.
msgno int Required MSG file number, JAM message number, Squish UID or Hudson message number.
board int | None None Explicit Hudson board where applicable.

Use identities returned by sessions. Core does not canonicalize base paths or validate board/number ranges. External MSGID is not part of this identity.

RevisionToken

API signature
RevisionToken(*, identity: MessageIdentity, location: tuple[int, ...], digest: str)
Field Type Default Meaning
identity MessageIdentity Required Target record identity.
location tuple[int, ...] Required Physical record placement defined by the concrete format.
digest str Required SHA-256 hexadecimal digest of raw message header, metadata and text.

A session issues this token from a consistent read and checks it again under the operation lock. Global base counters are not message revisions. Changes to other messages do not invalidate a token. Treat the entire token as opaque; core does not validate a manually constructed digest or location. It is an optimistic concurrency check, not authentication or a permanent message ID.

WriteResult

API signature
WriteResult(*, identity: MessageIdentity, revision: RevisionToken)
Field Type Default Meaning
identity MessageIdentity Required Identity of the appended or updated message.
revision RevisionToken Required Revision after that operation.

Use the returned revision for the next update/delete. An earlier revision can be stale after content or attribute changes.

SessionMessage

API signature
SessionMessage(*, message: ParsedMessage, identity: MessageIdentity,
               revision: RevisionToken)
Field Type Default Meaning
message ParsedMessage Required Decoded view from the consistent session read.
identity MessageIdentity Required Identity of that record.
revision RevisionToken Required Raw record revision read with the view.

Core does not enforce that manually constructed triples refer to the same record. Obtain them through the concrete session.

Python · RUNNABLE
from golded_ftn import UNSET, MessageIdentity, MessagePatch, OutgoingMessage

message = OutgoingMessage(
    from_name="Alice",
    to_name="Bob",
    subject="Ping",
    body_text="Hello",
    reply_to_msgno=7,
    routing_seen_by=("1/2 3",),
)
patch = MessagePatch(subject="Edited", external_id=None)
assert patch.subject == "Edited"
assert patch.external_id is None
assert patch.body_text is UNSET
record = MessageIdentity(format="hudson", base="area", board=2, msgno=7)
assert record.board == 2 and message.external_id is None
07

Protocols and session lifecycle

These are structural typing protocols. A matching concrete object can satisfy them without inheriting from the protocol. They contain method signatures, not concrete behavior. They are not marked runtime_checkable; do not use them with isinstance or instantiate them. Paths accept str | os.PathLike[str]. Concrete APIs may expose extra format-specific arguments.

MessageBaseReader

API signature
read(self, path: str | PathLike[str],
     options: ReaderOptions | None = None) -> Iterable[ParsedMessage]

Read an area through a concrete reader. None selects default reader options. The iterable may be lazy; errors can appear during iteration. A reader's plain ParsedMessage does not include a session revision for editing.

MessageSourceCatalog

API signature
sources(self, path: str | PathLike[str],
        options: ReaderOptions | None = None) -> Iterable[MessageSource]

Discover configured message sources through a concrete catalog. Ordering and path interpretation belong to that catalog. Core has no discovery implementation.

MessageSourceLocator

API signature
find(self, path: str | PathLike[str]) -> str | None

Find a source path through a concrete locator. None means no match. Search rules and the meaning of the input path belong to the concrete locator.

MessageWriter

API signature
write(self, path: str | PathLike[str], messages: Iterable[OutgoingMessage],
      options: WriterOptions | None = None) -> int

Batch-writing convenience contract returning the number written. Core does not provide a batch implementation or promise a transaction around the iterable. The MSG convenience writer commits each successful message separately; check other formats' APIs before assuming they implement this legacy method.

MessageBaseWriter

API signature
create(self, path: str | PathLike[str]) -> None
open(self, path: str | PathLike[str], options: WriterOptions | None = None)
    -> AbstractContextManager[MessageWriterSession]

create initializes an empty base and rejects existing base files. It does not open a session. open opens an existing base, with default options when omitted. Use the returned context manager to scope the session. Hudson's concrete open requires an explicit board; whole-base creation remains independent of board. Required initialization/lastread files are format-specific.

MessageWriterSession

API signature
read(self, msgno: int) -> SessionMessage
append(self, message: OutgoingMessage) -> WriteResult
update(self, identity: MessageIdentity, patch: MessagePatch,
       expected_revision: RevisionToken) -> WriteResult
delete(self, identity: MessageIdentity,
       expected_revision: RevisionToken) -> MessageIdentity

Each operation takes the format lock, reloads control data and holds the lock through validation, writing and flush. read supplies a consistent decoded view and revision. append allocates a format-specific identity; update preserves identity and returns the new revision. delete returns the deleted identity. A missing or changed update/delete target raises ConflictError.

One operation is the commit unit. Earlier successful operations survive a later failure. Ordinary write failures trigger rollback under the lock. Failed rollback makes the session unusable and reports RollbackError; reopening a session is not proof that the base is healthy. Concrete writers reject damaged bases and do not use archive recovery for editing.

Callers must not access base files directly while writer operations run. A Python writer lock alone does not establish safe coexistence with GoldED. Rollback has no guarantee for process termination or power loss, and there is no automatic detection promise for every partial write.

The following functions consume protocols. The caller supplies concrete objects; core has no file reader or writer to instantiate. MSG, JAM and Squish expose the shown writer shape. Hudson needs a board-bound adapter for this generic example, because its concrete open requires a board argument that the core protocol does not express.

Python · RUNNABLE
from golded_ftn import (
    MessageBaseReader,
    MessageBaseWriter,
    MessagePatch,
    OutgoingMessage,
    ParsedMessage,
    WriteResult,
)


def collect_messages(reader: MessageBaseReader, path: str) -> tuple[ParsedMessage, ...]:
    return tuple(reader.read(path))


def append_edit_delete(writer: MessageBaseWriter, path: str) -> WriteResult:
    writer.create(path)
    with writer.open(path) as session:
        created = session.append(
            OutgoingMessage(
                from_name="Alice",
                to_name="Bob",
                subject="Ping",
                body_text="Hello",
            )
        )
        current = session.read(created.identity.msgno)
        edited = session.update(
            current.identity,
            MessagePatch(subject="Edited"),
            current.revision,
        )
        session.delete(edited.identity, edited.revision)
        return edited
08

Exceptions

ParserException

API signature
class ParserException(RuntimeError)

Concrete readers and writer base-validation paths use this error for records they cannot parse. Concrete APIs may chain the underlying error. Filesystem failures and archive callback failures propagate separately; this is not a catch-all for every read error.

WriterError

API signature
class WriterError(RuntimeError)

Common base class for the writer-specific errors below. Concrete sessions also use it when closed or poisoned. Input validation and encoding failures can raise ValueError, LookupError or UnicodeEncodeError; filesystem/I/O failures can raise OSError. Catching only WriterError does not catch every failed write.

ConflictError

API signature
class ConflictError(WriterError)

The target is missing, belongs to another base/identity or no longer matches the expected revision. Read the current target and decide how to reconcile the change. Do not retry the same stale patch blindly. Concrete session reads also use this error for a missing message.

LockTimeoutError

API signature
class LockTimeoutError(WriterError)

The operation could not obtain the base lock within WriterOptions.lock_timeout. The operation has not begun mutating the base.

RollbackError

API signature
class RollbackError(WriterError)

An operation failed and restoring original bytes also failed. The exception reports base, operation and rollback failure. The concrete session stops accepting operations. The base may need inspection; automatic repair is outside this API.

UnsupportedOperationError

API signature
class UnsupportedOperationError(WriterError)

A requested operation or concurrency mode is unavailable. Examples include current concurrent=True requests and explicit Opus editing through the MSG writer. Unrepresentable message fields may instead raise ValueError, as documented by the concrete format.

09

Boundaries

Core does not read or write MSG, JAM, Squish or Hudson files itself. It does not implement BBS behavior, routing policy, duplicate checking, packet handling, packing, repair of damaged bases or GoldED configuration. Internal modules such as golded_ftn._writer_io are implementation details and are not public exports.

Current writer sessions are for offline editing. GoldED interoperability and concurrent reader/refresh tests are deferred; source review or passing Python tests do not establish those guarantees. Check the concrete package's format notes and the shared writer guide before choosing a format or platform.