imessage-chatdb 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. imessage_chatdb-0.1.0/.github/workflows/ci.yml +49 -0
  2. imessage_chatdb-0.1.0/.github/workflows/publish.yml +48 -0
  3. imessage_chatdb-0.1.0/.gitignore +23 -0
  4. imessage_chatdb-0.1.0/CHANGELOG.md +140 -0
  5. imessage_chatdb-0.1.0/LICENSE +21 -0
  6. imessage_chatdb-0.1.0/PKG-INFO +651 -0
  7. imessage_chatdb-0.1.0/README.md +633 -0
  8. imessage_chatdb-0.1.0/pyproject.toml +48 -0
  9. imessage_chatdb-0.1.0/src/imessage_chatdb/__init__.py +123 -0
  10. imessage_chatdb-0.1.0/src/imessage_chatdb/__main__.py +355 -0
  11. imessage_chatdb-0.1.0/src/imessage_chatdb/attachments.py +215 -0
  12. imessage_chatdb-0.1.0/src/imessage_chatdb/chats.py +377 -0
  13. imessage_chatdb-0.1.0/src/imessage_chatdb/connection.py +129 -0
  14. imessage_chatdb-0.1.0/src/imessage_chatdb/dates.py +58 -0
  15. imessage_chatdb-0.1.0/src/imessage_chatdb/db.py +346 -0
  16. imessage_chatdb-0.1.0/src/imessage_chatdb/errors.py +84 -0
  17. imessage_chatdb-0.1.0/src/imessage_chatdb/handles.py +54 -0
  18. imessage_chatdb-0.1.0/src/imessage_chatdb/keyed_archive.py +118 -0
  19. imessage_chatdb-0.1.0/src/imessage_chatdb/link_preview.py +190 -0
  20. imessage_chatdb-0.1.0/src/imessage_chatdb/messages.py +366 -0
  21. imessage_chatdb-0.1.0/src/imessage_chatdb/models.py +452 -0
  22. imessage_chatdb-0.1.0/src/imessage_chatdb/polling.py +241 -0
  23. imessage_chatdb-0.1.0/src/imessage_chatdb/py.typed +0 -0
  24. imessage_chatdb-0.1.0/src/imessage_chatdb/reactions.py +102 -0
  25. imessage_chatdb-0.1.0/src/imessage_chatdb/schema.py +315 -0
  26. imessage_chatdb-0.1.0/src/imessage_chatdb/search.py +125 -0
  27. imessage_chatdb-0.1.0/src/imessage_chatdb/services.py +54 -0
  28. imessage_chatdb-0.1.0/src/imessage_chatdb/sql.py +197 -0
  29. imessage_chatdb-0.1.0/src/imessage_chatdb/typedstream.py +111 -0
  30. imessage_chatdb-0.1.0/tests/__init__.py +0 -0
  31. imessage_chatdb-0.1.0/tests/conftest.py +332 -0
  32. imessage_chatdb-0.1.0/tests/fixtures/__init__.py +0 -0
  33. imessage_chatdb-0.1.0/tests/fixtures/builders.py +276 -0
  34. imessage_chatdb-0.1.0/tests/fixtures/keyed_archive_writer.py +192 -0
  35. imessage_chatdb-0.1.0/tests/fixtures/relay_sql_golden.py +212 -0
  36. imessage_chatdb-0.1.0/tests/fixtures/schema_profiles.py +197 -0
  37. imessage_chatdb-0.1.0/tests/fixtures/typedstream_writer.py +95 -0
  38. imessage_chatdb-0.1.0/tests/test_attachments.py +445 -0
  39. imessage_chatdb-0.1.0/tests/test_chats.py +307 -0
  40. imessage_chatdb-0.1.0/tests/test_cli.py +520 -0
  41. imessage_chatdb-0.1.0/tests/test_connection.py +403 -0
  42. imessage_chatdb-0.1.0/tests/test_dates.py +123 -0
  43. imessage_chatdb-0.1.0/tests/test_db.py +782 -0
  44. imessage_chatdb-0.1.0/tests/test_edits.py +165 -0
  45. imessage_chatdb-0.1.0/tests/test_find_chat.py +219 -0
  46. imessage_chatdb-0.1.0/tests/test_fuzz.py +342 -0
  47. imessage_chatdb-0.1.0/tests/test_link_preview.py +605 -0
  48. imessage_chatdb-0.1.0/tests/test_messages.py +593 -0
  49. imessage_chatdb-0.1.0/tests/test_models.py +940 -0
  50. imessage_chatdb-0.1.0/tests/test_reactions.py +165 -0
  51. imessage_chatdb-0.1.0/tests/test_readme_snippets.py +124 -0
  52. imessage_chatdb-0.1.0/tests/test_scaffold.py +384 -0
  53. imessage_chatdb-0.1.0/tests/test_schema.py +422 -0
  54. imessage_chatdb-0.1.0/tests/test_search.py +224 -0
  55. imessage_chatdb-0.1.0/tests/test_services.py +182 -0
  56. imessage_chatdb-0.1.0/tests/test_typedstream.py +441 -0
  57. imessage_chatdb-0.1.0/tests/test_watch.py +767 -0
