htmlship 0.1.2__tar.gz → 0.1.3__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 (33) hide show
  1. {htmlship-0.1.2 → htmlship-0.1.3}/PKG-INFO +21 -21
  2. {htmlship-0.1.2 → htmlship-0.1.3}/README.md +20 -20
  3. {htmlship-0.1.2 → htmlship-0.1.3}/pyproject.toml +1 -1
  4. {htmlship-0.1.2 → htmlship-0.1.3}/src/htmlship/__init__.py +10 -10
  5. htmlship-0.1.3/src/htmlship/_version.py +1 -0
  6. {htmlship-0.1.2 → htmlship-0.1.3}/src/htmlship/cli.py +35 -35
  7. {htmlship-0.1.2 → htmlship-0.1.3}/src/htmlship/client.py +17 -17
  8. {htmlship-0.1.2 → htmlship-0.1.3}/src/htmlship/exceptions.py +1 -1
  9. {htmlship-0.1.2 → htmlship-0.1.3}/src/htmlship/models.py +10 -10
  10. htmlship-0.1.3/src/htmlship_mcp/__init__.py +1 -0
  11. {htmlship-0.1.2 → htmlship-0.1.3}/src/htmlship_mcp/server.py +28 -28
  12. htmlship-0.1.3/src/htmlship_server/__init__.py +1 -0
  13. htmlship-0.1.3/src/htmlship_server/db_models/__init__.py +3 -0
  14. htmlship-0.1.2/src/htmlship_server/db_models/paste.py → htmlship-0.1.3/src/htmlship_server/db_models/page.py +6 -6
  15. {htmlship-0.1.2 → htmlship-0.1.3}/src/htmlship_server/exceptions.py +1 -1
  16. {htmlship-0.1.2 → htmlship-0.1.3}/src/htmlship_server/main.py +2 -2
  17. htmlship-0.1.2/src/htmlship_server/routers/pastes.py → htmlship-0.1.3/src/htmlship_server/routers/pages.py +76 -76
  18. {htmlship-0.1.2 → htmlship-0.1.3}/src/htmlship_server/routers/view.py +24 -24
  19. htmlship-0.1.2/src/htmlship_server/schemas/pastes.py → htmlship-0.1.3/src/htmlship_server/schemas/pages.py +5 -5
  20. {htmlship-0.1.2 → htmlship-0.1.3}/src/htmlship_server/storage.py +2 -2
  21. htmlship-0.1.2/src/htmlship/_version.py +0 -1
  22. htmlship-0.1.2/src/htmlship_mcp/__init__.py +0 -1
  23. htmlship-0.1.2/src/htmlship_server/__init__.py +0 -1
  24. htmlship-0.1.2/src/htmlship_server/db_models/__init__.py +0 -3
  25. {htmlship-0.1.2 → htmlship-0.1.3}/.gitignore +0 -0
  26. {htmlship-0.1.2 → htmlship-0.1.3}/src/htmlship_server/config.py +0 -0
  27. {htmlship-0.1.2 → htmlship-0.1.3}/src/htmlship_server/database.py +0 -0
  28. {htmlship-0.1.2 → htmlship-0.1.3}/src/htmlship_server/middleware.py +0 -0
  29. {htmlship-0.1.2 → htmlship-0.1.3}/src/htmlship_server/routers/__init__.py +0 -0
  30. {htmlship-0.1.2 → htmlship-0.1.3}/src/htmlship_server/routers/meta.py +0 -0
  31. {htmlship-0.1.2 → htmlship-0.1.3}/src/htmlship_server/schemas/__init__.py +0 -0
  32. {htmlship-0.1.2 → htmlship-0.1.3}/src/htmlship_server/security.py +0 -0
  33. {htmlship-0.1.2 → htmlship-0.1.3}/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.2
3
+ Version: 0.1.3
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,7 +50,7 @@ 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 pastes
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
@@ -64,13 +64,13 @@ pip install htmlship
64
64
  ```python
65
65
  import htmlship
66
66
 
67
- paste = htmlship.publish("<h1>Hello</h1>", title="Demo", expires_in=60) # minutes
68
- print(paste.url)
69
- print(paste.owner_key) # save this to update or delete the paste later
67
+ page = htmlship.publish("<h1>Hello</h1>", title="Demo", expires_in=60) # minutes
68
+ print(page.url)
69
+ print(page.owner_key) # save this to update or delete the page later
70
70
  ```
71
71
 
72
72
  ```bash
73
- curl -X POST https://api.htmlship.com/api/v1/pastes \
73
+ curl -X POST https://api.htmlship.com/api/v1/pages \
74
74
  -H "Content-Type: application/json" \
75
75
  -d '{"html":"<h1>Hello</h1>","title":"Demo"}'
76
76
  ```
@@ -84,23 +84,23 @@ The module-level helpers use `https://api.htmlship.com` by default. Override wit
84
84
  ```python
85
85
  import htmlship
86
86
 
87
- paste = htmlship.publish(
87
+ page = htmlship.publish(
88
88
  "<h1>Hello</h1>",
89
89
  title="Demo",
90
90
  password="optional-password",
91
91
  expires_in=1440, # minutes (24 hours)
92
92
  )
93
93
 
94
- fresh = htmlship.get(paste.slug)
95
- updated = htmlship.update(paste.slug, "<h1>Updated</h1>", owner_key=paste.owner_key)
96
- htmlship.delete(updated.slug, owner_key=paste.owner_key)
94
+ fresh = htmlship.get(page.slug)
95
+ updated = htmlship.update(page.slug, "<h1>Updated</h1>", owner_key=page.owner_key)
96
+ htmlship.delete(updated.slug, owner_key=page.owner_key)
97
97
  ```
98
98
 
99
- `owner_key` is returned only when a paste is created. It is required for updates and deletes, and the API does not return it again from metadata calls.
99
+ `owner_key` is returned only when a page is created. It is required for updates and deletes, and the API does not return it again from metadata calls.
100
100
 
101
101
  ## CLI
102
102
 
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 paste created in one is editable from the other.
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 page created in one is editable from the other.
104
104
 
105
105
  ```bash
106
106
  htmlship publish report.html
@@ -120,7 +120,7 @@ npx htmlship publish report.html
120
120
  npx htmlship list-mine
121
121
  ```
