Metadata-Version: 2.5
Name: golded-ftn
Version: 1.4.0
Summary: Shared FTN message contracts, values and text helpers
Project-URL: Repository, https://github.com/golded-dev/golded-ftn-python
Project-URL: Issues, https://github.com/golded-dev/golded-ftn-python/issues
Project-URL: Changelog, https://github.com/golded-dev/golded-ftn-python/blob/main/CHANGELOG.md
Author: GoldED.dev contributors
License-Expression: MIT
License-File: LICENSE
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Typing :: Typed
Requires-Python: >=3.12
Description-Content-Type: text/markdown

# golded-ftn

Repository: [`golded-ftn-python`](https://github.com/golded-dev/golded-ftn-python).
The distribution remains `golded-ftn`; imports use `golded_ftn`.
The source is public on GitHub. [Version 1.4.0 is available on PyPI](https://pypi.org/project/golded-ftn/1.4.0/).

Install with Python 3.12 or newer:

```sh
python -m pip install golded-ftn==1.4.0
```

Shared FTN message values, reader/writer protocols and text helpers for Python
3.12+. No runtime dependencies. No concrete format readers, writers, database
access or Nornir integration.

For development, clone the repository, then build and install a local wheel:

```sh
git clone https://github.com/golded-dev/golded-ftn-python.git
cd golded-ftn-python
uv sync --locked
uv build
python -m pip install dist/golded_ftn-1.4.0-py3-none-any.whl
```

```python
from golded_ftn import FtnAddress, detect_charset, parse_body, parse_message, to_utf8

address = FtnAddress.from_string("2:236/77.1@fidonet")
assert str(address) == "2:236/77.1@fidonet"
raw = b"\x01CHRS: IBMPC 2\r\nBruger m\x9bde\x00"
text = parse_body(to_utf8(raw, detect_charset(raw)))
assert text.endswith("Bruger møde")
assert parse_message(text).charset == "IBMPC 2"
```

```python
from golded_ftn import OutgoingMessage, repair_mojibake, synthetic_id

message = OutgoingMessage(
    from_name="Alice", to_name="Bob", subject="Ping", body_text="Hello"
)
identity = synthetic_id(
    message.from_name, message.to_name, message.subject, None, message.body_text
)
assert identity.startswith("hash:sha256:")
result = repair_mojibake("Bruger m°de")
assert result.text == "Bruger møde"
assert result.changed
```

Decoding is strict by default. Pass `errors="replace"` or `errors="ignore"`
explicitly when lossy decoding is intended. An unknown declared charset uses the
configured fallback. FTN aliases also work as fallback names. An invalid fallback
always raises `LookupError`, even when the message declares a known charset.

Historical Cyrillic declarations are supported in both `CHRS` and `CHARSET`:

| Names | Decoded as |
| --- | --- |
| `CP-866`, `+7FIDO`, `+7_FIDO`, `FIDO7`, `FIDO_7`, `RUS` | CP866 |
| `KOI`, `KOI8`, `GOST`, `CP20866` | KOI8-R |
| `WIN`, `WIN-1251`, `WINDOWS-1251`, `CP-1251` | CP1251 |
| `KOI8-U`, `KOI8U`, `KOU`, `KOI-U`, `CP21866` | KOI8-U |
| `CP1125`, `UKR` | CP1125 |

These aliases follow [GoldED+'s configuration](https://github.com/golded-plus/golded-plus/blob/1a251c6375081453a410bb8d647be120b8902de3/cfgs/config/advanced.cfg#L1498).
Names are case-insensitive and also work as configured fallbacks. The first FTN
charset declaration wins; RFC `Content-Type` is not used for body detection.
The default and `IBMPC` remain CP850.

`repair_mojibake` stays opt-in; its confidence is a heuristic score. Keep the
original bytes/text separately. Repair preserves box-drawing frames and block
art, literal degree signs and already-correct Danish/German words. Isolated
`┼`/`▀` inside words can still be damaged letters; strict UTF-8 recovery can also
repair graphics-looking bytes such as `m├©de`. These are contextual heuristics,
not a guarantee that every graphic or corrupted line is recognized.

Candidates cannot add C0 controls (except tab), DEL, C1 controls or replacement
characters. A MIME word labelled ASCII may fall back to Latin-1; invalid UTF-8
and unknown declarations do not get that fallback. Recognizable uuencode data
lines and complete PGP armour regions are excluded from repair. Pass the whole
text to preserve PGP context; separate per-line calls cannot retain that state.
Line endings still normalize to LF, including protected regions, so repaired
output is not a byte-preserving copy or a signature-verification input.

Values are frozen, slotted and keyword-only. Collections are tuples. Date fields
use `datetime`; the package never guesses a timezone. Unknown metadata stays
`None`. Parsed address fields remain strings; outgoing addresses use `FtnAddress`.

Control parsing retains valid unknown kludges, repeated fields and raw routing
strings. Both `parse_message` and `extract_msgid` ignore quoted examples, accept
case-insensitive kludge names and use the first MSGID. Interior nulls are preserved;
trailing nulls are removed from parsed values. `ControlLine.raw` is a text line without its
line separator, including trailing nulls; it is never original source bytes.

Repair is opt-in and returns a new value. Its confidence is a heuristic score,
not a probability. It is tuned to the PHP package's European-language fixtures,
not a general encoding detector. Repair normalizes line endings even if no line
is repaired; `changed` reports repairs only.

Synthetic IDs hash the exact UTF-8 JSON array of names, subject, date and complete
body. Consumers choose date formatting. These IDs neither prove identical source
records nor replace a supplied MSGID.

See the [complete API reference](docs/api.md), [PHP API mapping](docs/php-api.md), [contributing](CONTRIBUTING.md),
[release checks](docs/releasing.md) and [security](SECURITY.md).

## Archive reading

`ReaderOptions()` keeps strict reading. Concrete readers may support an explicit
archive mode with a required report callback:

```python
from golded_ftn import ReaderIssue, ReaderOptions

issues: list[ReaderIssue] = []
options = ReaderOptions(archive_mode=True, on_issue=issues.append)
assert options.archive_mode
```

Each `ReaderIssue` identifies the format, actual source path, record identity and
physical byte offset when known. Its action is `recovered`, `skipped` or `stopped`;
its code and detail explain the deviation without including message contents.
Multiple issues may describe one record. A recovery describes an accepted
metadata or decoding deviation; a later skip still excludes that record.
A stop means the area traversal is incomplete, even if earlier messages were
returned. Filesystem and callback failures propagate. The format package defines
the allowed recoveries; core does not read or repair files.

## Version 1.4.0

Add recoverable writer contracts, physical-base identity and staged mutations
with explicit settlement. FTN addresses may omit the zone, and readers can expose
streaming by default with eager validation as an explicit option. Archive recovery
still requires an issue callback.

Linux and macOS, Python 3.12–3.14, are the release targets. Windows is outside
this candidate's test and compatibility promises. See [release preparation](docs/release.md).