@@ -0,0 +1,49 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ test:
13
+ name: ${{ matrix.os }} / Python ${{ matrix.python }}
14
+ runs-on: ${{ matrix.os }}
15
+ strategy:
16
+ fail-fast: false
17
+ matrix:
18
+ os: [macos-latest, ubuntu-latest]
19
+ python: ["3.12", "3.14"]
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+ - uses: actions/setup-python@v5
23
+ with:
24
+ python-version: ${{ matrix.python }}
25
+ - name: Install
26
+ run: python -m pip install --upgrade pip && python -m pip install -e . "ruff==0.16.10" "mypy==2.4.0" pytest
27
+ - name: Lint
28
+ run: ruff check src tests
29
+ - name: Type-check
30
+ run: mypy --strict src
31
+ - name: Test
32
+ # The suite builds synthetic databases under the test tmp dir; it never
33
+ # opens a real ~/Library/Messages/chat.db and passes on a machine without one.
34
+ run: pytest -q
35
+
36
+ package:
37
+ name: Build sdist + wheel
38
+ runs-on: ubuntu-latest
39
+ steps:
40
+ - uses: actions/checkout@v4
41
+ - uses: actions/setup-python@v5
42
+ with:
43
+ python-version: "3.12"
44
+ - name: Build
45
+ run: python -m pip install --upgrade pip build twine && python -m build
46
+ - name: Check metadata
47
+ run: python -m twine check dist/*
48
+ - name: Smoke-install the wheel
49
+ run: python -m pip install dist/*.whl && python -c "import imessage_chatdb as m; print(m.__version__)" && python -m imessage_chatdb --help
@@ -0,0 +1,48 @@
1
+ name: Publish to PyPI
2
+
3
+ # Runs when a version tag is pushed (v0.1.0, v0.2.0, ...). Publishes through
4
+ # PyPI's trusted publishing (OpenID Connect): no API token is stored anywhere.
5
+ on:
6
+ push:
7
+ tags: ["v*"]
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ build:
14
+ name: Test, build and verify
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - uses: actions/setup-python@v5
19
+ with:
20
+ python-version: "3.12"
21
+ - name: Install
22
+ run: python -m pip install --upgrade pip && python -m pip install -e . pytest build twine
23
+ - name: Test
24
+ run: pytest -q
25
+ - name: Tag must match the package version
26
+ run: |
27
+ v=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
28
+ [ "v$v" = "$GITHUB_REF_NAME" ] || { echo "tag $GITHUB_REF_NAME does not match version $v in pyproject.toml"; exit 1; }
29
+ - name: Build
30
+ run: python -m build && python -m twine check dist/*
31
+ - uses: actions/upload-artifact@v4
32
+ with:
33
+ name: dist
34
+ path: dist/
35
+
36
+ publish:
37
+ name: Upload to PyPI
38
+ needs: build
39
+ runs-on: ubuntu-latest
40
+ environment: pypi
41
+ permissions:
42
+ id-token: write # trusted publishing
43
+ steps:
44
+ - uses: actions/download-artifact@v4
45
+ with:
46
+ name: dist
47
+ path: dist/
48
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,23 @@
1
+ # Real or scratch Messages databases must never enter the repo
2
+ *.db
3
+ *.db-wal
4
+ *.db-shm
5
+
6
+ # Relay state and secrets
7
+ relay_state*
8
+ .env
9
+
10
+ # DESIGN.md is tracked here but excluded from the sdist (see pyproject.toml).
11
+
12
+ # Environments and caches
13
+ .venv*/
14
+ __pycache__/
15
+ *.pyc
16
+ dist/
17
+ build/
18
+ .mypy_cache/
19
+ .ruff_cache/
20
+ .pytest_cache/
21
+
22
+ # macOS
23
+ .DS_Store
@@ -0,0 +1,140 @@
1
+ # Changelog
2
+
3
+ All notable changes to `imessage-chatdb` are recorded here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ ## 0.1.0 — 2026-10-03
8
+
9
+ First release: the chat.db reading code extracted from a private iMessage
10
+ relay into a stdlib-only, read-only, typed library whose results feed that
11
+ relay byte-for-byte.
12
+
13
+ ### Added
14
+
15
+ - `open()` / `ChatDB`: one fresh read-only connection per call (`?mode=ro`
16
+ URI + `PRAGMA query_only = 1`, autocommit, every query `fetchall()`-ed,
17
+ never `immutable=1`); `connection()` context manager for batching;
18
+ `readonly_uri=False` compatibility shim for callers that used a plain
19
+ `sqlite3.connect` (opens `?mode=rw`: read-write at the SQLite level, never
20
+ creates a missing file; SQLite is the first thing to touch the path in both
21
+ modes, there is no `os.path.exists` probe). `open()` and `refresh_schema()`
22
+ check the required
23
+ columns, so a SQLite file that is not a Messages database raises
24
+ `SchemaError` up front. `imessage_chatdb.open` is an attribute but not in
25
+ `__all__` (a star import never shadows the builtin).
26
+ - Errors: `ChatDBError`, `ChatDBAccessError` (`"<path>: <sqlite message>.
27
+ <advice>"` in both open modes; the advice names `sys.executable` and the
28
+ Full Disk Access pane, with the Homebrew-upgrade trap; `path`, `reason` and
29
+ `executable` are attributes), `ChatDBBusy`
30
+ (retryable), `CursorAhead(cursor_rowid, max_rowid)`, `SchemaError(column)`.
31
+ - `Schema`: `PRAGMA table_info` introspection of the seven tables, required /
32
+ optional column lists, `optional_missing`, best-effort `profile`
33
+ (`macos14`, `macos15`, `macos26`, `macos27`), `build_message_select()` that
34
+ renders missing optional columns as `NULL AS alias` and trims `payload_data`
35
+ to URL-balloon rows.
36
+ - Messages: `messages_after`, `messages_edited_after` (raw Apple-int edit
37
+ mark that never regresses), `thread_messages` (newest N, returned
38
+ ascending), `recent_messages`, `message`, `messages_by_guid`, `reply_targets`,
39
+ `enrich` (one attachments query for rows with attachments + one reply-target
40
+ query per batch), `max_rowid`, `max_date_edited`, `include_orphans`.
41
+ - Attachments: `attachments_for`, `attachment_by_guid`, `chat_attachments`,
42
+ `payload_for`; the default filter excludes `*.pluginPayloadAttachment` by
43
+ `transfer_name` suffix (not `hide_attachment`, which would also hide real
44
+ images Messages shows).
45
+ - Chats: `chats_by_activity`, `chat`, `chat_by_rowid`, `participants`,
46
+ `participants_map`, `chat_services` (outgoing > any > chat precedence),
47
+ `last_rowid_for`, `unread_count`, `lite_messages`, `one_to_one_activity`,
48
+ `find_chat` with a pluggable `key` and `exclude` (one address = one-to-one
49
+ branch; otherwise exact participant-set match in the group branch).
50
+ - Search: NUL-safe `instr()` search over `attributedBody` with a Python
51
+ recheck on the decoded text; `extract_urls`, `snippet`.
52
+ - Polling (`imessage_chatdb.polling`; `Cursor`, `Event`, `poll_once`,
53
+ `watch`, `run_watch` are all package exports, so `from imessage_chatdb
54
+ import watch` is the generator function and `ChatDB.watch` the same thing):
55
+ frozen `Cursor(rowid, edit_mark)` with JSON round-trip; `Event(kind,
56
+ message, cursor)` where `cursor` is the `Cursor` to persist once that event
57
+ has been handled (`"new"`: its own ROWID with the round's starting mark;
58
+ `"edited"`: the round's final ROWID with the largest `date_edited` so far,
59
+ where rows tied on `date_edited` advance the mark only on the tie's last
60
+ event so a checkpoint inside a tie replays the tie instead of skipping its
61
+ rest; the last event's cursor equals the one `poll_once` returns), so a
62
+ `watch()` consumer that checkpoints after each event resumes with
63
+ at-least-once delivery; `poll_once` (busy -> no events, nothing advanced; `new` before
64
+ `edited`; `CursorAhead` on a rebuilt database), `watch()` generator,
65
+ `run_watch()` with the per-round `on_cursor` persistence hook.
66
+ - Decoders (never raise): `extract_text` (typedstream byte-scan),
67
+ `effective_text` (the relay's text rule, exactly), `clean_text`;
68
+ `KeyedArchive` (plistlib + UID dereference with a visited set),
69
+ `parse_link_preview` / `LinkPreview`, `embedded_images` (`None` for an
70
+ unusable payload, `[]` for an archive without an image, else the blobs in
71
+ table order), `embedded_image` (the largest of them), `sniff_image_mime`.
72
+ - Pure helpers: `apple_to_unix` (the relay's expression verbatim, `0`/`None`
73
+ -> `None`), `unix_to_apple`, `apple_to_datetime`; `classify_reaction` /
74
+ `Reaction` for tapbacks 2000-2006 / 3000-3006 / 1000 incl. macOS 26+
75
+ `associated_message_emoji`; `parse_associated_guid` for `p:`, `bp:` and
76
+ bare GUID forms; `Service` enum, `normalize_service`, `service_family`;
77
+ `address_key` (documented as North American: the last 10 digits, which
78
+ collides or mismatches on international numbers — pass `find_chat` your own
79
+ `key=`), `is_email`, `is_group_style`.
80
+ - Models: frozen slotted dataclasses `Message`, `Attachment`, `ReplyTarget`,
81
+ `LiteMessage`, `Chat`, `ChatSummary`, `ChatMatch`, `SearchHit` with raw
82
+ Apple dates, `*_unix` / `datetime` properties and `to_dict()` in the relay's
83
+ key order with raw values; `ReplyTarget.to_dict()` (the `reply_to` of
84
+ `Message.to_dict()`) is facts only — `{"guid", "text", "is_from_me",
85
+ "sender_handle"}`, untruncated, no `"You"` / `"Attachment"` strings (those
86
+ are built by the relay's adapter).
87
+ - CLI: `python -m imessage_chatdb check | tail [--since-rowid N] [--follow]
88
+ [--limit N] [--interval S] | chats [--limit N] | search TEXT [--limit N]`,
89
+ `--db PATH` on every subcommand, JSON-lines output; exit codes 0 ok, 1
90
+ other library error, 2 with the access explanation on `ChatDBAccessError`,
91
+ 64 (`EX_USAGE`) for a command-line usage error so that 2 stays an
92
+ unambiguous "Full Disk Access lost" signal, 130 interrupted; a stdout closed
93
+ early by its reader (`tail ... | head -1`) ends the command quietly with
94
+ exit 0 (the broken descriptor is pointed at `os.devnull` so the interpreter's
95
+ shutdown flush cannot fail again); `main(argv)` returns the code and never
96
+ raises `SystemExit`. `tail` and `search` print message content to stdout
97
+ as JSON lines (the only place the library does), which `--help`, the module
98
+ docstring and the README say not to point at a log or a remote pipe.
99
+ - Tests: four schema profiles built under `tmp_path` only (the suite refuses
100
+ `~/Library`); `attributedBody` and `payload_data` writers that reproduce the
101
+ live byte layouts; WAL-vs-`immutable` and lock-contention tests; 2,000-
102
+ iteration seeded fuzzing of every decoder with random bytes, bit-flipped
103
+ valid blobs and random-UID plists; README snippets executed against a
104
+ fixture.
105
+ - Packaging: `DESIGN.md` (the design notes, kept in the repository) is
106
+ excluded from the sdist; `README.md`, `LICENSE` and `py.typed` ship.
107
+
108
+ ### Known deviations from the relay
109
+
110
+ - **Typedstream length tag `0x82` is read as 4 bytes** (u32 little-endian);
111
+ the relay read 3. `0x83` is read as 8 bytes; any other tag `>= 0x80` yields
112
+ `None` instead of being used as a raw length. This is the spec-correct
113
+ reading. No live row uses `0x82` or `0x83` (the longest observed text is
114
+ under 10 KB and `0x81` + u16 covers up to 65,535), so the relay's output
115
+ cannot change today; the case is exercised synthetically.
116
+ - **A truncated `attributedBody` decodes to `None`, not a partial string.**
117
+ The rule, pinned by the truncation-prefix tests at every byte offset: a blob
118
+ cut before the end of its text — in the header, in the length tag or its
119
+ length bytes, or inside the text itself — makes `extract_text` return
120
+ `None`, never a partial string; a blob cut only in the `0x86` trailer after
121
+ the text (the text is complete) decodes to the complete text, exactly as an
122
+ intact blob would. The relay returned whatever bytes were there. Every live
123
+ blob carries the trailer after its text, so the `None` case is reachable
124
+ only on a corrupt blob; through `effective_text` such a row's `text` is
125
+ `None` where the relay gave a fragment.
126
+
127
+ ### Deliberately preserved relay behaviour (to be revisited as versioned changes)
128
+
129
+ - `iMessageLite` is folded into the `iMessage` family by `service_family`.
130
+ - Attachment and participant queries carry no `ORDER BY` (SQLite scan order).
131
+ - Messages without a `chat_message_join` row are hidden unless
132
+ `include_orphans=True`.
133
+ - `date_retracted` is exposed as a field; retractions are not an event kind.
134
+ - The `guid.startswith("iMessage")` tie-break in `find_chat` is kept even
135
+ though every live chat guid starts with `any;`.
136
+
137
+ ### Not in this release
138
+
139
+ - Full typedstream parts reader, edit history from `message_summary_info`,
140
+ retraction events, `date_updated` as a universal change mark, FTS.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jeremy Pinchasi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.