pubkit 0.1.1__tar.gz → 0.3.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.
- pubkit-0.3.0/CHANGELOG.md +138 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/PKG-INFO +72 -9
- {pubkit-0.1.1 → pubkit-0.3.0}/README.md +71 -8
- {pubkit-0.1.1 → pubkit-0.3.0}/docs/FAILURE-MODES.md +69 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/pyproject.toml +1 -1
- {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/__init__.py +1 -1
- {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/adapters/medium.py +1 -1
- pubkit-0.3.0/src/pubkit/browserctl.py +490 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/cli.py +89 -10
- pubkit-0.3.0/src/pubkit/core/assets.py +96 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/core/browser.py +1 -1
- pubkit-0.3.0/src/pubkit/core/login.py +430 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/core/runner.py +7 -0
- pubkit-0.3.0/src/pubkit/render/tables.py +199 -0
- pubkit-0.3.0/tests/fixtures/fake_editor.html +119 -0
- pubkit-0.3.0/tests/fixtures/login_server.py +153 -0
- pubkit-0.3.0/tests/test_browser.py +182 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/tests/test_core.py +113 -0
- pubkit-0.3.0/tests/test_login.py +412 -0
- pubkit-0.1.1/CHANGELOG.md +0 -62
- pubkit-0.1.1/src/pubkit/browserctl.py +0 -78
- {pubkit-0.1.1 → pubkit-0.3.0}/.gitignore +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/LICENSE +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/NOTICE +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/docs/ADAPTERS.md +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/docs/ARCHITECTURE.md +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/docs/QUICKSTART.md +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/examples/inside-ai-infra/img/part1-01-model-sizes.png +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/examples/inside-ai-infra/img/part1-02-cpu-vs-gpu-ANIMATED.gif +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/examples/inside-ai-infra/img/part1-03-starving-cores-ANIMATED.gif +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/examples/inside-ai-infra/img/part1-04-gpu-comparison.png +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/examples/inside-ai-infra/img/part2-01-one-token-one-read-ANIMATED.gif +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/examples/inside-ai-infra/img/part2-02-decode-ceilings.png +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/examples/inside-ai-infra/img/part2-03-prefill-vs-decode.png +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/examples/inside-ai-infra/img/part2-04-kv-cache-cost.png +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/examples/inside-ai-infra/img/part2-05-batching-ANIMATED.gif +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/examples/inside-ai-infra/img/part2-06-vram-budget.png +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/examples/inside-ai-infra/img/part2-07-throughput-latency.png +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/examples/inside-ai-infra/img/part3-01-interconnect.png +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/examples/inside-ai-infra/img/part3-02-llmd-request-path.png +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/examples/inside-ai-infra/img/part3-03-llmd-objects.png +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/examples/inside-ai-infra/img/part3-04-slos.png +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/examples/inside-ai-infra/part-1.md +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/examples/inside-ai-infra/part-2.md +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/examples/inside-ai-infra/part-3.md +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/__main__.py +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/adapters/__init__.py +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/adapters/api_base.py +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/adapters/devto.py +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/adapters/substack.py +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/adapters/x.py +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/core/__init__.py +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/core/adapter.py +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/core/anchors.py +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/core/auth.py +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/core/capabilities.py +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/core/checks.py +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/core/ir.py +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/core/loader.py +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/core/state.py +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/core/transport.py +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/py.typed +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/registry.py +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/render/__init__.py +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/render/html.py +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/scaffold.py +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/workflows/__init__.py +0 -0
- {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/workflows/airflow.py +0 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
4
|
+
Versioning is [semantic](https://semver.org/); adapters may change behaviour on
|
|
5
|
+
a minor bump when a platform changes underneath them.
|
|
6
|
+
|
|
7
|
+
## [0.3.0] — 2026-09-11
|
|
8
|
+
|
|
9
|
+
Sign-in is the one step a human performs and a machine has to judge, and every
|
|
10
|
+
way it fails is silent. This release makes each of them say something.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`login_preflight()`** — playwright, browser, host reachability, a writable
|
|
15
|
+
session store and any existing session are checked before a window opens.
|
|
16
|
+
A login that cannot work no longer costs five minutes to find out.
|
|
17
|
+
- **Live observation during sign-in** — console errors, failed requests and
|
|
18
|
+
challenge-host scripts are recorded while the human works, and submissions to
|
|
19
|
+
the platform are counted. A button that never fires is now distinguishable
|
|
20
|
+
from a bot check that never completes; previously both were silence.
|
|
21
|
+
- **Post-flight proof** — the saved state is checked for cookies on the
|
|
22
|
+
platform's own domain and for the cookie names that authorise writing, its
|
|
23
|
+
expiry is reported, and a signed-in page is fetched and checked. **A session
|
|
24
|
+
is written only after it has been shown to work.**
|
|
25
|
+
- **`pubkit auth login <platform> --attach`** — watch a Chrome you started
|
|
26
|
+
yourself (`--remote-debugging-port=9222`) instead of launching one, for
|
|
27
|
+
platforms that will not complete a sign-in in a fresh profile.
|
|
28
|
+
- **`pubkit auth login --minutes`** to set the wait.
|
|
29
|
+
- **`core/login.py`** — one `LoginFlow` per platform declaring success markers,
|
|
30
|
+
required cookies, cookie domain, probe URL and signed-in markers. A new
|
|
31
|
+
platform inherits the whole validation suite rather than new special cases.
|
|
32
|
+
- **`tests/fixtures/login_server.py`** — a sign-in page that fails the six ways
|
|
33
|
+
real ones do: dead button, blocked challenge, partial cookies, short-lived
|
|
34
|
+
session, ghost session, and the happy path. 26 tests cover the matrix; the
|
|
35
|
+
unit half needs no browser at all.
|
|
36
|
+
- **Class E in `docs/FAILURE-MODES.md`** — six new entries.
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- A launched login window now uses a **persistent pubkit profile** and real
|
|
41
|
+
Google Chrome where it is installed, falling back to bundled Chromium.
|
|
42
|
+
pubkit does not try to disguise an automated browser; where a platform
|
|
43
|
+
declines one, `--attach` uses the browser you actually sign in with.
|
|
44
|
+
- `pubkit auth verify` runs the identical post-flight checks as `login`, so a
|
|
45
|
+
session cannot pass one and fail the other, and prints each finding.
|
|
46
|
+
- `BROWSER_PLATFORMS` derives from the login flow table — one source of truth.
|
|
47
|
+
|
|
48
|
+
## [0.2.0] — 2026-09-11
|
|
49
|
+
|
|
50
|
+
The release that makes `pubkit publish --to medium --confirm` actually work
|
|
51
|
+
unattended. 0.1.x could plan a Medium publish; it could not perform one.
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
|
|
55
|
+
- **Browser lifecycle** (`browserctl.BrowserPool`, `attached()`). This was the
|
|
56
|
+
missing seam: the runner called `adapter.authenticate()` on a `MediumAdapter`
|
|
57
|
+
whose `_page` was still `None`. Now one browser serves the whole run, one
|
|
58
|
+
context per platform so a Medium session is never presented to Substack, and
|
|
59
|
+
sessions are refreshed on the way out because cookies rotate. API-only runs
|
|
60
|
+
never launch Chromium at all.
|
|
61
|
+
- **Table rendering** (`render.tables`, `core.assets.materialise`). The planner
|
|
62
|
+
said `table_to_image` and listed the asset ids; nothing produced the files.
|
|
63
|
+
Tables now render to quantised PNGs at 2x, cached on cell content so a re-run
|
|
64
|
+
neither re-renders nor re-uploads an unchanged table. The source IR is never
|
|
65
|
+
mutated — the same document keeps native tables on Dev.to in the same run.
|
|
66
|
+
- `pubkit auth verify <platform>` — check a session before a publish depends on
|
|
67
|
+
it, rather than discovering it expired halfway through.
|
|
68
|
+
- **Browser integration tests** against a deliberately hostile fixture editor
|
|
69
|
+
that reproduces multiple content roots, image stripping, figures landing
|
|
70
|
+
above the caret, and uploads that sit on a `blob:` URL before resolving.
|
|
71
|
+
They skip cleanly where Chromium is unavailable.
|
|
72
|
+
|
|
73
|
+
### Fixed
|
|
74
|
+
|
|
75
|
+
- `marker_regex` was double-escaped, so every `fingerprint()` call threw
|
|
76
|
+
`SyntaxError: Invalid regular expression` inside the page. Verification was
|
|
77
|
+
therefore broken on every browser adapter. Found by the new integration
|
|
78
|
+
tests on their first run.
|
|
79
|
+
- `auth login` routed on a capability flag rather than the platform list, so
|
|
80
|
+
`pubkit auth login substack` took the API-token path and prompted for a token
|
|
81
|
+
that does not exist.
|
|
82
|
+
|
|
83
|
+
## [0.1.1] — 2026-09-11
|
|
84
|
+
|
|
85
|
+
### Added
|
|
86
|
+
|
|
87
|
+
- Docker image on the Playwright base (`ghcr.io/arunsingh/pubkit`), so browser
|
|
88
|
+
adapters work in CI without hand-installing Chromium's system libraries.
|
|
89
|
+
- `action.yml` — the repository is now a reusable GitHub Action.
|
|
90
|
+
- `CITATION.cff`, and a Homebrew formula template under `packaging/`.
|
|
91
|
+
|
|
92
|
+
### Fixed
|
|
93
|
+
|
|
94
|
+
- `pubkit doctor` ignored `PLAYWRIGHT_BROWSERS_PATH` and reported Chromium as
|
|
95
|
+
missing on managed images (CI runners, devcontainers, the Playwright Docker
|
|
96
|
+
image) where it was in fact installed — sending people off to fix a problem
|
|
97
|
+
they did not have. Found by running the published package in exactly such an
|
|
98
|
+
environment.
|
|
99
|
+
|
|
100
|
+
## [0.1.0] — 2026-09-11
|
|
101
|
+
|
|
102
|
+
First release. Extracted from a real, painful publishing run: a 15,000-word
|
|
103
|
+
illustrated three-part series to Medium that surfaced 21 distinct failure modes,
|
|
104
|
+
all catalogued in `docs/FAILURE-MODES.md`.
|
|
105
|
+
|
|
106
|
+
### Added
|
|
107
|
+
|
|
108
|
+
- **Post IR** — a canonical, platform-agnostic document model with a
|
|
109
|
+
content-addressed id used as the idempotency key everywhere.
|
|
110
|
+
- **Capability negotiation** — adapters declare what they support; the planner
|
|
111
|
+
emits explicit, printable degradations. `pubkit plan` shows them before
|
|
112
|
+
anything is sent.
|
|
113
|
+
- **Check pipeline** — placeholders, numeric assertions, cross-references,
|
|
114
|
+
number drift, assets, budget, structure. Pluggable via `pubkit.checks`.
|
|
115
|
+
- **Verified chunked transport** — per-chunk rolling-hash verification, probed
|
|
116
|
+
size ceilings, single-member gzip.
|
|
117
|
+
- **Browser adapter toolkit** — paste-based document replace with a block-count
|
|
118
|
+
assertion, reload-then-fingerprint verification, caption-anchored `File`
|
|
119
|
+
image insertion, Unicode-folding anchor resolution, tag-chip post-conditions.
|
|
120
|
+
- **Adapters** — Medium, Substack (browser); X, Dev.to, Hashnode (API).
|
|
121
|
+
- **Runner** — six resumable steps per (document, platform), sqlite state store,
|
|
122
|
+
two-phase publish for series, hash-pinned `--confirm` gate, per-platform rate
|
|
123
|
+
limiting and jittered retry.
|
|
124
|
+
- **CLI** — `init`, `validate`, `plan`, `publish`, `status`, `platforms`,
|
|
125
|
+
`doctor`, `auth login|logout|list`.
|
|
126
|
+
- **Workflows** — GitHub Actions examples and Airflow operators.
|
|
127
|
+
- **Extension points** — `pubkit.adapters`, `pubkit.renderers`, `pubkit.checks`,
|
|
128
|
+
`pubkit.hooks`, plus per-platform transforms.
|
|
129
|
+
|
|
130
|
+
### Known limitations
|
|
131
|
+
|
|
132
|
+
- Browser adapter selectors will need patching when a platform redesigns its
|
|
133
|
+
editor. They are isolated in one `EditorSelectors` dataclass per adapter
|
|
134
|
+
precisely so that is a small change.
|
|
135
|
+
- Table→image rendering requires the `render` extra; without it, tables degrade
|
|
136
|
+
to lists on platforms that lack table support.
|
|
137
|
+
- X media upload uses the v1.1 endpoint, which is what the v2 API still
|
|
138
|
+
requires.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pubkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Publish one source to many platforms, safely. Medium, Substack, X, Dev.to, Hashnode.
|
|
5
5
|
Project-URL: Homepage, https://github.com/arunsingh/pubkit
|
|
6
6
|
Project-URL: Issues, https://github.com/arunsingh/pubkit/issues
|
|
@@ -109,9 +109,10 @@ pubkit validate content/series/
|
|
|
109
109
|
# 2. See exactly what each platform will get — including every degradation.
|
|
110
110
|
pubkit plan content/series/ --to medium,devto,x
|
|
111
111
|
|
|
112
|
-
# 3. Sign in. pubkit never accepts a password.
|
|
113
|
-
pubkit auth login medium #
|
|
112
|
+
# 3. Sign in, once. pubkit never accepts a password.
|
|
113
|
+
pubkit auth login medium # a window opens; you sign in; it keeps the session
|
|
114
114
|
pubkit auth login devto # API token → OS keychain
|
|
115
|
+
pubkit auth verify medium # confirm before a publish depends on it
|
|
115
116
|
|
|
116
117
|
# 4. Drafts everywhere. Safe to re-run; a dropped connection costs one step.
|
|
117
118
|
pubkit publish content/series/ --to medium,devto,x
|
|
@@ -138,6 +139,64 @@ x: part-1 (bf9abfe78ce4)
|
|
|
138
139
|
|
|
139
140
|
Nothing is a surprise at publish time. That is the whole design goal.
|
|
140
141
|
|
|
142
|
+
## Sign in once, then it runs on its own
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
pubkit auth login medium
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
A real browser window opens at Medium's login page. You sign in — password
|
|
149
|
+
manager, MFA, device confirmation, whatever it asks for.
|
|
150
|
+
|
|
151
|
+
Before that window opens, pubkit checks the things that would otherwise cost you
|
|
152
|
+
five silent minutes: playwright, a browser, whether the login host is even
|
|
153
|
+
reachable from here, whether the session store is writable, and whether you
|
|
154
|
+
already have a working session. After you sign in, it checks the cookies are for
|
|
155
|
+
the platform's own domain, that the ones which authorise writing are present,
|
|
156
|
+
how long they last — and then fetches a page only a signed-in user can see.
|
|
157
|
+
**The session is saved only once it has been proven to work.** A saved session
|
|
158
|
+
that does not work is worse than none, because the failure moves into the middle
|
|
159
|
+
of a publish.
|
|
160
|
+
|
|
161
|
+
If the sign-in button does nothing at all — no error, no request, the single
|
|
162
|
+
most confusing failure there is — pubkit says so, and says which of the two
|
|
163
|
+
causes it was:
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
✗ timeout: no sign-in seen in 5 minutes
|
|
167
|
+
! last URL: https://medium.com/m/signin
|
|
168
|
+
✗ form submission: the page never sent a sign-in request
|
|
169
|
+
the button was not wired up, or a script it waits on never finished —
|
|
170
|
+
try: pubkit auth login medium --attach
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`--attach` watches a Chrome you started yourself rather than launching one:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
open -a "Google Chrome" --args --remote-debugging-port=9222
|
|
177
|
+
pubkit auth login medium --attach
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
pubkit drives nothing about that sign-in; it watches a tab you are already in.
|
|
181
|
+
It does not try to look like something it is not, and it does not attempt to
|
|
182
|
+
defeat a bot check — where a platform declines an automated browser, `--attach`
|
|
183
|
+
gets out of the way instead.
|
|
184
|
+
|
|
185
|
+
From then on, unattended:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
pubkit publish content/ --to medium,devto,x --confirm
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
One browser serves the whole run, one context per platform so a Medium session
|
|
192
|
+
is never presented to Substack, and sessions refresh on the way out because
|
|
193
|
+
cookies rotate. Publishing only to API platforms never launches Chromium at all.
|
|
194
|
+
|
|
195
|
+
pubkit sees cookies. It never sees, types or stores a password — which is both
|
|
196
|
+
the right security posture and the only thing that works, since platforms
|
|
197
|
+
increasingly gate login behind challenges an automation layer has no business
|
|
198
|
+
trying to defeat.
|
|
199
|
+
|
|
141
200
|
## Write once
|
|
142
201
|
|
|
143
202
|
````markdown
|
|
@@ -287,12 +346,16 @@ validate >> PubkitPublishOperator.expand_fanout(
|
|
|
287
346
|
|
|
288
347
|
## Status
|
|
289
348
|
|
|
290
|
-
v0.
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
349
|
+
v0.3. The core, the checks, the planner, the state machine, the browser pool,
|
|
350
|
+
the table renderer and the sign-in validator are covered by 68 tests, and the
|
|
351
|
+
whole pipeline is exercised end-to-end against a real published series in
|
|
352
|
+
`examples/inside-ai-infra/`. Two hostile fixtures carry most of the weight: an
|
|
353
|
+
editor that strips images, splits content roots and lands figures above the
|
|
354
|
+
caret, and a sign-in page that fails the six ways real ones do.
|
|
355
|
+
|
|
356
|
+
Selectors remain the part most likely to need a patch when a platform ships a
|
|
357
|
+
redesign, which is exactly why they are isolated in one dataclass per adapter —
|
|
358
|
+
and login flows in one `LoginFlow` per platform.
|
|
296
359
|
|
|
297
360
|
## Docs
|
|
298
361
|
|
|
@@ -59,9 +59,10 @@ pubkit validate content/series/
|
|
|
59
59
|
# 2. See exactly what each platform will get — including every degradation.
|
|
60
60
|
pubkit plan content/series/ --to medium,devto,x
|
|
61
61
|
|
|
62
|
-
# 3. Sign in. pubkit never accepts a password.
|
|
63
|
-
pubkit auth login medium #
|
|
62
|
+
# 3. Sign in, once. pubkit never accepts a password.
|
|
63
|
+
pubkit auth login medium # a window opens; you sign in; it keeps the session
|
|
64
64
|
pubkit auth login devto # API token → OS keychain
|
|
65
|
+
pubkit auth verify medium # confirm before a publish depends on it
|
|
65
66
|
|
|
66
67
|
# 4. Drafts everywhere. Safe to re-run; a dropped connection costs one step.
|
|
67
68
|
pubkit publish content/series/ --to medium,devto,x
|
|
@@ -88,6 +89,64 @@ x: part-1 (bf9abfe78ce4)
|
|
|
88
89
|
|
|
89
90
|
Nothing is a surprise at publish time. That is the whole design goal.
|
|
90
91
|
|
|
92
|
+
## Sign in once, then it runs on its own
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
pubkit auth login medium
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
A real browser window opens at Medium's login page. You sign in — password
|
|
99
|
+
manager, MFA, device confirmation, whatever it asks for.
|
|
100
|
+
|
|
101
|
+
Before that window opens, pubkit checks the things that would otherwise cost you
|
|
102
|
+
five silent minutes: playwright, a browser, whether the login host is even
|
|
103
|
+
reachable from here, whether the session store is writable, and whether you
|
|
104
|
+
already have a working session. After you sign in, it checks the cookies are for
|
|
105
|
+
the platform's own domain, that the ones which authorise writing are present,
|
|
106
|
+
how long they last — and then fetches a page only a signed-in user can see.
|
|
107
|
+
**The session is saved only once it has been proven to work.** A saved session
|
|
108
|
+
that does not work is worse than none, because the failure moves into the middle
|
|
109
|
+
of a publish.
|
|
110
|
+
|
|
111
|
+
If the sign-in button does nothing at all — no error, no request, the single
|
|
112
|
+
most confusing failure there is — pubkit says so, and says which of the two
|
|
113
|
+
causes it was:
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
✗ timeout: no sign-in seen in 5 minutes
|
|
117
|
+
! last URL: https://medium.com/m/signin
|
|
118
|
+
✗ form submission: the page never sent a sign-in request
|
|
119
|
+
the button was not wired up, or a script it waits on never finished —
|
|
120
|
+
try: pubkit auth login medium --attach
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`--attach` watches a Chrome you started yourself rather than launching one:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
open -a "Google Chrome" --args --remote-debugging-port=9222
|
|
127
|
+
pubkit auth login medium --attach
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
pubkit drives nothing about that sign-in; it watches a tab you are already in.
|
|
131
|
+
It does not try to look like something it is not, and it does not attempt to
|
|
132
|
+
defeat a bot check — where a platform declines an automated browser, `--attach`
|
|
133
|
+
gets out of the way instead.
|
|
134
|
+
|
|
135
|
+
From then on, unattended:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
pubkit publish content/ --to medium,devto,x --confirm
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
One browser serves the whole run, one context per platform so a Medium session
|
|
142
|
+
is never presented to Substack, and sessions refresh on the way out because
|
|
143
|
+
cookies rotate. Publishing only to API platforms never launches Chromium at all.
|
|
144
|
+
|
|
145
|
+
pubkit sees cookies. It never sees, types or stores a password — which is both
|
|
146
|
+
the right security posture and the only thing that works, since platforms
|
|
147
|
+
increasingly gate login behind challenges an automation layer has no business
|
|
148
|
+
trying to defeat.
|
|
149
|
+
|
|
91
150
|
## Write once
|
|
92
151
|
|
|
93
152
|
````markdown
|
|
@@ -237,12 +296,16 @@ validate >> PubkitPublishOperator.expand_fanout(
|
|
|
237
296
|
|
|
238
297
|
## Status
|
|
239
298
|
|
|
240
|
-
v0.
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
299
|
+
v0.3. The core, the checks, the planner, the state machine, the browser pool,
|
|
300
|
+
the table renderer and the sign-in validator are covered by 68 tests, and the
|
|
301
|
+
whole pipeline is exercised end-to-end against a real published series in
|
|
302
|
+
`examples/inside-ai-infra/`. Two hostile fixtures carry most of the weight: an
|
|
303
|
+
editor that strips images, splits content roots and lands figures above the
|
|
304
|
+
caret, and a sign-in page that fails the six ways real ones do.
|
|
305
|
+
|
|
306
|
+
Selectors remain the part most likely to need a patch when a platform ships a
|
|
307
|
+
redesign, which is exactly why they are isolated in one dataclass per adapter —
|
|
308
|
+
and login flows in one `LoginFlow` per platform.
|
|
246
309
|
|
|
247
310
|
## Docs
|
|
248
311
|
|
|
@@ -220,3 +220,72 @@ Publishing one document to five platforms is a fan-out where any leg can fail.
|
|
|
220
220
|
> **pubkit:** per-platform token-bucket limiter, bounded exponential backoff
|
|
221
221
|
> with jitter, and per-leg result reporting. One platform failing never blocks
|
|
222
222
|
> the others; the run exits non-zero with a machine-readable summary.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Class E — Sign-in
|
|
227
|
+
|
|
228
|
+
A login is the only step a human performs and a machine has to judge, and every
|
|
229
|
+
failure in it is silent. These six were all observed against live platforms.
|
|
230
|
+
|
|
231
|
+
### E1. The submit button does nothing at all
|
|
232
|
+
A sign-in page opens, the email is typed, Continue is clicked, and nothing
|
|
233
|
+
happens. No error, no navigation, no network request, nothing in the console.
|
|
234
|
+
It is the hardest login failure to report because from the outside there is
|
|
235
|
+
nothing to report.
|
|
236
|
+
|
|
237
|
+
> **pubkit:** the page is watched while the human works. Every POST/fetch to
|
|
238
|
+
> the platform is counted, so "you did not finish" and "the button never fired"
|
|
239
|
+
> stop being the same silence. Zero submissions after a timeout is reported as
|
|
240
|
+
> a failure in its own right, with `--attach` as the next step.
|
|
241
|
+
|
|
242
|
+
### E2. A bot check that never completes
|
|
243
|
+
Identical symptom to E1, opposite cause: the click *is* handled, but the
|
|
244
|
+
handler waits on a challenge script that a freshly launched browser profile
|
|
245
|
+
never gets a token from. Indistinguishable by eye.
|
|
246
|
+
|
|
247
|
+
> **pubkit:** requests to known challenge hosts (reCAPTCHA, hCaptcha, Arkose,
|
|
248
|
+
> PerimeterX, DataDome, Turnstile) are recorded separately, and a failed one is
|
|
249
|
+
> named in the diagnosis. The remedy is `pubkit auth login <platform> --attach`,
|
|
250
|
+
> which watches a Chrome the user started themselves. pubkit does not attempt
|
|
251
|
+
> to defeat a challenge; it gets out of the way of one.
|
|
252
|
+
|
|
253
|
+
### E3. Waiting five minutes for something that could never happen
|
|
254
|
+
playwright not installed, no browser downloaded, the login host unreachable
|
|
255
|
+
behind a VPN or proxy, a session directory that is not writable. Each produces
|
|
256
|
+
the same blank window and the same long wait.
|
|
257
|
+
|
|
258
|
+
> **pubkit:** `login_preflight()` runs first, synchronously and cheaply. A
|
|
259
|
+
> window is never opened when it cannot work, and each failure names its own
|
|
260
|
+
> fix. It also says when a valid session already exists, so the user is not
|
|
261
|
+
> signed out of something that was working.
|
|
262
|
+
|
|
263
|
+
### E4. The flow stops at the identity provider
|
|
264
|
+
Sign-in goes through Google or a corporate SSO hop, the redirect back never
|
|
265
|
+
completes, and what gets saved is a session for `accounts.google.com` with no
|
|
266
|
+
cookie for the platform at all.
|
|
267
|
+
|
|
268
|
+
> **pubkit:** the saved state is checked for cookies on the platform's own
|
|
269
|
+
> registrable domain, and for the specific cookie names that authorise writing.
|
|
270
|
+
> Missing ones are named.
|
|
271
|
+
|
|
272
|
+
### E5. A session that validates and does not work
|
|
273
|
+
Both session cookies present, correct domain, good expiry — and the signed-in
|
|
274
|
+
page still bounces back to sign-in. Cookie inspection cannot detect this; only
|
|
275
|
+
asking the platform can.
|
|
276
|
+
|
|
277
|
+
> **pubkit:** before anything is written, a page that renders only for a
|
|
278
|
+
> signed-in user is fetched and checked for a signed-in marker. A session is
|
|
279
|
+
> saved only once it has been *proven* to work. A saved session that does not
|
|
280
|
+
> work is worse than none, because it moves the failure into the middle of a
|
|
281
|
+
> publish.
|
|
282
|
+
|
|
283
|
+
### E6. A session that expires quietly
|
|
284
|
+
A session cookie with no persistent expiry dies with the browser; one with a
|
|
285
|
+
two-day expiry dies mid-week. Either way the failure surfaces days later,
|
|
286
|
+
inside a publish, looking like something else.
|
|
287
|
+
|
|
288
|
+
> **pubkit:** expiry is reported at login time, and a lifetime under a week is
|
|
289
|
+
> a warning with the number in it. `pubkit auth verify` re-runs the identical
|
|
290
|
+
> checks on demand — same code path, so a session cannot pass one and fail the
|
|
291
|
+
> other.
|
|
@@ -6,7 +6,7 @@ from .core.loader import load_document, load_series # noqa: F401
|
|
|
6
6
|
from .core.runner import Pipeline, RunReport # noqa: F401
|
|
7
7
|
from .registry import build_adapter, list_adapters, register # noqa: F401
|
|
8
8
|
|
|
9
|
-
__version__ = "0.
|
|
9
|
+
__version__ = "0.3.0"
|
|
10
10
|
__all__ = [
|
|
11
11
|
"Document", "Series", "Asset", "Figure", "Table", "Heading", "Paragraph", "Code",
|
|
12
12
|
"Pipeline", "RunReport", "load_document", "load_series",
|
|
@@ -68,7 +68,7 @@ class MediumAdapter(BrowserAdapter):
|
|
|
68
68
|
|
|
69
69
|
login_url = "https://medium.com/m/signin"
|
|
70
70
|
new_story_url = "https://medium.com/new-story"
|
|
71
|
-
marker_regex = r"
|
|
71
|
+
marker_regex = r"\[\[\s*IMAGE"
|
|
72
72
|
|
|
73
73
|
def __init__(self, page=None, upload=None) -> None:
|
|
74
74
|
super().__init__()
|