122
122
 
123
- The CLI stores owner keys in `~/.htmlship/keys.json` so `update`, `delete`, and `list-mine` can work with pastes 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.
123
+ 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
124
 
125
125
  ## API
126
126
 
@@ -130,11 +130,11 @@ Base URL: `https://api.htmlship.com`.
130
130
  | --- | --- | --- |
131
131
  | `GET` | `/health` | Health check with service version. |
132
132
  | `GET` | `/version` | Service version. |
133
- | `POST` | `/api/v1/pastes` | Create a paste. |
134
- | `GET` | `/api/v1/pastes/{slug}` | Fetch paste metadata. |
135
- | `PATCH` | `/api/v1/pastes/{slug}` | Replace HTML or title. Requires `X-Owner-Key`. |
136
- | `DELETE` | `/api/v1/pastes/{slug}` | Soft-delete a paste. Requires `X-Owner-Key`. |
137
- | `POST` | `/api/v1/pastes/{slug}/version` | Create a new paste linked to an existing parent slug. |
133
+ | `POST` | `/api/v1/pages` | Create a page. |
134
+ | `GET` | `/api/v1/pages/{slug}` | Fetch page metadata. |
135
+ | `PATCH` | `/api/v1/pages/{slug}` | Replace HTML or title. Requires `X-Owner-Key`. |
136
+ | `DELETE` | `/api/v1/pages/{slug}` | Soft-delete a page. Requires `X-Owner-Key`. |
137
+ | `POST` | `/api/v1/pages/{slug}/version` | Create a new page linked to an existing parent slug. |
138
138
 
139
139
  Create payload:
140
140
 
@@ -278,7 +278,7 @@ The server reads `.env` via Pydantic settings.
278
278
  | `DATABASE_URL` | `postgresql+asyncpg://htmlship:htmlship@localhost:5433/htmlship` | Async SQLAlchemy database URL. |
279
279
  | `PUBLIC_BASE_DOMAIN` | `htmlship.com` | Base domain used to derive host routing. |
280
280
  | `API_BASE_URL` | `https://api.htmlship.com` | Public API URL setting. |
281
- | `VIEW_BASE_URL` | `https://view.htmlship.com` | Public view URL used in paste responses. |
281
+ | `VIEW_BASE_URL` | `https://view.htmlship.com` | Public view URL used in page responses. |
282
282
  | `LANDING_BASE_URL` | `https://htmlship.com` | Public landing URL. |
283
283
  | `SPACES_BUCKET` | empty | If empty, use local blob storage; otherwise use DigitalOcean Spaces/S3. |
284
284
  | `SPACES_REGION` | `nyc3` | Spaces/S3 region. |
@@ -288,13 +288,13 @@ The server reads `.env` via Pydantic settings.
288
288
  | `ENVIRONMENT` | `development` | Enables API docs outside production and secure cookies in production. |
289
289
  | `LOG_LEVEL` | `info` | Application log level. |
290
290
  | `MAX_PAYLOAD_BYTES` | `10485760` | Server-side HTML size limit. |
291
- | `DEFAULT_EXPIRES_IN_MINUTES` | empty | Optional default TTL (minutes) for new pastes. |
291
+ | `DEFAULT_EXPIRES_IN_MINUTES` | empty | Optional default TTL (minutes) for new pages. |
292
292
 
293
293
  ## Architecture
294
294
 
295
295
  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
296
 
297
- Postgres stores paste 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.
297
+ 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
298
 
299
299
  ## Project Layout
300
300
 
@@ -6,7 +6,7 @@ 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 pastes
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
@@ -20,13 +20,13 @@ pip install htmlship
20
20
  ```python
21
21
  import htmlship
22
22
 
23
- paste = htmlship.publish("<h1>Hello</h1>", title="Demo", expires_in=60) # minutes
24
- print(paste.url)
25
- print(paste.owner_key) # save this to update or delete the paste later
23
+ page = htmlship.publish("<h1>Hello</h1>", title="Demo", expires_in=60) # minutes
24
+ print(page.url)
25
+ print(page.owner_key) # save this to update or delete the page later
26
26
  ```
27
27
 
28
28
  ```bash
29
- curl -X POST https://api.htmlship.com/api/v1/pastes \
29
+ curl -X POST https://api.htmlship.com/api/v1/pages \
30
30
  -H "Content-Type: application/json" \
31
31
  -d '{"html":"<h1>Hello</h1>","title":"Demo"}'
32
32
  ```
@@ -40,23 +40,23 @@ The module-level helpers use `https://api.htmlship.com` by default. Override wit
40
40
  ```python
41
41
  import htmlship
42
42
 
43
- paste = htmlship.publish(
43
+ page = htmlship.publish(
44
44
  "<h1>Hello</h1>",
45
45
  title="Demo",
46
46
  password="optional-password",
47
47
  expires_in=1440, # minutes (24 hours)
48
48
  )
49
49
 
50
- fresh = htmlship.get(paste.slug)
51
- updated = htmlship.update(paste.slug, "<h1>Updated</h1>", owner_key=paste.owner_key)
52
- htmlship.delete(updated.slug, owner_key=paste.owner_key)
50
+ fresh = htmlship.get(page.slug)
51
+ updated = htmlship.update(page.slug, "<h1>Updated</h1>", owner_key=page.owner_key)
52
+ htmlship.delete(updated.slug, owner_key=page.owner_key)
53
53
  ```
54
54
 
55
- `owner_key` is returned only when a paste is created. It is required for updates and deletes, and the API does not return it again from metadata calls.
55
+ `owner_key` is returned only when a page is created. It is required for updates and deletes, and the API does not return it again from metadata calls.
56
56
 
57
57
  ## CLI
58
58
 
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 paste created in one is editable from the other.
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 page created in one is editable from the other.
60
60
 
61
61
  ```bash
62
62
  htmlship publish report.html
@@ -76,7 +76,7 @@ npx htmlship publish report.html
76
76
  npx htmlship list-mine
77
77
  ```
78
78
 
79
- The CLI stores owner keys in `~/.htmlship/keys.json` so `update`, `delete`, and `list-mine` can work with pastes 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.
79
+ 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
80
 
