htmlship 0.1.2__tar.gz → 0.1.4__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.
- {htmlship-0.1.2 → htmlship-0.1.4}/.gitignore +1 -0
- {htmlship-0.1.2 → htmlship-0.1.4}/PKG-INFO +31 -23
- {htmlship-0.1.2 → htmlship-0.1.4}/README.md +30 -22
- {htmlship-0.1.2 → htmlship-0.1.4}/pyproject.toml +1 -1
- {htmlship-0.1.2 → htmlship-0.1.4}/src/htmlship/__init__.py +10 -10
- htmlship-0.1.4/src/htmlship/_version.py +1 -0
- {htmlship-0.1.2 → htmlship-0.1.4}/src/htmlship/cli.py +35 -35
- {htmlship-0.1.2 → htmlship-0.1.4}/src/htmlship/client.py +17 -17
- {htmlship-0.1.2 → htmlship-0.1.4}/src/htmlship/exceptions.py +1 -1
- {htmlship-0.1.2 → htmlship-0.1.4}/src/htmlship/models.py +14 -12
- htmlship-0.1.4/src/htmlship_mcp/__init__.py +1 -0
- {htmlship-0.1.2 → htmlship-0.1.4}/src/htmlship_mcp/server.py +35 -28
- htmlship-0.1.4/src/htmlship_server/__init__.py +1 -0
- htmlship-0.1.4/src/htmlship_server/db_models/__init__.py +3 -0
- htmlship-0.1.2/src/htmlship_server/db_models/paste.py → htmlship-0.1.4/src/htmlship_server/db_models/page.py +6 -6
- {htmlship-0.1.2 → htmlship-0.1.4}/src/htmlship_server/exceptions.py +1 -1
- {htmlship-0.1.2 → htmlship-0.1.4}/src/htmlship_server/main.py +7 -2
- htmlship-0.1.2/src/htmlship_server/routers/pastes.py → htmlship-0.1.4/src/htmlship_server/routers/pages.py +76 -76
- {htmlship-0.1.2 → htmlship-0.1.4}/src/htmlship_server/routers/view.py +24 -24
- htmlship-0.1.2/src/htmlship_server/schemas/pastes.py → htmlship-0.1.4/src/htmlship_server/schemas/pages.py +5 -5
- {htmlship-0.1.2 → htmlship-0.1.4}/src/htmlship_server/storage.py +2 -2
- htmlship-0.1.2/src/htmlship/_version.py +0 -1
- htmlship-0.1.2/src/htmlship_mcp/__init__.py +0 -1
- htmlship-0.1.2/src/htmlship_server/__init__.py +0 -1
- htmlship-0.1.2/src/htmlship_server/db_models/__init__.py +0 -3
- {htmlship-0.1.2 → htmlship-0.1.4}/src/htmlship_server/config.py +0 -0
- {htmlship-0.1.2 → htmlship-0.1.4}/src/htmlship_server/database.py +0 -0
- {htmlship-0.1.2 → htmlship-0.1.4}/src/htmlship_server/middleware.py +0 -0
- {htmlship-0.1.2 → htmlship-0.1.4}/src/htmlship_server/routers/__init__.py +0 -0
- {htmlship-0.1.2 → htmlship-0.1.4}/src/htmlship_server/routers/meta.py +0 -0
- {htmlship-0.1.2 → htmlship-0.1.4}/src/htmlship_server/schemas/__init__.py +0 -0
- {htmlship-0.1.2 → htmlship-0.1.4}/src/htmlship_server/security.py +0 -0
- {htmlship-0.1.2 → htmlship-0.1.4}/src/htmlship_server/slugs.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: htmlship
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Host and share HTML pages from LLMs and coding agents in one line.
|
|
5
5
|
Project-URL: Homepage, https://htmlship.com
|
|
6
6
|
Project-URL: Repository, https://github.com/htmlship/htmlship
|
|
@@ -50,12 +50,13 @@ HTMLShip has four surfaces:
|
|
|
50
50
|
|
|
51
51
|
- a public Python library and CLI (`htmlship` on PyPI)
|
|
52
52
|
- a Node.js CLI and MCP server (`htmlship` on npm — no install required, runs via `npx`)
|
|
53
|
-
- a FastAPI service for creating, updating, deleting, and viewing HTML
|
|
53
|
+
- a FastAPI service for creating, updating, deleting, and viewing HTML pages
|
|
54
54
|
- a stdio MCP server (the `mcp` subcommand of both CLIs) for agent clients
|
|
55
55
|
|
|
56
56
|
```bash
|
|
57
57
|
# Node — runs immediately, no install
|
|
58
58
|
npx htmlship publish report.html
|
|
59
|
+
npx htmlship publish report.html --password "demo-pass"
|
|
59
60
|
|
|
60
61
|
# Python
|
|
61
62
|
pip install htmlship
|
|
@@ -64,15 +65,20 @@ pip install htmlship
|
|
|
64
65
|
```python
|
|
65
66
|
import htmlship
|
|
66
67
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
page = htmlship.publish(
|
|
69
|
+
"<h1>Hello</h1>",
|
|
70
|
+
title="Demo",
|
|
71
|
+
password="demo-pass",
|
|
72
|
+
expires_in=60, # minutes
|
|
73
|
+
)
|
|
74
|
+
print(page.url)
|
|
75
|
+
print(page.owner_key) # save this to update or delete the page later
|
|
70
76
|
```
|
|
71
77
|
|
|
72
78
|
```bash
|
|
73
|
-
curl -X POST https://api.htmlship.com/api/v1/
|
|
79
|
+
curl -X POST https://api.htmlship.com/api/v1/pages \
|
|
74
80
|
-H "Content-Type: application/json" \
|
|
75
|
-
-d '{"html":"<h1>Hello</h1>","title":"Demo"}'
|
|
81
|
+
-d '{"html":"<h1>Hello</h1>","title":"Demo","password":"demo-pass"}'
|
|
76
82
|
```
|
|
77
83
|
|
|
78
84
|
See [`htmlship-implementation-spec.md`](./htmlship-implementation-spec.md) for the product spec and [`DEPLOY.md`](./DEPLOY.md) for the production runbook.
|
|
@@ -84,27 +90,28 @@ The module-level helpers use `https://api.htmlship.com` by default. Override wit
|
|
|
84
90
|
```python
|
|
85
91
|
import htmlship
|
|
86
92
|
|
|
87
|
-
|
|
93
|
+
page = htmlship.publish(
|
|
88
94
|
"<h1>Hello</h1>",
|
|
89
95
|
title="Demo",
|
|
90
96
|
password="optional-password",
|
|
91
97
|
expires_in=1440, # minutes (24 hours)
|
|
92
98
|
)
|
|
93
99
|
|
|
94
|
-
fresh = htmlship.get(
|
|
95
|
-
updated = htmlship.update(
|
|
96
|
-
htmlship.delete(updated.slug, owner_key=
|
|
100
|
+
fresh = htmlship.get(page.slug)
|
|
101
|
+
updated = htmlship.update(page.slug, "<h1>Updated</h1>", owner_key=page.owner_key)
|
|
102
|
+
htmlship.delete(updated.slug, owner_key=page.owner_key)
|
|
97
103
|
```
|
|
98
104
|
|
|
99
|
-
`owner_key` is returned only when a
|
|
105
|
+
`owner_key` is returned only when a page is created. It is the publisher-only secret required for updates and deletes, and the API does not return it again from metadata calls. `password` is only a view-time gate for readers; it does not authorize mutations.
|
|
100
106
|
|
|
101
107
|
## CLI
|
|
102
108
|
|
|
103
|
-
The CLI is shipped both as a Python package (`pip install htmlship`) and an npm package (`npx htmlship` or `npm i -g htmlship`). The two share the same on-disk owner-key store, so a
|
|
109
|
+
The CLI is shipped both as a Python package (`pip install htmlship`) and an npm package (`npx htmlship` or `npm i -g htmlship`). The two share the same on-disk owner-key store, so a page created in one is editable from the other.
|
|
104
110
|
|
|
105
111
|
```bash
|
|
106
112
|
htmlship publish report.html
|
|
107
113
|
cat report.html | htmlship publish -
|
|
114
|
+
htmlship publish report.html --password "demo-pass"
|
|
108
115
|
htmlship publish --file report.html --title "Q4 Report" --expires-in 60
|
|
109
116
|
|
|
110
117
|
htmlship get <slug>
|
|
@@ -117,10 +124,11 @@ Equivalent npx form (no install):
|
|
|
117
124
|
|
|
118
125
|
```bash
|
|
119
126
|
npx htmlship publish report.html
|
|
127
|
+
npx htmlship publish report.html --password "demo-pass"
|
|
120
128
|
npx htmlship list-mine
|
|
121
129
|
```
|
|
122
130
|
|
|
123
|
-
The CLI stores owner keys in `~/.htmlship/keys.json` so `update`, `delete`, and `list-mine` can work with
|
|
131
|
+
The CLI stores owner keys in `~/.htmlship/keys.json` so `update`, `delete`, and `list-mine` can work with pages you created locally. Set `HTMLSHIP_KEYS_DIR` to use another key-store directory, and set `HTMLSHIP_API_URL` to point the CLI at a local or staging API.
|
|
124
132
|
|
|
125
133
|
## API
|
|
126
134
|
|
|
@@ -130,11 +138,11 @@ Base URL: `https://api.htmlship.com`.
|
|
|
130
138
|
| --- | --- | --- |
|
|
131
139
|
| `GET` | `/health` | Health check with service version. |
|
|
132
140
|
| `GET` | `/version` | Service version. |
|
|
133
|
-
| `POST` | `/api/v1/
|
|
134
|
-
| `GET` | `/api/v1/
|
|
135
|
-
| `PATCH` | `/api/v1/
|
|
136
|
-
| `DELETE` | `/api/v1/
|
|
137
|
-
| `POST` | `/api/v1/
|
|
141
|
+
| `POST` | `/api/v1/pages` | Create a page. |
|
|
142
|
+
| `GET` | `/api/v1/pages/{slug}` | Fetch page metadata. |
|
|
143
|
+
| `PATCH` | `/api/v1/pages/{slug}` | Replace HTML or title. Requires `X-Owner-Key`. |
|
|
144
|
+
| `DELETE` | `/api/v1/pages/{slug}` | Soft-delete a page. Requires `X-Owner-Key`. |
|
|
145
|
+
| `POST` | `/api/v1/pages/{slug}/version` | Create a new page linked to an existing parent slug. |
|
|
138
146
|
|
|
139
147
|
Create payload:
|
|
140
148
|
|
|
@@ -185,7 +193,7 @@ curl "http://localhost:8000/<slug>?_host=view.htmlship.com"
|
|
|
185
193
|
|
|
186
194
|
HTMLShip ships a stdio MCP server with three tools:
|
|
187
195
|
|
|
188
|
-
- `publish_html`
|
|
196
|
+
- `publish_html` (accepts optional `password`)
|
|
189
197
|
- `fetch_html`
|
|
190
198
|
- `update_html`
|
|
191
199
|
|
|
@@ -278,7 +286,7 @@ The server reads `.env` via Pydantic settings.
|
|
|
278
286
|
| `DATABASE_URL` | `postgresql+asyncpg://htmlship:htmlship@localhost:5433/htmlship` | Async SQLAlchemy database URL. |
|
|
279
287
|
| `PUBLIC_BASE_DOMAIN` | `htmlship.com` | Base domain used to derive host routing. |
|
|
280
288
|
| `API_BASE_URL` | `https://api.htmlship.com` | Public API URL setting. |
|
|
281
|
-
| `VIEW_BASE_URL` | `https://view.htmlship.com` | Public view URL used in
|
|
289
|
+
| `VIEW_BASE_URL` | `https://view.htmlship.com` | Public view URL used in page responses. |
|
|
282
290
|
| `LANDING_BASE_URL` | `https://htmlship.com` | Public landing URL. |
|
|
283
291
|
| `SPACES_BUCKET` | empty | If empty, use local blob storage; otherwise use DigitalOcean Spaces/S3. |
|
|
284
292
|
| `SPACES_REGION` | `nyc3` | Spaces/S3 region. |
|
|
@@ -288,13 +296,13 @@ The server reads `.env` via Pydantic settings.
|
|
|
288
296
|
| `ENVIRONMENT` | `development` | Enables API docs outside production and secure cookies in production. |
|
|
289
297
|
| `LOG_LEVEL` | `info` | Application log level. |
|
|
290
298
|
| `MAX_PAYLOAD_BYTES` | `10485760` | Server-side HTML size limit. |
|
|
291
|
-
| `DEFAULT_EXPIRES_IN_MINUTES` | empty | Optional default TTL (minutes) for new
|
|
299
|
+
| `DEFAULT_EXPIRES_IN_MINUTES` | empty | Optional default TTL (minutes) for new pages. |
|
|
292
300
|
|
|
293
301
|
## Architecture
|
|
294
302
|
|
|
295
303
|
One FastAPI process hosts the landing page, JSON API, and view renderer. `HostRoutingMiddleware` classifies requests by host and prevents API routes from being served on the view host.
|
|
296
304
|
|
|
297
|
-
Postgres stores
|
|
305
|
+
Postgres stores page metadata, owner-key/password hashes, expiry, view counts, and parent-version links. HTML bodies are stored as blobs, either in `LocalBlobStore` for development/tests or DigitalOcean Spaces in production.
|
|
298
306
|
|
|
299
307
|
## Project Layout
|
|
300
308
|
|
|
@@ -6,12 +6,13 @@ HTMLShip has four surfaces:
|
|
|
6
6
|
|
|
7
7
|
- a public Python library and CLI (`htmlship` on PyPI)
|
|
8
8
|
- a Node.js CLI and MCP server (`htmlship` on npm — no install required, runs via `npx`)
|
|
9
|
-
- a FastAPI service for creating, updating, deleting, and viewing HTML
|
|
9
|
+
- a FastAPI service for creating, updating, deleting, and viewing HTML pages
|
|
10
10
|
- a stdio MCP server (the `mcp` subcommand of both CLIs) for agent clients
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
13
|
# Node — runs immediately, no install
|
|
14
14
|
npx htmlship publish report.html
|
|
15
|
+
npx htmlship publish report.html --password "demo-pass"
|
|
15
16
|
|
|
16
17
|
# Python
|
|
17
18
|
pip install htmlship
|
|
@@ -20,15 +21,20 @@ pip install htmlship
|
|
|
20
21
|
```python
|
|
21
22
|
import htmlship
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
page = htmlship.publish(
|
|
25
|
+
"<h1>Hello</h1>",
|
|
26
|
+
title="Demo",
|
|
27
|
+
password="demo-pass",
|
|
28
|
+
expires_in=60, # minutes
|
|
29
|
+
)
|
|
30
|
+
print(page.url)
|
|
31
|
+
print(page.owner_key) # save this to update or delete the page later
|
|
26
32
|
```
|
|
27
33
|
|
|
28
34
|
```bash
|
|
29
|
-
curl -X POST https://api.htmlship.com/api/v1/
|
|
35
|
+
curl -X POST https://api.htmlship.com/api/v1/pages \
|
|
30
36
|
-H "Content-Type: application/json" \
|
|
31
|
-
-d '{"html":"<h1>Hello</h1>","title":"Demo"}'
|
|
37
|
+
-d '{"html":"<h1>Hello</h1>","title":"Demo","password":"demo-pass"}'
|
|
32
38
|
```
|
|
33
39
|
|
|
34
40
|
See [`htmlship-implementation-spec.md`](./htmlship-implementation-spec.md) for the product spec and [`DEPLOY.md`](./DEPLOY.md) for the production runbook.
|
|
@@ -40,27 +46,28 @@ The module-level helpers use `https://api.htmlship.com` by default. Override wit
|
|
|
40
46
|
```python
|
|
41
47
|
import htmlship
|
|
42
48
|
|
|
43
|
-
|
|
49
|
+
page = htmlship.publish(
|
|
44
50
|
"<h1>Hello</h1>",
|
|
45
51
|
title="Demo",
|
|
46
52
|
password="optional-password",
|
|
47
53
|
expires_in=1440, # minutes (24 hours)
|
|
48
54
|
)
|
|
49
55
|
|
|
50
|
-
fresh = htmlship.get(
|
|
51
|
-
updated = htmlship.update(
|
|
52
|
-
htmlship.delete(updated.slug, owner_key=
|
|
56
|
+
fresh = htmlship.get(page.slug)
|
|
57
|
+
updated = htmlship.update(page.slug, "<h1>Updated</h1>", owner_key=page.owner_key)
|
|
58
|
+
htmlship.delete(updated.slug, owner_key=page.owner_key)
|
|
53
59
|
```
|
|
54
60
|
|
|
55
|
-
`owner_key` is returned only when a
|
|
61
|
+
`owner_key` is returned only when a page is created. It is the publisher-only secret required for updates and deletes, and the API does not return it again from metadata calls. `password` is only a view-time gate for readers; it does not authorize mutations.
|
|
56
62
|
|
|
57
63
|
## CLI
|
|
58
64
|
|
|
59
|
-
The CLI is shipped both as a Python package (`pip install htmlship`) and an npm package (`npx htmlship` or `npm i -g htmlship`). The two share the same on-disk owner-key store, so a
|
|
65
|
+
The CLI is shipped both as a Python package (`pip install htmlship`) and an npm package (`npx htmlship` or `npm i -g htmlship`). The two share the same on-disk owner-key store, so a page created in one is editable from the other.
|
|
60
66
|
|
|
61
67
|
```bash
|
|
62
68
|
htmlship publish report.html
|
|
63
69
|
cat report.html | htmlship publish -
|
|
70
|
+
htmlship publish report.html --password "demo-pass"
|
|
64
71
|
htmlship publish --file report.html --title "Q4 Report" --expires-in 60
|
|
65
72
|
|
|
66
73
|
htmlship get <slug>
|
|
@@ -73,10 +80,11 @@ Equivalent npx form (no install):
|
|
|
73
80
|
|
|
74
81
|
```bash
|
|
75
82
|
npx htmlship publish report.html
|
|
83
|
+
npx htmlship publish report.html --password "demo-pass"
|
|
76
84
|
npx htmlship list-mine
|
|
77
85
|
```
|
|
78
86
|
|
|
79
|
-
The CLI stores owner keys in `~/.htmlship/keys.json` so `update`, `delete`, and `list-mine` can work with
|
|
87
|
+
The CLI stores owner keys in `~/.htmlship/keys.json` so `update`, `delete`, and `list-mine` can work with pages you created locally. Set `HTMLSHIP_KEYS_DIR` to use another key-store directory, and set `HTMLSHIP_API_URL` to point the CLI at a local or staging API.
|
|
80
88
|
|
|
81
89
|
## API
|
|
82
90
|
|
|
@@ -86,11 +94,11 @@ Base URL: `https://api.htmlship.com`.
|
|
|
86
94
|
| --- | --- | --- |
|
|
87
95
|
| `GET` | `/health` | Health check with service version. |
|
|
88
96
|
| `GET` | `/version` | Service version. |
|
|
89
|
-
| `POST` | `/api/v1/
|
|
90
|
-
| `GET` | `/api/v1/
|
|
91
|
-
| `PATCH` | `/api/v1/
|
|
92
|
-
| `DELETE` | `/api/v1/
|
|
93
|
-
| `POST` | `/api/v1/
|
|
97
|
+
| `POST` | `/api/v1/pages` | Create a page. |
|
|
98
|
+
| `GET` | `/api/v1/pages/{slug}` | Fetch page metadata. |
|
|
99
|
+
| `PATCH` | `/api/v1/pages/{slug}` | Replace HTML or title. Requires `X-Owner-Key`. |
|
|
100
|
+
| `DELETE` | `/api/v1/pages/{slug}` | Soft-delete a page. Requires `X-Owner-Key`. |
|
|
101
|
+
| `POST` | `/api/v1/pages/{slug}/version` | Create a new page linked to an existing parent slug. |
|
|
94
102
|
|
|
95
103
|
Create payload:
|
|
96
104
|
|
|
@@ -141,7 +149,7 @@ curl "http://localhost:8000/<slug>?_host=view.htmlship.com"
|
|
|
141
149
|
|
|
142
150
|
HTMLShip ships a stdio MCP server with three tools:
|
|
143
151
|
|
|
144
|
-
- `publish_html`
|
|
152
|
+
- `publish_html` (accepts optional `password`)
|
|
145
153
|
- `fetch_html`
|
|
146
154
|
- `update_html`
|
|
147
155
|
|
|
@@ -234,7 +242,7 @@ The server reads `.env` via Pydantic settings.
|
|
|
234
242
|
| `DATABASE_URL` | `postgresql+asyncpg://htmlship:htmlship@localhost:5433/htmlship` | Async SQLAlchemy database URL. |
|
|
235
243
|
| `PUBLIC_BASE_DOMAIN` | `htmlship.com` | Base domain used to derive host routing. |
|
|
236
244
|
| `API_BASE_URL` | `https://api.htmlship.com` | Public API URL setting. |
|
|
237
|
-
| `VIEW_BASE_URL` | `https://view.htmlship.com` | Public view URL used in
|
|
245
|
+
| `VIEW_BASE_URL` | `https://view.htmlship.com` | Public view URL used in page responses. |
|
|
238
246
|
| `LANDING_BASE_URL` | `https://htmlship.com` | Public landing URL. |
|
|
239
247
|
| `SPACES_BUCKET` | empty | If empty, use local blob storage; otherwise use DigitalOcean Spaces/S3. |
|
|
240
248
|
| `SPACES_REGION` | `nyc3` | Spaces/S3 region. |
|
|
@@ -244,13 +252,13 @@ The server reads `.env` via Pydantic settings.
|
|
|
244
252
|
| `ENVIRONMENT` | `development` | Enables API docs outside production and secure cookies in production. |
|
|
245
253
|
| `LOG_LEVEL` | `info` | Application log level. |
|
|
246
254
|
| `MAX_PAYLOAD_BYTES` | `10485760` | Server-side HTML size limit. |
|
|
247
|
-
| `DEFAULT_EXPIRES_IN_MINUTES` | empty | Optional default TTL (minutes) for new
|
|
255
|
+
| `DEFAULT_EXPIRES_IN_MINUTES` | empty | Optional default TTL (minutes) for new pages. |
|
|
248
256
|
|
|
249
257
|
## Architecture
|
|
250
258
|
|
|
251
259
|
One FastAPI process hosts the landing page, JSON API, and view renderer. `HostRoutingMiddleware` classifies requests by host and prevents API routes from being served on the view host.
|
|
252
260
|
|
|
253
|
-
Postgres stores
|
|
261
|
+
Postgres stores page metadata, owner-key/password hashes, expiry, view counts, and parent-version links. HTML bodies are stored as blobs, either in `LocalBlobStore` for development/tests or DigitalOcean Spaces in production.
|
|
254
262
|
|
|
255
263
|
## Project Layout
|
|
256
264
|
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
Quick start:
|
|
4
4
|
|
|
5
5
|
import htmlship
|
|
6
|
-
|
|
7
|
-
print(
|
|
8
|
-
print(
|
|
6
|
+
page = htmlship.publish("<h1>Hello</h1>", expires_in=60) # minutes
|
|
7
|
+
print(page.url)
|
|
8
|
+
print(page.owner_key)
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
page.update("<h1>Hello, again</h1>")
|
|
11
|
+
page.delete()
|
|
12
12
|
|
|
13
13
|
Configuration via environment:
|
|
14
14
|
HTMLSHIP_API_URL (default: https://api.htmlship.com)
|
|
@@ -25,7 +25,7 @@ from .exceptions import (
|
|
|
25
25
|
NotFoundError,
|
|
26
26
|
RateLimitError,
|
|
27
27
|
)
|
|
28
|
-
from .models import
|
|
28
|
+
from .models import Page
|
|
29
29
|
|
|
30
30
|
_default_client: HTMLShipClient | None = None
|
|
31
31
|
|
|
@@ -61,7 +61,7 @@ def publish(
|
|
|
61
61
|
expires_in: int | None = None,
|
|
62
62
|
parent_slug: str | None = None,
|
|
63
63
|
sandbox_mode: str = "strict",
|
|
64
|
-
) ->
|
|
64
|
+
) -> Page:
|
|
65
65
|
"""Publish HTML. ``expires_in`` is in minutes (max 10080 = 7 days)."""
|
|
66
66
|
return _get_default_client().publish(
|
|
67
67
|
html,
|
|
@@ -73,11 +73,11 @@ def publish(
|
|
|
73
73
|
)
|
|
74
74
|
|
|
75
75
|
|
|
76
|
-
def get(slug: str) ->
|
|
76
|
+
def get(slug: str) -> Page:
|
|
77
77
|
return _get_default_client().get(slug)
|
|
78
78
|
|
|
79
79
|
|
|
80
|
-
def update(slug: str, html: str, *, owner_key: str, title: str | None = None) ->
|
|
80
|
+
def update(slug: str, html: str, *, owner_key: str, title: str | None = None) -> Page:
|
|
81
81
|
return _get_default_client().update(slug, html, owner_key=owner_key, title=title)
|
|
82
82
|
|
|
83
83
|
|
|
@@ -88,7 +88,7 @@ def delete(slug: str, *, owner_key: str) -> None:
|
|
|
88
88
|
__all__ = [
|
|
89
89
|
"__version__",
|
|
90
90
|
"HTMLShipClient",
|
|
91
|
-
"
|
|
91
|
+
"Page",
|
|
92
92
|
"HTMLShipError",
|
|
93
93
|
"NotFoundError",
|
|
94
94
|
"AuthError",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.4"
|
|
@@ -111,7 +111,7 @@ def main(ctx: click.Context, api_url: str | None) -> None:
|
|
|
111
111
|
@click.argument("source", required=False)
|
|
112
112
|
@click.option("--file", "-f", "file_", type=click.Path(exists=True, dir_okay=False), help="HTML file path.")
|
|
113
113
|
@click.option("--title", default=None, help="Optional title.")
|
|
114
|
-
@click.option("--password", default=None, help="Password-protect the
|
|
114
|
+
@click.option("--password", default=None, help="Password-protect the page.")
|
|
115
115
|
@click.option(
|
|
116
116
|
"--expires-in",
|
|
117
117
|
type=int,
|
|
@@ -142,7 +142,7 @@ def publish(
|
|
|
142
142
|
html = _read_html(source, file_)
|
|
143
143
|
client = _make_client(ctx.obj["api_url"])
|
|
144
144
|
try:
|
|
145
|
-
|
|
145
|
+
page = client.publish(
|
|
146
146
|
html,
|
|
147
147
|
title=title,
|
|
148
148
|
password=password,
|
|
@@ -153,18 +153,18 @@ def publish(
|
|
|
153
153
|
finally:
|
|
154
154
|
client.close()
|
|
155
155
|
|
|
156
|
-
_remember(
|
|
156
|
+
_remember(page.slug, page.owner_key or "", page.url, title)
|
|
157
157
|
|
|
158
158
|
if quiet:
|
|
159
|
-
click.echo(
|
|
159
|
+
click.echo(page.url)
|
|
160
160
|
return
|
|
161
161
|
|
|
162
|
-
click.echo(
|
|
163
|
-
click.echo(f"slug: {
|
|
164
|
-
click.echo(f"owner_key: {
|
|
165
|
-
if
|
|
166
|
-
click.echo(f"expires: {
|
|
167
|
-
if not no_clipboard and _try_clipboard(
|
|
162
|
+
click.echo(page.url)
|
|
163
|
+
click.echo(f"slug: {page.slug}", err=True)
|
|
164
|
+
click.echo(f"owner_key: {page.owner_key} (saved to {KEYS_FILE})", err=True)
|
|
165
|
+
if page.expires_at:
|
|
166
|
+
click.echo(f"expires: {page.expires_at.isoformat()}", err=True)
|
|
167
|
+
if not no_clipboard and _try_clipboard(page.url):
|
|
168
168
|
click.echo("(URL copied to clipboard)", err=True)
|
|
169
169
|
|
|
170
170
|
|
|
@@ -172,12 +172,12 @@ def publish(
|
|
|
172
172
|
@click.argument("slug")
|
|
173
173
|
@click.pass_context
|
|
174
174
|
def get(ctx: click.Context, slug: str) -> None:
|
|
175
|
-
"""Show metadata for a
|
|
175
|
+
"""Show metadata for a page."""
|
|
176
176
|
client = _make_client(ctx.obj["api_url"])
|
|
177
177
|
try:
|
|
178
|
-
|
|
178
|
+
page = client.get(slug)
|
|
179
179
|
except NotFoundError as exc:
|
|
180
|
-
raise click.ClickException(f"
|
|
180
|
+
raise click.ClickException(f"page '{slug}' not found") from exc
|
|
181
181
|
except HTMLShipError as exc:
|
|
182
182
|
raise click.ClickException(str(exc)) from exc
|
|
183
183
|
finally:
|
|
@@ -185,16 +185,16 @@ def get(ctx: click.Context, slug: str) -> None:
|
|
|
185
185
|
|
|
186
186
|
click.echo(json.dumps(
|
|
187
187
|
{
|
|
188
|
-
"slug":
|
|
189
|
-
"url":
|
|
190
|
-
"title":
|
|
191
|
-
"view_count":
|
|
192
|
-
"size_bytes":
|
|
193
|
-
"has_password":
|
|
194
|
-
"parent_slug":
|
|
195
|
-
"expires_at":
|
|
196
|
-
"created_at":
|
|
197
|
-
"updated_at":
|
|
188
|
+
"slug": page.slug,
|
|
189
|
+
"url": page.url,
|
|
190
|
+
"title": page.title,
|
|
191
|
+
"view_count": page.view_count,
|
|
192
|
+
"size_bytes": page.size_bytes,
|
|
193
|
+
"has_password": page.has_password,
|
|
194
|
+
"parent_slug": page.parent_slug,
|
|
195
|
+
"expires_at": page.expires_at.isoformat() if page.expires_at else None,
|
|
196
|
+
"created_at": page.created_at.isoformat() if page.created_at else None,
|
|
197
|
+
"updated_at": page.updated_at.isoformat() if page.updated_at else None,
|
|
198
198
|
},
|
|
199
199
|
indent=2,
|
|
200
200
|
))
|
|
@@ -215,7 +215,7 @@ def update(
|
|
|
215
215
|
title: str | None,
|
|
216
216
|
owner_key: str | None,
|
|
217
217
|
) -> None:
|
|
218
|
-
"""Replace HTML for an existing
|
|
218
|
+
"""Replace HTML for an existing page."""
|
|
219
219
|
html = _read_html(source, file_)
|
|
220
220
|
key = owner_key or _lookup_owner_key(slug)
|
|
221
221
|
if not key:
|
|
@@ -224,13 +224,13 @@ def update(
|
|
|
224
224
|
)
|
|
225
225
|
client = _make_client(ctx.obj["api_url"])
|
|
226
226
|
try:
|
|
227
|
-
|
|
227
|
+
page = client.update(slug, html, owner_key=key, title=title)
|
|
228
228
|
except HTMLShipError as exc:
|
|
229
229
|
raise click.ClickException(str(exc)) from exc
|
|
230
230
|
finally:
|
|
231
231
|
client.close()
|
|
232
232
|
|
|
233
|
-
click.echo(
|
|
233
|
+
click.echo(page.url)
|
|
234
234
|
|
|
235
235
|
|
|
236
236
|
@main.command()
|
|
@@ -239,14 +239,14 @@ def update(
|
|
|
239
239
|
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation.")
|
|
240
240
|
@click.pass_context
|
|
241
241
|
def delete(ctx: click.Context, slug: str, owner_key: str | None, yes: bool) -> None:
|
|
242
|
-
"""Soft-delete a
|
|
242
|
+
"""Soft-delete a page."""
|
|
243
243
|
key = owner_key or _lookup_owner_key(slug)
|
|
244
244
|
if not key:
|
|
245
245
|
raise click.ClickException(
|
|
246
246
|
f"no owner key for '{slug}' in {KEYS_FILE}; pass --owner-key explicitly"
|
|
247
247
|
)
|
|
248
248
|
if not yes:
|
|
249
|
-
click.confirm(f"Delete
|
|
249
|
+
click.confirm(f"Delete page '{slug}'?", abort=True)
|
|
250
250
|
client = _make_client(ctx.obj["api_url"])
|
|
251
251
|
try:
|
|
252
252
|
client.delete(slug, owner_key=key)
|
|
@@ -262,10 +262,10 @@ def delete(ctx: click.Context, slug: str, owner_key: str | None, yes: bool) -> N
|
|
|
262
262
|
@click.option("--limit", type=int, default=20, show_default=True)
|
|
263
263
|
@click.pass_context
|
|
264
264
|
def list_mine(ctx: click.Context, limit: int) -> None:
|
|
265
|
-
"""List
|
|
265
|
+
"""List pages whose owner keys are saved locally."""
|
|
266
266
|
keys = _load_keys()
|
|
267
267
|
if not keys:
|
|
268
|
-
click.echo(f"No saved
|
|
268
|
+
click.echo(f"No saved pages in {KEYS_FILE}.")
|
|
269
269
|
return
|
|
270
270
|
items = sorted(keys.items(), key=lambda kv: kv[1].get("saved_at", ""), reverse=True)[:limit]
|
|
271
271
|
|
|
@@ -274,13 +274,13 @@ def list_mine(ctx: click.Context, limit: int) -> None:
|
|
|
274
274
|
rows = []
|
|
275
275
|
for slug, info in items:
|
|
276
276
|
try:
|
|
277
|
-
|
|
278
|
-
size = f"{
|
|
279
|
-
views =
|
|
280
|
-
title =
|
|
277
|
+
page = client.get(slug)
|
|
278
|
+
size = f"{page.size_bytes}B"
|
|
279
|
+
views = page.view_count
|
|
280
|
+
title = page.title or info.get("title") or ""
|
|
281
281
|
status = "ok"
|
|
282
282
|
except NotFoundError:
|
|
283
|
-
|
|
283
|
+
page = None
|
|
284
284
|
size = "—"
|
|
285
285
|
views = "—"
|
|
286
286
|
title = info.get("title") or ""
|
|
@@ -8,7 +8,7 @@ import httpx
|
|
|
8
8
|
|
|
9
9
|
from ._version import __version__
|
|
10
10
|
from .exceptions import APIError, AuthError, HTMLShipError, NotFoundError, RateLimitError
|
|
11
|
-
from .models import
|
|
11
|
+
from .models import Page
|
|
12
12
|
|
|
13
13
|
DEFAULT_API_URL = "https://api.htmlship.com"
|
|
14
14
|
DEFAULT_TIMEOUT = 30.0
|
|
@@ -65,10 +65,10 @@ class HTMLShipClient:
|
|
|
65
65
|
expires_in: int | None = None,
|
|
66
66
|
parent_slug: str | None = None,
|
|
67
67
|
sandbox_mode: str = "strict",
|
|
68
|
-
) ->
|
|
69
|
-
"""Create a
|
|
68
|
+
) -> Page:
|
|
69
|
+
"""Create a page. ``expires_in`` is in minutes (max 10080 = 7 days).
|
|
70
70
|
|
|
71
|
-
Returns a
|
|
71
|
+
Returns a Page with owner_key set.
|
|
72
72
|
"""
|
|
73
73
|
body: dict[str, Any] = {"html": html, "sandbox_mode": sandbox_mode}
|
|
74
74
|
if title is not None:
|
|
@@ -79,9 +79,9 @@ class HTMLShipClient:
|
|
|
79
79
|
body["expires_in"] = expires_in
|
|
80
80
|
if parent_slug is not None:
|
|
81
81
|
body["parent_slug"] = parent_slug
|
|
82
|
-
r = self._http.post("/api/v1/
|
|
82
|
+
r = self._http.post("/api/v1/pages", json=body)
|
|
83
83
|
data = self._handle(r)
|
|
84
|
-
return
|
|
84
|
+
return Page(
|
|
85
85
|
slug=data["slug"],
|
|
86
86
|
url=data["url"],
|
|
87
87
|
owner_key=data["owner_key"],
|
|
@@ -90,11 +90,11 @@ class HTMLShipClient:
|
|
|
90
90
|
_client=self,
|
|
91
91
|
)
|
|
92
92
|
|
|
93
|
-
def get(self, slug: str) ->
|
|
94
|
-
"""Fetch metadata for a
|
|
95
|
-
r = self._http.get(f"/api/v1/
|
|
93
|
+
def get(self, slug: str) -> Page:
|
|
94
|
+
"""Fetch metadata for a page. owner_key is None on the returned object."""
|
|
95
|
+
r = self._http.get(f"/api/v1/pages/{slug}")
|
|
96
96
|
data = self._handle(r)
|
|
97
|
-
return
|
|
97
|
+
return Page(
|
|
98
98
|
slug=data["slug"],
|
|
99
99
|
url=data["url"],
|
|
100
100
|
owner_key=None,
|
|
@@ -116,17 +116,17 @@ class HTMLShipClient:
|
|
|
116
116
|
*,
|
|
117
117
|
owner_key: str,
|
|
118
118
|
title: str | None = None,
|
|
119
|
-
) ->
|
|
119
|
+
) -> Page:
|
|
120
120
|
body: dict[str, Any] = {"html": html}
|
|
121
121
|
if title is not None:
|
|
122
122
|
body["title"] = title
|
|
123
123
|
r = self._http.patch(
|
|
124
|
-
f"/api/v1/
|
|
124
|
+
f"/api/v1/pages/{slug}",
|
|
125
125
|
json=body,
|
|
126
126
|
headers={"X-Owner-Key": owner_key},
|
|
127
127
|
)
|
|
128
128
|
data = self._handle(r)
|
|
129
|
-
return
|
|
129
|
+
return Page(
|
|
130
130
|
slug=data["slug"],
|
|
131
131
|
url=data["url"],
|
|
132
132
|
owner_key=owner_key,
|
|
@@ -137,7 +137,7 @@ class HTMLShipClient:
|
|
|
137
137
|
|
|
138
138
|
def delete(self, slug: str, *, owner_key: str) -> None:
|
|
139
139
|
r = self._http.delete(
|
|
140
|
-
f"/api/v1/
|
|
140
|
+
f"/api/v1/pages/{slug}",
|
|
141
141
|
headers={"X-Owner-Key": owner_key},
|
|
142
142
|
)
|
|
143
143
|
self._handle(r, expect_no_body=True)
|
|
@@ -150,7 +150,7 @@ class HTMLShipClient:
|
|
|
150
150
|
title: str | None = None,
|
|
151
151
|
password: str | None = None,
|
|
152
152
|
expires_in: int | None = None,
|
|
153
|
-
) ->
|
|
153
|
+
) -> Page:
|
|
154
154
|
body: dict[str, Any] = {"html": html, "sandbox_mode": "strict"}
|
|
155
155
|
if title is not None:
|
|
156
156
|
body["title"] = title
|
|
@@ -158,9 +158,9 @@ class HTMLShipClient:
|
|
|
158
158
|
body["password"] = password
|
|
159
159
|
if expires_in is not None:
|
|
160
160
|
body["expires_in"] = expires_in
|
|
161
|
-
r = self._http.post(f"/api/v1/
|
|
161
|
+
r = self._http.post(f"/api/v1/pages/{parent_slug}/version", json=body)
|
|
162
162
|
data = self._handle(r)
|
|
163
|
-
return
|
|
163
|
+
return Page(
|
|
164
164
|
slug=data["slug"],
|
|
165
165
|
url=data["url"],
|
|
166
166
|
owner_key=data["owner_key"],
|