Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ on:
tag:
description: Reviewed GitHub release tag
required: true
default: v1.0.0
default: v1.1.0
type: string
permissions:
contents: read
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## 1.1.0 — 2026-10-27

- Rename reader, writer and session area_code parameters and attributes to area.
- Return area metadata through ParsedMessage.area.
- Close database connections after create, add_area and areas operations.
- Require golded-ftn 1.5.0 or newer.

## 1.0.0 — 2026-10-25

- Add schema version 1 for single- and multi-area SQLite message bases.
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# golded-ftn-sqlite

Repository: [`golded-ftn-sqlite-python`](https://github.com/golded-dev/golded-ftn-sqlite-python).
Install version 1.0.0 with Python 3.12 or newer:
Install version 1.1.0 with Python 3.12 or newer:

```sh
python -m pip install golded-ftn-sqlite==1.0.0
python -m pip install golded-ftn-sqlite==1.1.0
```

`golded-ftn-sqlite` stores FTN netmail and echomail in SQLite through the
Expand All @@ -20,7 +20,7 @@ and recovery local, and limits the blast radius when a disk has opinions.
`ftnt` treats export envelopes as write input for every format, including SQLite:

```sh
ftnt create ./general.sqlite3 --format sqlite --area-code FIDO.GENERAL
ftnt create ./general.sqlite3 --format sqlite --area FIDO.GENERAL
ftnt export ./general --format jam |
ftnt write ./general.sqlite3 --format sqlite --jsonl
```
Expand Down
4 changes: 2 additions & 2 deletions RELEASE-SHA256.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
4f9dc10e12dd24f19855a3791980c5d7b358bf5dfc05d70c284a90fa802d6557 dist/golded_ftn_sqlite-1.0.0-py3-none-any.whl
7e974ec3f4236249b0bfac8c96bfb57ebf8ab852c209808023f42ef8a7ca0b01 dist/golded_ftn_sqlite-1.0.0.tar.gz
16dd03e87560802640f2c4e880f73f3a27947ba67379896df3dad2c5ebed7c0a dist/golded_ftn_sqlite-1.1.0-py3-none-any.whl
ce84f158139f7fa5340d0d840a519749ebf8fb007aaa17f2f4c1e2e8c8de69f5 dist/golded_ftn_sqlite-1.1.0.tar.gz
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ build-backend = "hatchling.build"

[project]
name = "golded-ftn-sqlite"
version = "1.0.0"
version = "1.1.0"
description = "Transactional SQLite message bases for FTN netmail and echomail"
readme = "README.md"
requires-python = ">=3.12"
license = "MIT"
license-files = ["LICENSE"]
authors = [{name = "GoldED.dev contributors"}]
dependencies = ["golded-ftn>=1.4.0,<2"]
dependencies = ["golded-ftn>=1.5.0,<2"]
classifiers = [
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.12",
Expand Down
4 changes: 3 additions & 1 deletion scripts/verify_distribution.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import sys
import tarfile
import tempfile
import tomllib
import zipfile
from pathlib import Path

Expand All @@ -18,14 +19,15 @@ def run(arguments: list[str], directory: Path) -> None:


def main() -> None:
project = tomllib.loads((ROOT / "pyproject.toml").read_text())["project"]
(wheel,) = ROOT.joinpath("dist").glob("*.whl")
(sdist,) = ROOT.joinpath("dist").glob("*.tar.gz")
with zipfile.ZipFile(wheel) as archive:
names = archive.namelist()
metadata_name = next(name for name in names if name.endswith("/METADATA"))
metadata = email.message_from_bytes(archive.read(metadata_name))
assert metadata["Name"] == "golded-ftn-sqlite"
assert metadata["Version"] == "1.0.0"
assert metadata["Version"] == project["version"]
assert metadata["License-Expression"] == "MIT"
assert any(name.endswith("/py.typed") for name in names)
assert any(name.endswith("/licenses/LICENSE") for name in names)
Expand Down
71 changes: 41 additions & 30 deletions src/golded_ftn_sqlite/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,9 @@ def create(
database_path.parent.mkdir(parents=True, exist_ok=True)
descriptor = os.open(database_path, os.O_CREAT | os.O_EXCL | os.O_RDWR, 0o600)
os.close(descriptor)
connection = sqlite3.connect(database_path)
try:
with sqlite3.connect(database_path) as connection:
with connection:
connection.execute("PRAGMA journal_mode = WAL")
connection.execute("PRAGMA synchronous = NORMAL")
connection.executescript(_SCHEMA)
Expand All @@ -176,35 +177,45 @@ def create(
except BaseException:
database_path.unlink(missing_ok=True)
raise
finally:
connection.close()
return cls(database_path)

def add_area(self, area: AreaDefinition) -> None:
with sqlite3.connect(self.path) as connection:
connection.execute(
"""
INSERT INTO areas (
code, name, kind, sort_order, meta_key, origin_address
) VALUES (?, ?, ?, ?, ?, ?)
""",
(
area.code,
area.name,
area.kind.value,
area.sort_order,
area.meta_key,
format_address(area.default_origin),
),
)
connection = sqlite3.connect(self.path)
try:
with connection:
connection.execute(
"""
INSERT INTO areas (
code, name, kind, sort_order, meta_key, origin_address
) VALUES (?, ?, ?, ?, ?, ?)
""",
(
area.code,
area.name,
area.kind.value,
area.sort_order,
area.meta_key,
format_address(area.default_origin),
),
)
finally:
connection.close()

def areas(self) -> tuple[AreaDefinition, ...]:
with sqlite3.connect(self.path) as connection:
rows = connection.execute(
"""
SELECT code, name, kind, sort_order, meta_key, origin_address
FROM areas
ORDER BY id
"""
).fetchall()
connection = sqlite3.connect(self.path)
try:
with connection:
rows = connection.execute(
"""
SELECT code, name, kind, sort_order, meta_key, origin_address
FROM areas
ORDER BY id
"""
).fetchall()
finally:
connection.close()
return tuple(
AreaDefinition(
code=row[0],
Expand Down Expand Up @@ -243,15 +254,15 @@ def connect_database(
return connection


def select_area(connection: sqlite3.Connection, area_code: str | None) -> sqlite3.Row:
if area_code is not None:
def select_area(connection: sqlite3.Connection, area: str | None) -> sqlite3.Row:
if area is not None:
row = connection.execute(
"SELECT * FROM areas WHERE code = ?", (area_code,)
"SELECT * FROM areas WHERE code = ?", (area,)
).fetchone()
if row is None:
raise KeyError(f"unknown area [{area_code}]")
raise KeyError(f"unknown area [{area}]")
return cast(sqlite3.Row, row)
rows = connection.execute("SELECT * FROM areas ORDER BY id LIMIT 2").fetchall()
if len(rows) != 1:
raise ValueError("area_code is required unless the database has one area")
raise ValueError("area is required unless the database has one area")
return cast(sqlite3.Row, rows[0])
8 changes: 4 additions & 4 deletions src/golded_ftn_sqlite/reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def _message_from_row(
reply_to_msgno=row["reply_to_msgno"],
reply1st_msgno=row["reply1st_msgno"],
reply_next_msgno=row["reply_next_msgno"],
area_code=area["code"],
area=area["code"],
area_name=area["name"],
area_sort_order=area["sort_order"],
area_meta_key=area["meta_key"],
Expand All @@ -109,8 +109,8 @@ class SqliteReader:
``ReaderOptions(eager=True)`` to load every message before returning.
"""

def __init__(self, area_code: str | None = None) -> None:
self.area_code = area_code
def __init__(self, area: str | None = None) -> None:
self.area = area

def read(
self,
Expand All @@ -122,7 +122,7 @@ def read(
connection = connect_database(database_path, read_only=True)
try:
connection.execute("BEGIN")
area = select_area(connection, self.area_code)
area = select_area(connection, self.area)
rows = connection.execute(
"""
SELECT * FROM messages
Expand Down
24 changes: 11 additions & 13 deletions src/golded_ftn_sqlite/writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@
from .reader import _message_from_row


def _identity(path: Path, area_code: str, msgno: int) -> MessageIdentity:
def _identity(path: Path, area: str, msgno: int) -> MessageIdentity:
return MessageIdentity(
format="sqlite",
base=f"{path}#{area_code}",
base=f"{path}#{area}",
msgno=msgno,
)

Expand Down Expand Up @@ -86,25 +86,23 @@ def _revision(


class SqliteWriter:
def __init__(self, area_code: str | None = None) -> None:
self.area_code = area_code
def __init__(self, area: str | None = None) -> None:
self.area = area

def open(
self,
path: str | PathLike[str],
options: WriterOptions | None = None,
) -> SqliteSession:
return SqliteSession(
Path(path).resolve(), self.area_code, options or WriterOptions()
Path(path).resolve(), self.area, options or WriterOptions()
)


class SqliteSession:
def __init__(
self, path: Path, area_code: str | None, options: WriterOptions
) -> None:
def __init__(self, path: Path, area: str | None, options: WriterOptions) -> None:
self.path = path
self.area_code = area_code
self.area = area
self._connection = connect_database(
path, read_only=False, timeout=options.lock_timeout
)
Expand Down Expand Up @@ -132,7 +130,7 @@ def append(self, message: OutgoingMessage) -> WriteResult:
connection = self._connection
try:
connection.execute("BEGIN IMMEDIATE")
area = select_area(connection, self.area_code)
area = select_area(connection, self.area)
default_origin = parse_address(area["origin_address"])
from_address = message.from_address or default_origin
if area["kind"] == "netmail":
Expand Down Expand Up @@ -242,7 +240,7 @@ def read(self, msgno: int) -> SessionMessage:
connection = self._connection
try:
connection.execute("BEGIN")
area = select_area(connection, self.area_code)
area = select_area(connection, self.area)
row = connection.execute(
"""
SELECT * FROM messages
Expand Down Expand Up @@ -275,7 +273,7 @@ def update(
connection = self._connection
try:
connection.execute("BEGIN IMMEDIATE")
area = select_area(connection, self.area_code)
area = select_area(connection, self.area)
row = connection.execute(
"""
SELECT * FROM messages
Expand Down Expand Up @@ -477,7 +475,7 @@ def delete(
connection = self._connection
try:
connection.execute("BEGIN IMMEDIATE")
area = select_area(connection, self.area_code)
area = select_area(connection, self.area)
row = connection.execute(
"""
SELECT * FROM messages
Expand Down
14 changes: 8 additions & 6 deletions tests/test_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def test_append_echomail_and_read_it_through_public_api(tmp_path: Path) -> None:
written.identity.format,
written.identity.msgno,
message.msgno,
message.area_code,
message.area,
message.from_name,
message.to_name,
message.subject,
Expand Down Expand Up @@ -601,7 +601,7 @@ def test_multi_area_database_requires_selection_and_numbers_per_area(
)
)

with SqliteWriter("FIDO.SQLITE").open(path) as session:
with SqliteWriter(area="FIDO.SQLITE").open(path) as session:
echo = session.append(
OutgoingMessage(
from_name="Alice",
Expand All @@ -610,7 +610,7 @@ def test_multi_area_database_requires_selection_and_numbers_per_area(
body_text="Area one.",
)
)
with SqliteWriter("NETMAIL").open(path) as session:
with SqliteWriter(area="NETMAIL").open(path) as session:
netmail = session.append(
OutgoingMessage(
from_name="Alice",
Expand All @@ -621,13 +621,15 @@ def test_multi_area_database_requires_selection_and_numbers_per_area(
)
)

with pytest.raises(ValueError, match="area_code is required"):
with pytest.raises(ValueError, match="area is required"):
tuple(SqliteReader().read(path))
assert (
echo.identity.msgno,
netmail.identity.msgno,
tuple(message.subject for message in SqliteReader("FIDO.SQLITE").read(path)),
tuple(message.subject for message in SqliteReader("netmail").read(path)),
tuple(
message.subject for message in SqliteReader(area="FIDO.SQLITE").read(path)
),
tuple(message.subject for message in SqliteReader(area="netmail").read(path)),
) == (1, 1, ("Echo",), ("Netmail",))


Expand Down
Loading