81
81
  ## API
82
82
 
@@ -86,11 +86,11 @@ Base URL: `https://api.htmlship.com`.
86
86
  | --- | --- | --- |
87
87
  | `GET` | `/health` | Health check with service version. |
88
88
  | `GET` | `/version` | Service version. |
89
- | `POST` | `/api/v1/pastes` | Create a paste. |
90
- | `GET` | `/api/v1/pastes/{slug}` | Fetch paste metadata. |
91
- | `PATCH` | `/api/v1/pastes/{slug}` | Replace HTML or title. Requires `X-Owner-Key`. |
92
- | `DELETE` | `/api/v1/pastes/{slug}` | Soft-delete a paste. Requires `X-Owner-Key`. |
93
- | `POST` | `/api/v1/pastes/{slug}/version` | Create a new paste linked to an existing parent slug. |
89
+ | `POST` | `/api/v1/pages` | Create a page. |
90
+ | `GET` | `/api/v1/pages/{slug}` | Fetch page metadata. |
91
+ | `PATCH` | `/api/v1/pages/{slug}` | Replace HTML or title. Requires `X-Owner-Key`. |
92
+ | `DELETE` | `/api/v1/pages/{slug}` | Soft-delete a page. Requires `X-Owner-Key`. |
93
+ | `POST` | `/api/v1/pages/{slug}/version` | Create a new page linked to an existing parent slug. |
94
94
 
95
95
  Create payload:
96
96
 
@@ -234,7 +234,7 @@ The server reads `.env` via Pydantic settings.
234
234
  | `DATABASE_URL` | `postgresql+asyncpg://htmlship:htmlship@localhost:5433/htmlship` | Async SQLAlchemy database URL. |
235
235
  | `PUBLIC_BASE_DOMAIN` | `htmlship.com` | Base domain used to derive host routing. |
236
236
  | `API_BASE_URL` | `https://api.htmlship.com` | Public API URL setting. |
237
- | `VIEW_BASE_URL` | `https://view.htmlship.com` | Public view URL used in paste responses. |
237
+ | `VIEW_BASE_URL` | `https://view.htmlship.com` | Public view URL used in page responses. |
238
238
  | `LANDING_BASE_URL` | `https://htmlship.com` | Public landing URL. |
239
239
  | `SPACES_BUCKET` | empty | If empty, use local blob storage; otherwise use DigitalOcean Spaces/S3. |
240
240
  | `SPACES_REGION` | `nyc3` | Spaces/S3 region. |
@@ -244,13 +244,13 @@ The server reads `.env` via Pydantic settings.
244
244
  | `ENVIRONMENT` | `development` | Enables API docs outside production and secure cookies in production. |
245
245
  | `LOG_LEVEL` | `info` | Application log level. |
246
246
  | `MAX_PAYLOAD_BYTES` | `10485760` | Server-side HTML size limit. |
247
- | `DEFAULT_EXPIRES_IN_MINUTES` | empty | Optional default TTL (minutes) for new pastes. |
247
+ | `DEFAULT_EXPIRES_IN_MINUTES` | empty | Optional default TTL (minutes) for new pages. |
248
248
 
249
249
  ## Architecture
250
250
 
251
251
  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
252
 
253
- Postgres stores paste 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.
253
+ 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
254
 
255
255
  ## Project Layout
256
256
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "htmlship"
3
- version = "0.1.2"
3
+ version = "0.1.3"
4
4
  description = "Host and share HTML pages from LLMs and coding agents in one line."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -3,12 +3,12 @@
3
3
  Quick start:
4
4
 
5
5
  import htmlship
6
- paste = htmlship.publish("<h1>Hello</h1>", expires_in=60) # minutes
7
- print(paste.url)
8
- print(paste.owner_key)
6
+ page = htmlship.publish("<h1>Hello</h1>", expires_in=60) # minutes
7
+ print(page.url)
8
+ print(page.owner_key)
9
9
 
10
- paste.update("<h1>Hello, again</h1>")
11
- paste.delete()
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 Paste
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
- ) -> Paste:
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) -> Paste:
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) -> Paste:
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
- "Paste",
91
+ "Page",
92
92
  "HTMLShipError",
93
93
  "NotFoundError",
94
94
  "AuthError",
@@ -0,0 +1 @@
1
+ __version__ = "0.1.3"
@@ -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 paste.")
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
- paste = client.publish(
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(paste.slug, paste.owner_key or "", paste.url, title)
156
+ _remember(page.slug, page.owner_key or "", page.url, title)
157
157
 
158
158
  if quiet:
159
- click.echo(paste.url)
159
+ click.echo(page.url)
160
160
  return
161
161
 
162
- click.echo(paste.url)
163
- click.echo(f"slug: {paste.slug}", err=True)
164
- click.echo(f"owner_key: {paste.owner_key} (saved to {KEYS_FILE})", err=True)
165
- if paste.expires_at:
166
- click.echo(f"expires: {paste.expires_at.isoformat()}", err=True)
167
- if not no_clipboard and _try_clipboard(paste.url):
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 paste."""
175
+ """Show metadata for a page."""
176
176
  client = _make_client(ctx.obj["api_url"])
177
177
  try:
178
- paste = client.get(slug)
178
+ page = client.get(slug)
179
179
  except NotFoundError as exc:
180
- raise click.ClickException(f"paste '{slug}' not found") from exc
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": paste.slug,
189
- "url": paste.url,
190
- "title": paste.title,
191
- "view_count": paste.view_count,
192
- "size_bytes": paste.size_bytes,
193
- "has_password": paste.has_password,
194
- "parent_slug": paste.parent_slug,
195
- "expires_at": paste.expires_at.isoformat() if paste.expires_at else None,
196
- "created_at": paste.created_at.isoformat() if paste.created_at else None,
197
- "updated_at": paste.updated_at.isoformat() if paste.updated_at else None,
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 paste."""
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
- paste = client.update(slug, html, owner_key=key, title=title)
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(paste.url)
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 paste."""
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 paste '{slug}'?", abort=True)
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 pastes whose owner keys are saved locally."""
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 pastes in {KEYS_FILE}.")
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
- paste = client.get(slug)
278
- size = f"{paste.size_bytes}B"
279
- views = paste.view_count
280
- title = paste.title or info.get("title") or ""
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
- paste = None
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 Paste
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
- ) -> Paste:
69
- """Create a paste. ``expires_in`` is in minutes (max 10080 = 7 days).
68
+ ) -> Page:
69
+ """Create a page. ``expires_in`` is in minutes (max 10080 = 7 days).
70
70
 
