outlooks 0.2.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.
- outlooks-0.2.0/.gitignore +22 -0
- outlooks-0.2.0/CHANGELOG.md +147 -0
- outlooks-0.2.0/LICENSE +21 -0
- outlooks-0.2.0/PKG-INFO +327 -0
- outlooks-0.2.0/README.md +314 -0
- outlooks-0.2.0/outlooks/__init__.py +1 -0
- outlooks-0.2.0/outlooks/capture.py +454 -0
- outlooks-0.2.0/outlooks/classify.py +158 -0
- outlooks-0.2.0/outlooks/cli.py +688 -0
- outlooks-0.2.0/outlooks/config.py +274 -0
- outlooks-0.2.0/outlooks/coverage.py +417 -0
- outlooks-0.2.0/outlooks/detail.py +88 -0
- outlooks-0.2.0/outlooks/doctor.py +184 -0
- outlooks-0.2.0/outlooks/hook.py +246 -0
- outlooks-0.2.0/outlooks/host.py +110 -0
- outlooks-0.2.0/outlooks/lookup.py +317 -0
- outlooks-0.2.0/outlooks/prompts/__init__.py +1 -0
- outlooks-0.2.0/outlooks/prompts/references/connector.md +245 -0
- outlooks-0.2.0/outlooks/prompts/references/profile-template.md +104 -0
- outlooks-0.2.0/outlooks/prompts/references/queries.md +296 -0
- outlooks-0.2.0/outlooks/prompts/skills/lookup/SKILL.md +315 -0
- outlooks-0.2.0/outlooks/prompts/skills/lookup/references/reader-brief.md +72 -0
- outlooks-0.2.0/outlooks/prompts/skills/lookup/references/searcher-brief.md +59 -0
- outlooks-0.2.0/outlooks/prompts/skills/lookup/references/timeline.md +56 -0
- outlooks-0.2.0/outlooks/prompts/skills/sweep/SKILL.md +228 -0
- outlooks-0.2.0/outlooks/prompts/skills/window/SKILL.md +334 -0
- outlooks-0.2.0/outlooks/prompts/skills/window/references/bulk-reader-brief.md +68 -0
- outlooks-0.2.0/outlooks/prompts/skills/window/references/multi-pager.md +12 -0
- outlooks-0.2.0/outlooks/prompts/skills/window/references/pager-brief.md +44 -0
- outlooks-0.2.0/outlooks/py.typed +0 -0
- outlooks-0.2.0/outlooks/render.py +328 -0
- outlooks-0.2.0/outlooks/senders.py +70 -0
- outlooks-0.2.0/outlooks/sizing.py +103 -0
- outlooks-0.2.0/outlooks/store.py +321 -0
- outlooks-0.2.0/outlooks/window_plan.py +317 -0
- outlooks-0.2.0/outlooks/worklist.py +267 -0
- outlooks-0.2.0/pyproject.toml +107 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
.archive/
|
|
2
|
+
.claude/
|
|
3
|
+
.hypothesis/
|
|
4
|
+
.pytest_cache/
|
|
5
|
+
.ruff_cache/
|
|
6
|
+
.venv/
|
|
7
|
+
.vscode/
|
|
8
|
+
.worktrees/
|
|
9
|
+
|
|
10
|
+
__pycache__/
|
|
11
|
+
build/
|
|
12
|
+
dist/
|
|
13
|
+
*.egg-info/
|
|
14
|
+
|
|
15
|
+
.DS_Store
|
|
16
|
+
|
|
17
|
+
.coverage
|
|
18
|
+
.coverage.*
|
|
19
|
+
|
|
20
|
+
.env
|
|
21
|
+
.env.*
|
|
22
|
+
!.env.example
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
7
|
+
|
|
8
|
+
A repo that builds on this package depends on more than its commands. A
|
|
9
|
+
change to any of the following is listed under *Changed* or *Removed*, with
|
|
10
|
+
what a consuming repo has to do about it:
|
|
11
|
+
|
|
12
|
+
- a public Python name or one of its parameters (the README's "Python API");
|
|
13
|
+
- a settings key or its default. A default counts because a repo that leaves
|
|
14
|
+
a key unset still has the default's path in its hook script and its ignore
|
|
15
|
+
file;
|
|
16
|
+
- the archive's file format;
|
|
17
|
+
- a command's name or exit code;
|
|
18
|
+
- a line another tool is told to read: the `coverage` footer (`ledger
|
|
19
|
+
high-water mark`, `coverage ends`, `next sweep from`), and `COVERED`,
|
|
20
|
+
`INCOMPLETE`, and `DIFFERS` from `import`;
|
|
21
|
+
- a skill step's name;
|
|
22
|
+
- a key of a lookup's timeline file.
|
|
23
|
+
|
|
24
|
+
## [Unreleased]
|
|
25
|
+
|
|
26
|
+
## [0.2.0] - 2026-09-29
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- A declared Python API: `store`, `classify`, `detail`, and `config` each
|
|
31
|
+
name what a consuming repo may import in `__all__`, listed in the README's
|
|
32
|
+
"Python API" section with a reading example and an example of refining a
|
|
33
|
+
`system` role in the repo's own wrapper. Every other module says in its
|
|
34
|
+
docstring that it is internal. A contract test pins the names, and each
|
|
35
|
+
function's parameter names, which of them have a default, and how each
|
|
36
|
+
may be passed.
|
|
37
|
+
- `outlooks coverage --ledger <path>` reads a ledger kept somewhere other
|
|
38
|
+
than `ledger.csv` in `archive_dir`. A path that is not a file is an error
|
|
39
|
+
(exit 1), never an empty ledger.
|
|
40
|
+
- `outlooks lookup-reset <name>` moves a lookup's page and timeline files
|
|
41
|
+
to `earlier/{timestamp}/` under `scratch_dir` and prints what it moved, so
|
|
42
|
+
a repeat lookup of a name never merges the last one's pages. It deletes
|
|
43
|
+
nothing, and the lookup skill runs it before launching searchers.
|
|
44
|
+
- `outlooks doc lookup/timeline` lists every key of a lookup's timeline
|
|
45
|
+
file, which of them `check` reads, and which are required.
|
|
46
|
+
- `outlooks doctor` checks in one read-only pass that a repo is wired
|
|
47
|
+
correctly, one line per check, and exits 1 if any fails: the settings
|
|
48
|
+
(the table is there, every key is a setting, and each parses), the
|
|
49
|
+
profile (the file exists when `profile` is set), the archive
|
|
50
|
+
(`coverage.csv` and every hits file parse), the hook script, the hook's
|
|
51
|
+
settings entry, and the skill stub. It makes no connector call, so a
|
|
52
|
+
consuming repo's CI can run it.
|
|
53
|
+
- `outlooks senders [--since YYYY-MM-DD]` counts the archived inbound hits
|
|
54
|
+
by sender address, most frequent first, and marks each `own`, `system`, or
|
|
55
|
+
`unlisted`, so a notifier missing from `system_senders` is visible.
|
|
56
|
+
Read-only. The profile template's *Who we are* points at it.
|
|
57
|
+
- Every step of the three modes has a fixed name, given where the step
|
|
58
|
+
starts (the sweep's are `setup`, `sweep`, `classify`, `match`, `worklist`,
|
|
59
|
+
`file`, `ledger`, and `report`). A repo's own skill or profile cites a step
|
|
60
|
+
by name, which a renumbering does not change; `outlooks doc
|
|
61
|
+
profile-template` says so. A test pins the names and their order per mode.
|
|
62
|
+
- The README gives one install recipe for a repo that only runs the
|
|
63
|
+
commands and one for a repo that imports the package, an upgrade recipe
|
|
64
|
+
that ends in `outlooks doctor`, a CI example, how to run any command
|
|
65
|
+
against a copy of the archive, and the fixture a consuming repo's tests
|
|
66
|
+
pin the settings with.
|
|
67
|
+
|
|
68
|
+
### Changed
|
|
69
|
+
|
|
70
|
+
- `outlooks coverage` ends with one more line, `next sweep from`, giving the
|
|
71
|
+
instant the next sweep passes as `afterDateTime`, in UTC: the earlier of
|
|
72
|
+
the ledger's high-water mark less five minutes and the end of recorded
|
|
73
|
+
coverage. The sweep reads it, where it used to compute it. With an empty
|
|
74
|
+
ledger the line is the end of recorded coverage, and the sweep still asks
|
|
75
|
+
the operator where to start, because the archived mail before that instant
|
|
76
|
+
has never been classified. A tool that reads the footer should find each
|
|
77
|
+
line by its label, not its position.
|
|
78
|
+
- `outlooks import` names the fields that differ on a `DIFFERS` line (`DIFFERS
|
|
79
|
+
from the stored copy in body.content, attachments[].uri: <path>`) and no
|
|
80
|
+
longer says `(--replace rewrites)`. A tool that matches the line should
|
|
81
|
+
match on `DIFFERS from the stored copy` and take the path after the last
|
|
82
|
+
`: `. The line names no field, `DIFFERS from the stored copy: <path>`,
|
|
83
|
+
when the stored copy is not JSON. The README says when `--replace` is the
|
|
84
|
+
fix.
|
|
85
|
+
- `outlooks check` prints why a row is `BAD` on a line of its own under the
|
|
86
|
+
row: `missing key date_local` (or `internet_message_id`), `not archived`,
|
|
87
|
+
or the two dates that disagree. A timeline file with no
|
|
88
|
+
`internet_message_id` used to stop the command with a traceback, and one
|
|
89
|
+
with no `date_local` read as a date mismatch. The exit code is unchanged.
|
|
90
|
+
|
|
91
|
+
### Fixed
|
|
92
|
+
|
|
93
|
+
- `outlooks split <name>` no longer merges the page files of another lookup
|
|
94
|
+
whose name starts with `<name>-` (`smith` and `smith-jones`). A page file
|
|
95
|
+
is the name, one of the searchers' kinds (`sender`, `title`, `sent`) or
|
|
96
|
+
none, and `page-{n}.json`.
|
|
97
|
+
|
|
98
|
+
## [0.1.0] - 2026-09-27
|
|
99
|
+
|
|
100
|
+
### Added
|
|
101
|
+
|
|
102
|
+
- The mailbox archive: the two-tier, write-once store (`store`), the capture
|
|
103
|
+
import (`capture`), window coverage (`coverage`), window sizing and the
|
|
104
|
+
item-id proof (`sizing`), the backfill planner (`window_plan`), message
|
|
105
|
+
classification (`classify`), the message detail view (`detail`), and the
|
|
106
|
+
`.docx` transcription (`render`).
|
|
107
|
+
- `[tool.outlooks]` configuration with `OUTLOOKS_*` overrides, printed by
|
|
108
|
+
`outlooks config`: `mailbox`, `own_addresses`, `system_senders`,
|
|
109
|
+
`decision_tag`, `archive_dir`, `captured_dir`, `scratch_dir`, `timezone`,
|
|
110
|
+
`render_label` (the label `render` gives a file when `--label` is not
|
|
111
|
+
passed, default `Message`), and `profile`.
|
|
112
|
+
- The signed-in account's own mailbox: with `mailbox` unset the archive files
|
|
113
|
+
under the first of `own_addresses`, and `import` takes the captures that
|
|
114
|
+
name no mailbox or name any own address. Its pulls report no total, so a
|
|
115
|
+
window is covered once its last page is captured.
|
|
116
|
+
- The skill helpers as commands: `split`, `check`, `worklist`, `totals`,
|
|
117
|
+
`audit`, `windows`, and `hash`; `hook` writes and checks the capture hook,
|
|
118
|
+
whose script writes to the repo's `captured_dir`.
|
|
119
|
+
- The `outlooks` skill (lookup, sweep, window) and its reference documents,
|
|
120
|
+
installed as a version-stamped stub through pkgskills. The sweep takes the
|
|
121
|
+
ledger's classification column name and outcome words from the profile
|
|
122
|
+
when it gives them, and skipping replies is a default the profile can
|
|
123
|
+
override.
|
|
124
|
+
- Message roles from `classify`: `arrival`, `followup`, `ours`, `decision`
|
|
125
|
+
(our mail with the `decision_tag` in its subject, reported with the label
|
|
126
|
+
after the tag), and `system` (mail from one of `system_senders`).
|
|
127
|
+
- Mass sends: a hand-owned `mass-sends.csv` beside the archive (`sender`,
|
|
128
|
+
`subject`, `after`, `before`, `exemplar`) lists the bulk sends whose copies
|
|
129
|
+
differ only in the recipient. `worklist` reads the exemplar and skips the
|
|
130
|
+
other copies; `coverage` counts them on their own line rather than as
|
|
131
|
+
covered mail with no full read.
|
|
132
|
+
- `worklist` refuses, writing nothing, while the newest capture was written
|
|
133
|
+
under 90 seconds ago, so a reader still running never has its batch files
|
|
134
|
+
replaced.
|
|
135
|
+
- `outlooks hook` reads a script written by a pre-release build as `stale`,
|
|
136
|
+
so `--apply` rewrites it; for a script with a local change it says that
|
|
137
|
+
`--apply --force` overwrites it.
|
|
138
|
+
|
|
139
|
+
### Security
|
|
140
|
+
|
|
141
|
+
- The capture hook and `outlooks import` follow the saved-file path in an
|
|
142
|
+
oversized-result note only into Claude Code's projects directory
|
|
143
|
+
(`~/.claude/projects/`, or under `CLAUDE_CONFIG_DIR`), comparing folders as
|
|
144
|
+
the filesystem resolves them and refusing a file that is itself a link, so
|
|
145
|
+
neither a `..` nor a link leads out of it. A test or tool that feeds the
|
|
146
|
+
hook a spill note must put the saved file, as a regular file, under that
|
|
147
|
+
directory; anywhere else, nothing is copied.
|
outlooks-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ronald E. Robertson
|
|
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.
|
outlooks-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: outlooks
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Archive and read an Outlook mailbox through the Microsoft 365 connector, with the Claude Code skill that drives it.
|
|
5
|
+
Project-URL: repository, https://github.com/gitronald/outlooks
|
|
6
|
+
Author-email: gitronald <gitronald@users.noreply.github.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Requires-Dist: pkgskills<0.6.0,>=0.5.1
|
|
11
|
+
Requires-Dist: typer>=0.15.0
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# outlooks
|
|
15
|
+
|
|
16
|
+
Archive and read an Outlook mailbox through the claude.ai Microsoft 365
|
|
17
|
+
connector, and ship the Claude Code skill that drives it.
|
|
18
|
+
|
|
19
|
+
The connector is only reachable from an interactive Claude Code session, so
|
|
20
|
+
this package never talks to Microsoft itself. A PostToolUse hook writes every
|
|
21
|
+
`outlook_email_search` and `read_resource` result verbatim to a captures
|
|
22
|
+
directory; `outlooks import` files those captures into a committed, write-once
|
|
23
|
+
archive, and records every date window whose pages were all captured as
|
|
24
|
+
covered. The skill (`/outlooks`) tells an agent how to search, page, and read
|
|
25
|
+
the mailbox so that the archive fills itself.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
Python 3.11 or later. `render` needs [pandoc](https://pandoc.org) on the PATH.
|
|
30
|
+
|
|
31
|
+
There are two recipes, one for each way a repo uses the package. Both pin a
|
|
32
|
+
release.
|
|
33
|
+
|
|
34
|
+
### Operator only
|
|
35
|
+
|
|
36
|
+
The repo runs the commands, the skill, and the capture hook from an
|
|
37
|
+
interactive session, and none of its own code imports `outlooks`. Put the
|
|
38
|
+
package in a dependency group of its own, so it is installed for the
|
|
39
|
+
operator and the repo's package cannot come to depend on it by accident:
|
|
40
|
+
|
|
41
|
+
```toml
|
|
42
|
+
[dependency-groups]
|
|
43
|
+
mailbox = ["outlooks==X.Y.Z"]
|
|
44
|
+
|
|
45
|
+
[tool.uv]
|
|
46
|
+
default-groups = ["dev", "mailbox"]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Importing
|
|
50
|
+
|
|
51
|
+
The repo's own code or tests `import outlooks` (see [Python API](#python-api)).
|
|
52
|
+
It is a plain dependency:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
uv add "outlooks==X.Y.Z"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Either way
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
uv sync
|
|
62
|
+
uv run outlooks install # the /outlooks skill stub, under .claude/skills/
|
|
63
|
+
uv run outlooks hook --apply # the capture hook script and its settings entry
|
|
64
|
+
uv run outlooks doctor # every check reads ok
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Where the package index cannot be used, the same release installs from its
|
|
68
|
+
tag. Keep the dependency as above and add a source:
|
|
69
|
+
|
|
70
|
+
```toml
|
|
71
|
+
[tool.uv.sources]
|
|
72
|
+
outlooks = { git = "https://github.com/gitronald/outlooks", tag = "vX.Y.Z" }
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
A git source cannot be resolved on a network that allows only the package
|
|
76
|
+
index, and it pins one tag, not a version range, so the index is the first
|
|
77
|
+
choice.
|
|
78
|
+
|
|
79
|
+
### In CI
|
|
80
|
+
|
|
81
|
+
`outlooks doctor` makes no connector call, so CI can run it. It checks what
|
|
82
|
+
the repo commits: the `[tool.outlooks]` table, the profile, the archive, and,
|
|
83
|
+
under `.claude/`, the hook script, `settings.json`, and the skill stub.
|
|
84
|
+
|
|
85
|
+
```yaml
|
|
86
|
+
- run: uv sync --frozen
|
|
87
|
+
- run: uv run outlooks doctor
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Upgrade
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# 1. change the version (or the tag) in pyproject.toml, then
|
|
94
|
+
uv lock --upgrade-package outlooks
|
|
95
|
+
uv sync
|
|
96
|
+
uv run outlooks hook --apply # rewrites a hook script an earlier release wrote
|
|
97
|
+
uv run outlooks install # rewrites the skill stub for the new version
|
|
98
|
+
uv run outlooks doctor
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Read the changelog's *Changed* and *Removed* entries for every release
|
|
102
|
+
between the two versions first: they say what a consuming repo has to do.
|
|
103
|
+
|
|
104
|
+
The lock file is the pin that holds. For a release from the index it records
|
|
105
|
+
the version and the hashes of its files, and for a git source the commit the
|
|
106
|
+
tag resolved to, so `uv sync --frozen` installs the same code whatever
|
|
107
|
+
happens upstream.
|
|
108
|
+
|
|
109
|
+
## Configure
|
|
110
|
+
|
|
111
|
+
Everything repo-specific comes from `[tool.outlooks]` in the nearest
|
|
112
|
+
`pyproject.toml` holding the table, each key overridable by an
|
|
113
|
+
`OUTLOOKS_{KEY}` environment variable. Paths are relative to that file.
|
|
114
|
+
|
|
115
|
+
```toml
|
|
116
|
+
[tool.outlooks]
|
|
117
|
+
mailbox = "team@example.org" # unset: the signed-in account's own mailbox
|
|
118
|
+
own_addresses = ["team@example.org"] # default: [mailbox]; required when mailbox is unset
|
|
119
|
+
system_senders = ["notices@system.example.org"]
|
|
120
|
+
decision_tag = "[Decision]"
|
|
121
|
+
archive_dir = "data/outlook"
|
|
122
|
+
captured_dir = "temp/outlook/captured"
|
|
123
|
+
scratch_dir = "temp/outlook"
|
|
124
|
+
timezone = "America/Los_Angeles"
|
|
125
|
+
render_label = "Message" # render's --label when none is passed
|
|
126
|
+
profile = "docs/outlook-profile.md"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
For the signed-in account's own mailbox, leave `mailbox` unset and list the
|
|
130
|
+
account's addresses in `own_addresses`: the archive files under the first of
|
|
131
|
+
them. Its searches report no totals, so a window counts as covered once its
|
|
132
|
+
last page is captured, and the window planner does not apply.
|
|
133
|
+
|
|
134
|
+
`uv run outlooks config` prints the effective values and where each came from.
|
|
135
|
+
The `profile` is repo-owned prose the skill reads for what this package cannot
|
|
136
|
+
know: where a name resolves, how the sweep classifies, what filing a candidate
|
|
137
|
+
means, and which of the repo's own records a message can contradict. `uv run
|
|
138
|
+
outlooks doc profile-template` prints the headings it should answer.
|
|
139
|
+
|
|
140
|
+
## Trying it without touching the archive
|
|
141
|
+
|
|
142
|
+
Every path is a setting, and every setting has an `OUTLOOKS_*` override, so
|
|
143
|
+
any command runs against a copy. To see what `import --replace` would
|
|
144
|
+
rewrite before it rewrites anything:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
cp -r data/outlook temp/outlook-trial
|
|
148
|
+
OUTLOOKS_ARCHIVE_DIR=temp/outlook-trial \
|
|
149
|
+
OUTLOOKS_SCRATCH_DIR=temp/outlook-trial-scratch \
|
|
150
|
+
uv run outlooks import --replace
|
|
151
|
+
git diff --no-index data/outlook temp/outlook-trial
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The captures are only read, so `captured_dir` can stay as it is.
|
|
155
|
+
`outlooks config` run with the same variables shows each path and that it
|
|
156
|
+
came from the environment.
|
|
157
|
+
|
|
158
|
+
## The archive
|
|
159
|
+
|
|
160
|
+
| Path | Holds |
|
|
161
|
+
|---|---|
|
|
162
|
+
| `hits/{mailbox}/{YYYY-MM}.jsonl` | every search hit ever listed, deduped by `internetMessageId` |
|
|
163
|
+
| `messages/{id-hash}.json` | full `read_resource` payloads, bodies raw |
|
|
164
|
+
| `messages/{id-hash}.{n}.txt` | attachment `n`'s extracted text |
|
|
165
|
+
| `coverage.csv` | one row per window pull that ran to completion |
|
|
166
|
+
| `mass-sends.csv` | optional, hand-owned: bulk sends whose copies are read once |
|
|
167
|
+
|
|
168
|
+
`{id-hash}` is the first 16 hex digits of the SHA-256 of the
|
|
169
|
+
`internetMessageId` (`outlooks hash <id>`). Both tiers are write-once: a second
|
|
170
|
+
import of an unchanged message is a no-op, and one that differs is listed, not
|
|
171
|
+
rewritten:
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
DIFFERS from the stored copy in body.content, attachments[].uri: temp/outlook/captured/20260105T101500-4242.json
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The line names the fields that differ, so the two copies need no diffing by
|
|
178
|
+
hand. It names none when the stored copy is not JSON. Which copy is right
|
|
179
|
+
decides what to do:
|
|
180
|
+
|
|
181
|
+
- **The stored copy was not written from a capture** (it was saved by hand
|
|
182
|
+
with `outlooks save`, or typed out): the capture is the connector's own
|
|
183
|
+
output, and `outlooks import --replace` rewrites the stored copy from it.
|
|
184
|
+
- **The capture is the older of the two** (the message changed after the
|
|
185
|
+
capture was written, and the stored copy came from a later read): the
|
|
186
|
+
stored copy is right. Leave it, and do not pass `--replace`.
|
|
187
|
+
|
|
188
|
+
`outlooks import <paths> --replace` limits the rewrite to the captures named,
|
|
189
|
+
so one repair never rewrites anything else that differs.
|
|
190
|
+
|
|
191
|
+
## Commands
|
|
192
|
+
|
|
193
|
+
| Command | Does |
|
|
194
|
+
|---|---|
|
|
195
|
+
| `outlooks import [paths] [--replace]` | file the hook's captures; record complete windows |
|
|
196
|
+
| `outlooks coverage [--since] [--ledger]` | covered windows, gaps, unread counts, ledger lag, and the next sweep's start |
|
|
197
|
+
| `outlooks senders [--since]` | inbound hits counted by sender, each marked `own`, `system`, or `unlisted` |
|
|
198
|
+
| `outlooks split <name> --match ...` | a lookup's hits: archived (timeline written) vs to-read |
|
|
199
|
+
| `outlooks check <name>` | a lookup's timeline files against the archive |
|
|
200
|
+
| `outlooks lookup-reset <name>` | move a lookup's page and timeline files aside before a repeat lookup |
|
|
201
|
+
| `outlooks worklist --after --before --out` | reader worklists for a full read of a range |
|
|
202
|
+
| `outlooks totals [prefix ...]` / `--check AFTER BEFORE` | window sizes; the item-id proof |
|
|
203
|
+
| `outlooks windows init/fill/split/batches/remaining` | the backfill planner |
|
|
204
|
+
| `outlooks audit <prefix> [--sweep]` | attachment-text audit for a period |
|
|
205
|
+
| `outlooks render <message.json>` | a message as `{Surname} - {render_label}.docx` (pandoc, sandboxed) |
|
|
206
|
+
| `outlooks save <payload>` | archive a hand-saved payload (sessions use `import`) |
|
|
207
|
+
| `outlooks hash <id>` | a message's archive file stem |
|
|
208
|
+
| `outlooks hook [--apply]` | check or wire the capture hook |
|
|
209
|
+
| `outlooks config` | the effective settings |
|
|
210
|
+
| `outlooks doctor` | settings, profile, archive, hook, and skill stub in one read-only pass; exit 1 if any check fails |
|
|
211
|
+
| `outlooks skill`, `outlooks doc`, `outlooks install`, `outlooks permissions` | the skill, its documents, the stub, and its permission profile ([pkgskills](https://github.com/gitronald/pkgskills)) |
|
|
212
|
+
|
|
213
|
+
## Python API
|
|
214
|
+
|
|
215
|
+
A repo that imports this package may import the names below, and nothing
|
|
216
|
+
else. Every other module is internal, and the commands are its interface. A
|
|
217
|
+
change to one of these names, or to a parameter of one, is listed in the
|
|
218
|
+
changelog under *Changed* or *Removed*.
|
|
219
|
+
|
|
220
|
+
| Module | Public names |
|
|
221
|
+
|---|---|
|
|
222
|
+
| `outlooks.store` | `archive_root`, `id_hash`, `stable`, `load_hits`, `load_messages`, `newest_hit`, `StoreError` |
|
|
223
|
+
| `outlooks.classify` | `Mail`, `Facts`, `view`, `classify`, `is_reply`, `thread_subject` |
|
|
224
|
+
| `outlooks.detail` | `detail`, `details_by_id`, `body_text` |
|
|
225
|
+
| `outlooks.config` | `Settings`, `load`, `settings`, `KEYS`, `DEFAULTS`, `ConfigError`, `mailbox`, `archive_dir`, `captured_dir`, `scratch_dir`, `zone`, `zone_label` |
|
|
226
|
+
|
|
227
|
+
The API reads the archive and never writes it: a repo fills the archive
|
|
228
|
+
through `outlooks import`.
|
|
229
|
+
|
|
230
|
+
Reading the archive:
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
from outlooks import classify, detail, store
|
|
234
|
+
|
|
235
|
+
hits = store.load_hits() # every message ever listed, by internetMessageId
|
|
236
|
+
messages = store.load_messages() # the ones read in full
|
|
237
|
+
|
|
238
|
+
for mid, hit in hits.items():
|
|
239
|
+
mail = classify.view(messages.get(mid, hit))
|
|
240
|
+
role = classify.classify(mail).role
|
|
241
|
+
print(mail.day, role, mail.sender, mail.subject)
|
|
242
|
+
|
|
243
|
+
for mid, message in messages.items():
|
|
244
|
+
fields = detail.detail(message) # from, to, cc, bcc, attachments, body
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
`classify` reports the role a message plays (`arrival`, `followup`, `ours`,
|
|
248
|
+
`decision`, `system`) and stops there. What a system sender's notification
|
|
249
|
+
says is the repo's own business, and the place for it is a thin wrapper in
|
|
250
|
+
the repo. For a ticketing system whose notices read "Ticket #12 was opened
|
|
251
|
+
by ...":
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
import re
|
|
255
|
+
|
|
256
|
+
from outlooks import classify as cl
|
|
257
|
+
|
|
258
|
+
OPENED = re.compile(r"Ticket #(\d+) was opened by (.+?)\.")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def facts(payload):
|
|
262
|
+
mail = cl.view(payload)
|
|
263
|
+
base = cl.classify(mail)
|
|
264
|
+
if base.role != "system":
|
|
265
|
+
return base.role, None
|
|
266
|
+
found = OPENED.search(mail.text)
|
|
267
|
+
return ("system-opened", int(found[1])) if found else ("system-other", None)
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
`Mail` carries what such a wrapper reads: `sender`, `recipients`, `subject`,
|
|
271
|
+
and `text` (the body as plain text for a message read in full, the summary
|
|
272
|
+
for a search hit).
|
|
273
|
+
|
|
274
|
+
## Testing against it
|
|
275
|
+
|
|
276
|
+
A consuming repo's tests pin every setting through `OUTLOOKS_*` in a
|
|
277
|
+
fixture, so they never read the repo's own `[tool.outlooks]` table, its
|
|
278
|
+
archive, or the zone of the machine they run on:
|
|
279
|
+
|
|
280
|
+
```python
|
|
281
|
+
import pytest
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@pytest.fixture(autouse=True)
|
|
285
|
+
def outlooks_settings(monkeypatch, tmp_path):
|
|
286
|
+
monkeypatch.chdir(tmp_path)
|
|
287
|
+
monkeypatch.setenv("OUTLOOKS_MAILBOX", "desk@example.org")
|
|
288
|
+
monkeypatch.setenv("OUTLOOKS_OWN_ADDRESSES", "desk@example.org")
|
|
289
|
+
monkeypatch.setenv("OUTLOOKS_SYSTEM_SENDERS", "notices@system.example.org")
|
|
290
|
+
monkeypatch.setenv("OUTLOOKS_DECISION_TAG", "[Decision]")
|
|
291
|
+
monkeypatch.setenv("OUTLOOKS_TIMEZONE", "America/Los_Angeles")
|
|
292
|
+
monkeypatch.setenv("OUTLOOKS_ARCHIVE_DIR", str(tmp_path / "archive"))
|
|
293
|
+
monkeypatch.setenv("OUTLOOKS_CAPTURED_DIR", str(tmp_path / "captured"))
|
|
294
|
+
monkeypatch.setenv("OUTLOOKS_SCRATCH_DIR", str(tmp_path / "scratch"))
|
|
295
|
+
monkeypatch.delenv("OUTLOOKS_PROFILE", raising=False)
|
|
296
|
+
monkeypatch.delenv("OUTLOOKS_RENDER_LABEL", raising=False)
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
The settings are resolved again whenever the working directory or an
|
|
300
|
+
`OUTLOOKS_*` variable changes, so a test that sets one more variable gets
|
|
301
|
+
it. A test that needs an archive writes its own messages under `tmp_path`
|
|
302
|
+
in the connector's shape, with invented senders.
|
|
303
|
+
|
|
304
|
+
## Development
|
|
305
|
+
|
|
306
|
+
```bash
|
|
307
|
+
uv sync --all-groups
|
|
308
|
+
uv run pytest
|
|
309
|
+
uv run ruff check .
|
|
310
|
+
uv run ruff format --check .
|
|
311
|
+
uv run pyrefly check
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
The tests use synthetic fixtures only (`example.org` addresses and an invented
|
|
315
|
+
cast), and every test runs in its own temporary directory with its settings
|
|
316
|
+
pinned through `OUTLOOKS_*`, so the suite never reads a real archive.
|
|
317
|
+
|
|
318
|
+
### Releases
|
|
319
|
+
|
|
320
|
+
A published tag never moves. A fix ships as the next version, under its own
|
|
321
|
+
changelog heading, because a consuming repo's lock file names the commit a
|
|
322
|
+
tag resolved to, and a tag that moved would leave two repos on the same
|
|
323
|
+
version running different code.
|
|
324
|
+
|
|
325
|
+
The changelog's preamble lists what counts as a change a consuming repo has
|
|
326
|
+
to hear about. Each such change is entered under *Changed* or *Removed*,
|
|
327
|
+
with what to do.
|