71
- Returns a Paste with owner_key set.
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/pastes", json=body)
82
+ r = self._http.post("/api/v1/pages", json=body)
83
83
  data = self._handle(r)
84
- return Paste(
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) -> Paste:
94
- """Fetch metadata for a paste. owner_key is None on the returned object."""
95
- r = self._http.get(f"/api/v1/pastes/{slug}")
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 Paste(
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
- ) -> Paste:
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/pastes/{slug}",
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 Paste(
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/pastes/{slug}",
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
- ) -> Paste:
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/pastes/{parent_slug}/version", json=body)
161
+ r = self._http.post(f"/api/v1/pages/{parent_slug}/version", json=body)
162
162
  data = self._handle(r)
163
- return Paste(
163
+ return Page(
164
164
  slug=data["slug"],
165
165
  url=data["url"],
166
166
  owner_key=data["owner_key"],
@@ -6,7 +6,7 @@ class HTMLShipError(Exception):
6
6
 
7
7
 
8
8
  class NotFoundError(HTMLShipError):
9
- """Returned when a paste does not exist or has expired."""
9
+ """Returned when a page does not exist or has expired."""
10
10
 
11
11
 
12
12
  class AuthError(HTMLShipError):
@@ -9,11 +9,11 @@ if TYPE_CHECKING:
9
9
 
10
10
 
11
11
  @dataclass
12
- class Paste:
13
- """A paste created via the HTMLShip API.
12
+ class Page:
13
+ """A page created via the HTMLShip API.
14
14
 
15
15
  The `owner_key` is set on creation and is the only credential to mutate or
16
- delete the paste. It is None for pastes loaded via `get()` since the server
16
+ delete the page. It is None for pages loaded via `get()` since the server
17
17
  does not return it after creation.
18
18
  """
19
19
 
@@ -31,10 +31,10 @@ class Paste:
31
31
 
32
32
  _client: HTMLShipClient | None = field(default=None, repr=False, compare=False)
33
33
 
34
- def update(self, html: str, *, title: str | None = None) -> Paste:
35
- """Replace this paste's HTML. Requires owner_key."""
34
+ def update(self, html: str, *, title: str | None = None) -> Page:
35
+ """Replace this page's HTML. Requires owner_key."""
36
36
  if self._client is None:
37
- raise RuntimeError("Paste is detached from a client; reattach to mutate")
37
+ raise RuntimeError("Page is detached from a client; reattach to mutate")
38
38
  if not self.owner_key:
39
39
  raise RuntimeError(
40
40
  "owner_key is required to update; load via get() first or pass owner_key explicitly"
@@ -42,19 +42,19 @@ class Paste:
42
42
  return self._client.update(self.slug, html, owner_key=self.owner_key, title=title)
43
43
 
44
44
  def delete(self) -> None:
45
- """Soft-delete this paste. Requires owner_key."""
45
+ """Soft-delete this page. Requires owner_key."""
46
46
  if self._client is None:
47
- raise RuntimeError("Paste is detached from a client; reattach to delete")
47
+ raise RuntimeError("Page is detached from a client; reattach to delete")
48
48
  if not self.owner_key:
49
49
  raise RuntimeError(
50
50
  "owner_key is required to delete; load via get() first or pass owner_key explicitly"
51
51
  )
52
52
  self._client.delete(self.slug, owner_key=self.owner_key)
53
53
 
54
- def fetch_metadata(self) -> Paste:
54
+ def fetch_metadata(self) -> Page:
55
55
  """Refresh metadata from the server."""
56
56
  if self._client is None:
57
- raise RuntimeError("Paste is detached from a client; reattach to fetch metadata")
57
+ raise RuntimeError("Page is detached from a client; reattach to fetch metadata")
58
58
  fresh = self._client.get(self.slug)
59
59
  # Preserve owner_key (server doesn't return it).
60
60
  fresh.owner_key = self.owner_key
@@ -0,0 +1 @@
1
+ __version__ = "0.1.3"
@@ -37,81 +37,81 @@ def publish_html(
37
37
  be blocked by Content Security Policy at view time.
38
38
  title: Optional human-readable title.
39
39
  expires_in: Optional TTL in minutes (1–10080, i.e. up to 7 days). If
40
- omitted, the paste is permanent (subject to the server's default
40
+ omitted, the page is permanent (subject to the server's default
41
41
  policy).
42
42
 
43
43
  Returns:
44
44
  A dict with keys: url, slug, owner_key, expires_at.
45
45
  IMPORTANT: owner_key is the only credential to update or delete the
46
- paste later. Save it.
46
+ page later. Save it.
47
47
  """
48
48
  with _client() as c:
49
49
  try:
50
- paste = c.publish(html, title=title, expires_in=expires_in)
50
+ page = c.publish(html, title=title, expires_in=expires_in)
51
51
  except HTMLShipError as exc:
52
52
  raise RuntimeError(f"htmlship publish failed: {exc}") from exc
53
53
  return {
54
- "url": paste.url,
55
- "slug": paste.slug,
56
- "owner_key": paste.owner_key,
57
- "expires_at": paste.expires_at.isoformat() if paste.expires_at else None,
54
+ "url": page.url,
55
+ "slug": page.slug,
56
+ "owner_key": page.owner_key,
57
+ "expires_at": page.expires_at.isoformat() if page.expires_at else None,
58
58
  }
59
59
 
60
60
 
61
61
  @mcp.tool()
62
62
  def fetch_html(slug: str) -> dict[str, Any]:
63
- """Fetch a paste's metadata.
63
+ """Fetch a page's metadata.
64
64
 
65
65
  NOTE: this returns metadata only. To view the rendered HTML, open the
66
66
  `url` in a browser — the content is served from a sandboxed subdomain.
67
67
 
68
68
  Args:
69
- slug: The paste's short identifier.
69
+ slug: The page's short identifier.
70
70
  """
71
71
  with _client() as c:
72
72
  try:
73
- paste = c.get(slug)
73
+ page = c.get(slug)
74
74
  except NotFoundError as exc:
75
- raise RuntimeError(f"paste '{slug}' not found") from exc
75
+ raise RuntimeError(f"page '{slug}' not found") from exc
76
76
  except HTMLShipError as exc:
77
77
  raise RuntimeError(f"htmlship fetch failed: {exc}") from exc
78
78
  return {
79
- "slug": paste.slug,
80
- "url": paste.url,
81
- "title": paste.title,
82
- "view_count": paste.view_count,
83
- "size_bytes": paste.size_bytes,
84
- "has_password": paste.has_password,
85
- "parent_slug": paste.parent_slug,
86
- "expires_at": paste.expires_at.isoformat() if paste.expires_at else None,
87
- "created_at": paste.created_at.isoformat() if paste.created_at else None,
88
- "updated_at": paste.updated_at.isoformat() if paste.updated_at else None,
79
+ "slug": page.slug,
80
+ "url": page.url,
81
+ "title": page.title,
82
+ "view_count": page.view_count,
83
+ "size_bytes": page.size_bytes,
84
+ "has_password": page.has_password,
85
+ "parent_slug": page.parent_slug,
86
+ "expires_at": page.expires_at.isoformat() if page.expires_at else None,
87
+ "created_at": page.created_at.isoformat() if page.created_at else None,
88
+ "updated_at": page.updated_at.isoformat() if page.updated_at else None,
89
89
  }
90
90
 
91
91
 
92
92
  @mcp.tool()
93
93
  def update_html(slug: str, html: str, owner_key: str, title: str | None = None) -> dict[str, Any]:
94
- """Replace the HTML for an existing paste. Requires the original owner_key.
94
+ """Replace the HTML for an existing page. Requires the original owner_key.
95
95
 
96
96
  Args:
97
- slug: The paste's short identifier.
97
+ slug: The page's short identifier.
98
98
  html: The new HTML body.
99
99
  owner_key: The owner key returned at publish time.
100
100
  title: Optional new title.
101
101
  """
102
102
  with _client() as c:
103
103
  try:
104
- paste = c.update(slug, html, owner_key=owner_key, title=title)
104
+ page = c.update(slug, html, owner_key=owner_key, title=title)
105
105
  except AuthError as exc:
106
106
  raise RuntimeError(f"invalid owner_key for '{slug}'") from exc
107
107
  except NotFoundError as exc:
108
- raise RuntimeError(f"paste '{slug}' not found") from exc
108
+ raise RuntimeError(f"page '{slug}' not found") from exc
109
109
  except HTMLShipError as exc:
110
110
  raise RuntimeError(f"htmlship update failed: {exc}") from exc
111
111
  return {
112
- "url": paste.url,
113
- "slug": paste.slug,
114
- "updated_at": paste.updated_at.isoformat() if paste.updated_at else None,
112
+ "url": page.url,
113
+ "slug": page.slug,
114
+ "updated_at": page.updated_at.isoformat() if page.updated_at else None,
115
115
  }
116
116
 
117
117
 
@@ -0,0 +1 @@
1
+ __version__ = "0.1.3"
@@ -0,0 +1,3 @@
1
+ from .page import Base, Page
2
+
3
+ __all__ = ["Base", "Page"]
@@ -12,8 +12,8 @@ class Base(DeclarativeBase):
12
12
  pass
13
13
 
14
14
 
15
- class Paste(Base):
16
- __tablename__ = "pastes"
15
+ class Page(Base):
16
+ __tablename__ = "pages"
17
17
 
18
18
  id: Mapped[uuid.UUID] = mapped_column(
19
19
  UUID(as_uuid=True),
@@ -53,8 +53,8 @@ class Paste(Base):
53
53
  __table_args__ = (
54
54
  CheckConstraint("sandbox_mode IN ('strict', 'relaxed')", name="ck_sandbox_mode"),
55
55
  CheckConstraint("size_bytes <= 10485760", name="ck_size_bytes"),
56
- Index("idx_pastes_slug", "slug"),
57
- Index("idx_pastes_expires_at", "expires_at"),
58
- Index("idx_pastes_parent_slug", "parent_slug"),
59
- Index("idx_pastes_created_at", "created_at"),
56
+ Index("idx_pages_slug", "slug"),
57
+ Index("idx_pages_expires_at", "expires_at"),
58
+ Index("idx_pages_parent_slug", "parent_slug"),
59
+ Index("idx_pages_created_at", "created_at"),
60
60
  )
@@ -5,5 +5,5 @@ class StorageError(Exception):
5
5
  """Raised when the blob store fails."""
6
6
 
7
7
 
8
- class PasteNotFoundError(Exception):
8
+ class PageNotFoundError(Exception):
9
9
  pass
@@ -10,7 +10,7 @@ from starlette.staticfiles import StaticFiles
10
10
  from . import __version__
11
11
  from .config import get_settings
12
12
  from .middleware import HostRoutingMiddleware
13
- from .routers import meta, pastes, view
13
+ from .routers import meta, pages, view
14
14
 
15
15
 
16
16
  @asynccontextmanager
@@ -31,7 +31,7 @@ def create_app() -> FastAPI:
31
31
  )
32
32
 
33
33
  app.include_router(meta.router)
34
- app.include_router(pastes.router)
34
+ app.include_router(pages.router)
35
35
 
36
36
  # Landing-page static assets, served from /static/* on landing/api hosts.
37
37
  web_dir = Path(__file__).resolve().parents[2] / "web"
@@ -8,13 +8,13 @@ from sqlalchemy.ext.asyncio import AsyncSession
8
8
 
9
9
  from ..config import Settings, get_settings
10
10
  from ..database import get_session
11
- from ..db_models.paste import Paste
12
- from ..schemas.pastes import (
13
- PasteCreate,
14
- PasteCreateResponse,
15
- PasteMetadata,
16
- PasteUpdate,
17
- PasteUpdateResponse,
11
+ from ..db_models.page import Page
12
+ from ..schemas.pages import (
13
+ PageCreate,
14
+ PageCreateResponse,
15
+ PageMetadata,
16
+ PageUpdate,
17
+ PageUpdateResponse,
18
18
  )
19
19
  from ..security import (
20
20
  generate_owner_key,
@@ -33,18 +33,18 @@ def _view_url(settings: Settings, slug: str) -> str:
33
33
 
34
34
 
35
35
  async def _slug_exists(session: AsyncSession, slug: str) -> bool:
36
- res = await session.execute(select(Paste.id).where(Paste.slug == slug))
36
+ res = await session.execute(select(Page.id).where(Page.slug == slug))
37
37
  return res.first() is not None
38
38
 
39
39
 
40
- async def _load_active(session: AsyncSession, slug: str) -> Paste:
41
- res = await session.execute(select(Paste).where(Paste.slug == slug))
42
- paste = res.scalar_one_or_none()
43
- if paste is None or paste.deleted_at is not None:
44
- raise HTTPException(status_code=404, detail="paste not found")
45
- if paste.expires_at is not None and paste.expires_at <= datetime.now(UTC):
46
- raise HTTPException(status_code=404, detail="paste expired")
47
- return paste
40
+ async def _load_active(session: AsyncSession, slug: str) -> Page:
41
+ res = await session.execute(select(Page).where(Page.slug == slug))
42
+ page = res.scalar_one_or_none()
43
+ if page is None or page.deleted_at is not None:
44
+ raise HTTPException(status_code=404, detail="page not found")
45
+ if page.expires_at is not None and page.expires_at <= datetime.now(UTC):
46
+ raise HTTPException(status_code=404, detail="page expired")
47
+ return page
48
48
 
49
49
 
50
50
  def _require_owner_key(x_owner_key: str | None) -> str:
@@ -54,17 +54,17 @@ def _require_owner_key(x_owner_key: str | None) -> str:
54
54
 
55
55
 
56
56
  @router.post(
57
- "/pastes",
57
+ "/pages",
58
58
  status_code=status.HTTP_201_CREATED,
59
- response_model=PasteCreateResponse,
59
+ response_model=PageCreateResponse,
60
60
  )
61
- async def create_paste(
62
- payload: PasteCreate,
61
+ async def create_page(
62
+ payload: PageCreate,
63
63
  request: Request,
64
64
  settings: Settings = Depends(get_settings),
65
65
  session: AsyncSession = Depends(get_session),
66
- ) -> PasteCreateResponse:
67
- return await _create_paste_impl(
66
+ ) -> PageCreateResponse:
67
+ return await _create_page_impl(
68
68
  payload=payload,
69
69
  parent_override=payload.parent_slug,
70
70
  settings=settings,
@@ -73,18 +73,18 @@ async def create_paste(
73
73
 
74
74
 
75
75
  @router.post(
76
- "/pastes/{slug}/version",
76
+ "/pages/{slug}/version",
77
77
  status_code=status.HTTP_201_CREATED,
78
- response_model=PasteCreateResponse,
78
+ response_model=PageCreateResponse,
79
79
  )
80
80
  async def create_version(
81
81
  slug: str,
82
- payload: PasteCreate,
82
+ payload: PageCreate,
83
83
  settings: Settings = Depends(get_settings),
84
84
  session: AsyncSession = Depends(get_session),
85
- ) -> PasteCreateResponse:
85
+ ) -> PageCreateResponse:
86
86
  parent = await _load_active(session, slug)
87
- return await _create_paste_impl(
87
+ return await _create_page_impl(
88
88
  payload=payload,
89
89
  parent_override=parent.slug,
90
90
  settings=settings,
@@ -92,13 +92,13 @@ async def create_version(
92
92
  )
93
93
 
94
94
 
95
- async def _create_paste_impl(
95
+ async def _create_page_impl(
96
96
  *,
97
- payload: PasteCreate,
97
+ payload: PageCreate,
98
98
  parent_override: str | None,
99
99
  settings: Settings,
100
100
  session: AsyncSession,
101
- ) -> PasteCreateResponse:
101
+ ) -> PageCreateResponse:
102
102
  html_bytes = payload.html.encode("utf-8")
103
103
  if len(html_bytes) > settings.max_payload_bytes:
104
104
  raise HTTPException(status_code=413, detail="html exceeds size limit")
@@ -122,7 +122,7 @@ async def _create_paste_impl(
122
122
  elif settings.default_expires_in_minutes:
123
123
  expires_at = datetime.now(UTC) + timedelta(minutes=settings.default_expires_in_minutes)
124
124
 
125
- paste = Paste(
125
+ page = Page(
126
126
  slug=slug,
127
127
  blob_key="", # set after we know the id
128
128
  title=payload.title,
@@ -133,101 +133,101 @@ async def _create_paste_impl(
133
133
  size_bytes=len(html_bytes),
134
134
  parent_slug=parent_override,
135
135
  )
136
- session.add(paste)
136
+ session.add(page)
137
137
  await session.flush()
138
138
 
139
- blob_key = make_blob_key(paste.id)
140
- paste.blob_key = blob_key
139
+ blob_key = make_blob_key(page.id)
140
+ page.blob_key = blob_key
141
141
 
142
142
  store = get_blob_store()
143
143
  await store.put(blob_key, html_bytes)
144
144
 
145
145
  await session.commit()
146
- await session.refresh(paste)
146
+ await session.refresh(page)
147
147
 
148
- return PasteCreateResponse(
149
- slug=paste.slug,
150
- url=_view_url(settings, paste.slug),
148
+ return PageCreateResponse(
149
+ slug=page.slug,
150
+ url=_view_url(settings, page.slug),
151
151
  owner_key=owner_key,
152
- expires_at=paste.expires_at,
153
- created_at=paste.created_at,
152
+ expires_at=page.expires_at,
153
+ created_at=page.created_at,
154
154
  )
155
155
 
156
156
 
157
- @router.get("/pastes/{slug}", response_model=PasteMetadata)
158
- async def get_paste_metadata(
157
+ @router.get("/pages/{slug}", response_model=PageMetadata)
158
+ async def get_page_metadata(
159
159
  slug: str,
160
160
  settings: Settings = Depends(get_settings),
161
161
  session: AsyncSession = Depends(get_session),
162
- ) -> PasteMetadata:
163
- paste = await _load_active(session, slug)
164
- return PasteMetadata(
165
- slug=paste.slug,
166
- title=paste.title,
167
- url=_view_url(settings, paste.slug),
168
- expires_at=paste.expires_at,
169
- created_at=paste.created_at,
170
- updated_at=paste.updated_at,
171
- view_count=paste.view_count,
172
- size_bytes=paste.size_bytes,
173
- has_password=paste.password_hash is not None,
174
- parent_slug=paste.parent_slug,
162
+ ) -> PageMetadata:
163
+ page = await _load_active(session, slug)
164
+ return PageMetadata(
165
+ slug=page.slug,
166
+ title=page.title,
167
+ url=_view_url(settings, page.slug),
168
+ expires_at=page.expires_at,
169
+ created_at=page.created_at,
170
+ updated_at=page.updated_at,
171
+ view_count=page.view_count,
172
+ size_bytes=page.size_bytes,
173
+ has_password=page.password_hash is not None,
174
+ parent_slug=page.parent_slug,
175
175
  )
176
176
 
177
177
 
178
- @router.patch("/pastes/{slug}", response_model=PasteUpdateResponse)
179
- async def update_paste(
178
+ @router.patch("/pages/{slug}", response_model=PageUpdateResponse)
179
+ async def update_page(
180
180
  slug: str,
181
- payload: PasteUpdate,
181
+ payload: PageUpdate,
182
182
  x_owner_key: str | None = Header(default=None, alias="X-Owner-Key"),
183
183
  settings: Settings = Depends(get_settings),
184
184
  session: AsyncSession = Depends(get_session),
185
- ) -> PasteUpdateResponse:
185
+ ) -> PageUpdateResponse:
186
186
  owner_key = _require_owner_key(x_owner_key)
187
- paste = await _load_active(session, slug)
188
- if not verify_owner_key(owner_key, paste.owner_key_hash):
187
+ page = await _load_active(session, slug)
188
+ if not verify_owner_key(owner_key, page.owner_key_hash):
189
189
  raise HTTPException(status_code=403, detail="invalid owner key")
190
190
 
191
191
  html_bytes = payload.html.encode("utf-8")
192
192
  if len(html_bytes) > settings.max_payload_bytes:
193
193
  raise HTTPException(status_code=413, detail="html exceeds size limit")
194
194
 
195
- paste.size_bytes = len(html_bytes)
195
+ page.size_bytes = len(html_bytes)
196
196
  if payload.title is not None:
197
- paste.title = payload.title
197
+ page.title = payload.title
198
198
 
199
199
  store = get_blob_store()
200
- await store.put(paste.blob_key, html_bytes)
200
+ await store.put(page.blob_key, html_bytes)
201
201
 
202
202
  await session.commit()
203
- await session.refresh(paste)
203
+ await session.refresh(page)
204
204
 
205
- return PasteUpdateResponse(
206
- slug=paste.slug,
207
- url=_view_url(settings, paste.slug),
208
- expires_at=paste.expires_at,
209
- updated_at=paste.updated_at,
205
+ return PageUpdateResponse(
206
+ slug=page.slug,
207
+ url=_view_url(settings, page.slug),
208
+ expires_at=page.expires_at,
209
+ updated_at=page.updated_at,
210
210
  )
211
211
 
212
212
 
213
213
  @router.delete(
214
- "/pastes/{slug}",
214
+ "/pages/{slug}",
215
215
  status_code=status.HTTP_204_NO_CONTENT,
216
216
  response_class=Response,
217
217
  )
218
- async def delete_paste(
218
+ async def delete_page(
219
219
  slug: str,
220
220
  x_owner_key: str | None = Header(default=None, alias="X-Owner-Key"),
221
221
  session: AsyncSession = Depends(get_session),
222
222
  ) -> Response:
223
223
  owner_key = _require_owner_key(x_owner_key)
224
- paste = await _load_active(session, slug)
225
- if not verify_owner_key(owner_key, paste.owner_key_hash):
224
+ page = await _load_active(session, slug)
225
+ if not verify_owner_key(owner_key, page.owner_key_hash):
226
226
  raise HTTPException(status_code=403, detail="invalid owner key")
227
- paste.deleted_at = datetime.now(UTC)
227
+ page.deleted_at = datetime.now(UTC)
228
228
  store = get_blob_store()
229
229
  try:
230
- await store.delete(paste.blob_key)
230
+ await store.delete(page.blob_key)
231
231
  except Exception:
232
232
  pass
233
233
  await session.commit()
@@ -10,7 +10,7 @@ from starlette.responses import HTMLResponse
10
10
 
11
11
  from ..config import Settings, get_settings
12
12
  from ..database import get_session
13
- from ..db_models.paste import Paste
13
+ from ..db_models.page import Page
14
14
  from ..security import verify_password
15
15
  from ..storage import get_blob_store
16
16
 
@@ -55,7 +55,7 @@ PASSWORD_FORM_TEMPLATE = """<!doctype html>
55
55
  </head>
56
56
  <body>
57
57
  <main>
58
- <h1>This paste is password-protected.</h1>
58
+ <h1>This page is password-protected.</h1>
59
59
  <p>Enter the password to view it.</p>
60
60
  {error_html}
61
61
  <form method="post">
@@ -69,7 +69,7 @@ PASSWORD_FORM_TEMPLATE = """<!doctype html>
69
69
 
70
70
 
71
71
  def _serializer(settings: Settings) -> URLSafeSerializer:
72
- return URLSafeSerializer(settings.secret_key, salt="htmlship-paste-session")
72
+ return URLSafeSerializer(settings.secret_key, salt="htmlship-page-session")
73
73
 
74
74
 
75
75
  def _cookie_name(slug: str) -> str:
@@ -108,27 +108,27 @@ def _password_form(slug: str, error: str | None = None) -> HTMLResponse:
108
108
  return HTMLResponse(body, headers=headers)
109
109
 
110
110
 
111
- async def _load_active_view(session: AsyncSession, slug: str) -> Paste:
111
+ async def _load_active_view(session: AsyncSession, slug: str) -> Page:
112
112
  from sqlalchemy import select
113
113
 
114
- res = await session.execute(select(Paste).where(Paste.slug == slug))
115
- paste = res.scalar_one_or_none()
116
- if paste is None or paste.deleted_at is not None:
114
+ res = await session.execute(select(Page).where(Page.slug == slug))
115
+ page = res.scalar_one_or_none()
116
+ if page is None or page.deleted_at is not None:
117
117
  raise HTTPException(status_code=404, detail="not found")
118
- if paste.expires_at is not None and paste.expires_at <= datetime.now(UTC):
118
+ if page.expires_at is not None and page.expires_at <= datetime.now(UTC):
119
119
  raise HTTPException(status_code=404, detail="expired")
120
- return paste
120
+ return page
121
121
 
122
122
 
123
- async def _serve_html(paste: Paste) -> HTMLResponse:
123
+ async def _serve_html(page: Page) -> HTMLResponse:
124
124
  store = get_blob_store()
125
- data = await store.get(paste.blob_key)
125
+ data = await store.get(page.blob_key)
126
126
  return HTMLResponse(data, headers=SECURITY_HEADERS)
127
127
 
128
128
 
129
- async def _bump_view_count(session: AsyncSession, paste_id) -> None:
129
+ async def _bump_view_count(session: AsyncSession, page_id) -> None:
130
130
  await session.execute(
131
- update(Paste).where(Paste.id == paste_id).values(view_count=Paste.view_count + 1)
131
+ update(Page).where(Page.id == page_id).values(view_count=Page.view_count + 1)
132
132
  )
133
133
  await session.commit()
134
134
 
@@ -140,13 +140,13 @@ async def view_get(
140
140
  settings: Settings = Depends(get_settings),
141
141
  session: AsyncSession = Depends(get_session),
142
142
  ) -> Response:
143
- paste = await _load_active_view(session, slug)
143
+ page = await _load_active_view(session, slug)
144
144
 
145
- if paste.password_hash and not _has_valid_session(request, slug, settings):
145
+ if page.password_hash and not _has_valid_session(request, slug, settings):
146
146
  return _password_form(slug)
147
147
 
148
- response = await _serve_html(paste)
149
- await _bump_view_count(session, paste.id)
148
+ response = await _serve_html(page)
149
+ await _bump_view_count(session, page.id)
150
150
  return response
151
151
 
152
152
 
@@ -158,18 +158,18 @@ async def view_post_password(
158
158
  settings: Settings = Depends(get_settings),
159
159
  session: AsyncSession = Depends(get_session),
160
160
  ) -> Response:
161
- paste = await _load_active_view(session, slug)
161
+ page = await _load_active_view(session, slug)
162
162
 
163
- if not paste.password_hash:
163
+ if not page.password_hash:
164
164
  # No password set; redirect-equivalent: just serve.
165
- response = await _serve_html(paste)
166
- await _bump_view_count(session, paste.id)
165
+ response = await _serve_html(page)
166
+ await _bump_view_count(session, page.id)
167
167
  return response
168
168
 
169
- if not verify_password(password, paste.password_hash):
169
+ if not verify_password(password, page.password_hash):
170
170
  return _password_form(slug, error="Wrong password.")
171
171
 
172
- response = await _serve_html(paste)
172
+ response = await _serve_html(page)
173
173
  _set_session_cookie(response, slug, settings)
174
- await _bump_view_count(session, paste.id)
174
+ await _bump_view_count(session, page.id)
175
175
  return response
@@ -8,7 +8,7 @@ from pydantic import BaseModel, Field, field_validator
8
8
  MAX_EXPIRES_IN_MINUTES = 60 * 24 * 7 # 7 days
9
9
 
10
10
 
11
- class PasteCreate(BaseModel):
11
+ class PageCreate(BaseModel):
12
12
  html: str = Field(..., min_length=0, max_length=10 * 1024 * 1024)
13
13
  title: str | None = Field(default=None, max_length=200)
14
14
  password: str | None = Field(default=None, max_length=128)
@@ -28,12 +28,12 @@ class PasteCreate(BaseModel):
28
28
  return v
29
29
 
30
30
 
31
- class PasteUpdate(BaseModel):
31
+ class PageUpdate(BaseModel):
32
32
  html: str = Field(..., min_length=0, max_length=10 * 1024 * 1024)
33
33
  title: str | None = Field(default=None, max_length=200)
34
34
 
35
35
 
36
- class PasteCreateResponse(BaseModel):
36
+ class PageCreateResponse(BaseModel):
37
37
  slug: str
38
38
  url: str
39
39
  owner_key: str
@@ -41,7 +41,7 @@ class PasteCreateResponse(BaseModel):
41
41
  created_at: datetime
42
42
 
43
43
 
44
- class PasteMetadata(BaseModel):
44
+ class PageMetadata(BaseModel):
45
45
  slug: str
46
46
  title: str | None
47
47
  url: str
@@ -54,7 +54,7 @@ class PasteMetadata(BaseModel):
54
54
  parent_slug: str | None
55
55
 
56
56
 
57
- class PasteUpdateResponse(BaseModel):
57
+ class PageUpdateResponse(BaseModel):
58
58
  slug: str
59
59
  url: str
60
60
  expires_at: datetime | None
@@ -11,9 +11,9 @@ from botocore.config import Config
11
11
  from .config import get_settings
12
12
 
13
13
 
14
- def make_blob_key(paste_id: uuid.UUID, *, when: datetime | None = None) -> str:
14
+ def make_blob_key(page_id: uuid.UUID, *, when: datetime | None = None) -> str:
15
15
  when = when or datetime.now(UTC)
16
- return f"pastes/{when.year:04d}/{when.month:02d}/{paste_id}.html"
16
+ return f"pages/{when.year:04d}/{when.month:02d}/{page_id}.html"
17
17
 
18
18
 
19
19
  class BlobStore(Protocol):
@@ -1 +0,0 @@
1
- __version__ = "0.1.2"
@@ -1 +0,0 @@
1
- __version__ = "0.1.2"
@@ -1 +0,0 @@
1
- __version__ = "0.1.2"
@@ -1,3 +0,0 @@
1
- from .paste import Base, Paste
2
-
3
- __all__ = ["Base", "Paste"]
File without changes