wagapi 0.2.2__tar.gz → 0.2.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 (27) hide show
  1. {wagapi-0.2.2 → wagapi-0.2.3}/PKG-INFO +6 -4
  2. {wagapi-0.2.2 → wagapi-0.2.3}/README.md +5 -3
  3. {wagapi-0.2.2 → wagapi-0.2.3}/pyproject.toml +1 -1
  4. {wagapi-0.2.2 → wagapi-0.2.3}/tests/conftest.py +17 -0
  5. {wagapi-0.2.2 → wagapi-0.2.3}/tests/test_commands.py +66 -2
  6. wagapi-0.2.3/wagapi/__init__.py +1 -0
  7. {wagapi-0.2.2 → wagapi-0.2.3}/wagapi/commands/pages.py +22 -3
  8. wagapi-0.2.2/wagapi/__init__.py +0 -1
  9. {wagapi-0.2.2 → wagapi-0.2.3}/.github/workflows/publish.yml +0 -0
  10. {wagapi-0.2.2 → wagapi-0.2.3}/.gitignore +0 -0
  11. {wagapi-0.2.2 → wagapi-0.2.3}/tests/__init__.py +0 -0
  12. {wagapi-0.2.2 → wagapi-0.2.3}/tests/test_client.py +0 -0
  13. {wagapi-0.2.2 → wagapi-0.2.3}/tests/test_config.py +0 -0
  14. {wagapi-0.2.2 → wagapi-0.2.3}/tests/test_markdown.py +0 -0
  15. {wagapi-0.2.2 → wagapi-0.2.3}/wagapi/__main__.py +0 -0
  16. {wagapi-0.2.2 → wagapi-0.2.3}/wagapi/cli.py +0 -0
  17. {wagapi-0.2.2 → wagapi-0.2.3}/wagapi/client.py +0 -0
  18. {wagapi-0.2.2 → wagapi-0.2.3}/wagapi/commands/__init__.py +0 -0
  19. {wagapi-0.2.2 → wagapi-0.2.3}/wagapi/commands/images.py +0 -0
  20. {wagapi-0.2.2 → wagapi-0.2.3}/wagapi/commands/init.py +0 -0
  21. {wagapi-0.2.2 → wagapi-0.2.3}/wagapi/commands/schema.py +0 -0
  22. {wagapi-0.2.2 → wagapi-0.2.3}/wagapi/config.py +0 -0
  23. {wagapi-0.2.2 → wagapi-0.2.3}/wagapi/exceptions.py +0 -0
  24. {wagapi-0.2.2 → wagapi-0.2.3}/wagapi/formatting/__init__.py +0 -0
  25. {wagapi-0.2.2 → wagapi-0.2.3}/wagapi/formatting/markdown.py +0 -0
  26. {wagapi-0.2.2 → wagapi-0.2.3}/wagapi/formatting/output.py +0 -0
  27. {wagapi-0.2.2 → wagapi-0.2.3}/wagapi/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wagapi
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: CLI client for the Wagtail Write API, optimised for LLM orchestration
5
5
  Project-URL: Repository, https://github.com/tomdyson/wagapi
6
6
  Project-URL: Issues, https://github.com/tomdyson/wagapi/issues
@@ -255,8 +255,8 @@ wagapi pages list [OPTIONS]
255
255
  | Option | Description |
256
256
  |---|---|
257
257
  | `--type TYPE` | Filter by page type, e.g. `testapp.BlogPage` |
258
- | `--parent ID` | Direct children of page ID |
259
- | `--descendant-of ID` | All descendants of page ID |
258
+ | `--parent ID_OR_PATH` | Direct children of page ID or URL path (e.g. `5` or `/blog/`) |
259
+ | `--descendant-of ID_OR_PATH` | All descendants of page ID or URL path |
260
260
  | `--status STATUS` | `draft`, `live`, or `live+draft` |
261
261
  | `--slug SLUG` | Exact slug match |
262
262
  | `--path PATH` | Exact URL path match, e.g. `/blog/my-post/` |
@@ -288,7 +288,9 @@ wagapi pages create <type> --parent ID_OR_PATH --title TITLE [OPTIONS]
288
288
  | `--publish` | Publish immediately (default: create as draft) |
289
289
  | `--raw` | Treat field values as raw JSON (no auto-wrapping) |
290
290
 
291
- **Markdown body (auto-converted to StreamField):**
291
+ **Markdown body (auto-detected: StreamField blocks or RichTextField HTML):**
292
+
293
+ `--body` checks the page type schema to determine the field type. For StreamField fields, markdown is converted to blocks. For RichTextField fields, markdown is sent as-is for server-side conversion to HTML.
292
294
 
293
295
  ```bash
294
296
  wagapi pages create testapp.BlogPage --parent /blog/ \
@@ -225,8 +225,8 @@ wagapi pages list [OPTIONS]
225
225
  | Option | Description |
226
226
  |---|---|
227
227
  | `--type TYPE` | Filter by page type, e.g. `testapp.BlogPage` |
228
- | `--parent ID` | Direct children of page ID |
229
- | `--descendant-of ID` | All descendants of page ID |
228
+ | `--parent ID_OR_PATH` | Direct children of page ID or URL path (e.g. `5` or `/blog/`) |
229
+ | `--descendant-of ID_OR_PATH` | All descendants of page ID or URL path |
230
230
  | `--status STATUS` | `draft`, `live`, or `live+draft` |
231
231
  | `--slug SLUG` | Exact slug match |
232
232
  | `--path PATH` | Exact URL path match, e.g. `/blog/my-post/` |
@@ -258,7 +258,9 @@ wagapi pages create <type> --parent ID_OR_PATH --title TITLE [OPTIONS]
258
258
  | `--publish` | Publish immediately (default: create as draft) |
259
259
  | `--raw` | Treat field values as raw JSON (no auto-wrapping) |
260
260
 
261
- **Markdown body (auto-converted to StreamField):**
261
+ **Markdown body (auto-detected: StreamField blocks or RichTextField HTML):**
262
+
263
+ `--body` checks the page type schema to determine the field type. For StreamField fields, markdown is converted to blocks. For RichTextField fields, markdown is sent as-is for server-side conversion to HTML.
262
264
 
263
265
  ```bash
264
266
  wagapi pages create testapp.BlogPage --parent /blog/ \
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "wagapi"
7
- version = "0.2.2"
7
+ version = "0.2.3"
8
8
  description = "CLI client for the Wagtail Write API, optimised for LLM orchestration"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -51,6 +51,23 @@ SAMPLE_PAGE_TYPE_SCHEMA = {
51
51
  },
52
52
  }
53
53
 
54
+ SAMPLE_RICHTEXT_PAGE_TYPE_SCHEMA = {
55
+ "name": "home.SimplePage",
56
+ "verbose_name": "simple page",
57
+ "create_schema": {
58
+ "required": ["type", "parent", "title"],
59
+ "properties": {
60
+ "type": {"type": "string"},
61
+ "parent": {"type": "integer"},
62
+ "title": {"type": "string"},
63
+ "body": {"type": "string", "description": "RichTextField body content"},
64
+ },
65
+ },
66
+ "allowed_parents": ["wagtailcore.Page"],
67
+ "allowed_children": [],
68
+ "streamfield_blocks": {},
69
+ }
70
+
54
71
  SAMPLE_PAGE = {
55
72
  "id": 42,
56
73
  "title": "Hello",
@@ -158,7 +158,12 @@ def test_pages_create(runner):
158
158
 
159
159
 
160
160
  @respx.mock
161
- def test_pages_create_with_body(runner):
161
+ def test_pages_create_with_body_streamfield(runner):
162
+ """Body is converted to StreamField blocks when the field is a StreamField."""
163
+ schema = {"streamfield_blocks": {"body": [{"type": "heading"}, {"type": "paragraph"}]}}
164
+ respx.get(f"{BASE_URL}/schema/blog.BlogPage/").mock(
165
+ return_value=Response(200, json=schema)
166
+ )
162
167
  data = {
163
168
  "id": 43,
164
169
  "title": "Iris",
@@ -179,13 +184,45 @@ def test_pages_create_with_body(runner):
179
184
  ],
180
185
  )
181
186
  assert result.exit_code == 0
182
- # Verify the request body contained streamfield blocks
183
187
  request_body = json.loads(route.calls[0].request.content)
184
188
  assert isinstance(request_body["body"], list)
185
189
  assert request_body["body"][0]["type"] == "heading"
186
190
  assert request_body["body"][1]["type"] == "paragraph"
187
191
 
188
192
 
193
+ @respx.mock
194
+ def test_pages_create_with_body_richtext(runner):
195
+ """Body is sent as richtext format dict when the field is a RichTextField."""
196
+ schema = {"streamfield_blocks": {}}
197
+ respx.get(f"{BASE_URL}/schema/home.SimplePage/").mock(
198
+ return_value=Response(200, json=schema)
199
+ )
200
+ data = {
201
+ "id": 50,
202
+ "title": "About",
203
+ "slug": "about",
204
+ "meta": {"type": "home.SimplePage", "live": False, "parent_id": 3},
205
+ }
206
+ route = respx.post(f"{BASE_URL}/pages/").mock(
207
+ return_value=Response(201, json=data)
208
+ )
209
+ with mock.patch.dict("os.environ", ENV):
210
+ result = runner.invoke(
211
+ cli,
212
+ [
213
+ "pages", "create", "home.SimplePage",
214
+ "--parent", "3",
215
+ "--title", "About",
216
+ "--body", "Hello **world**",
217
+ ],
218
+ )
219
+ assert result.exit_code == 0
220
+ request_body = json.loads(route.calls[0].request.content)
221
+ assert isinstance(request_body["body"], dict)
222
+ assert request_body["body"]["format"] == "markdown"
223
+ assert request_body["body"]["content"] == "Hello **world**"
224
+
225
+
189
226
  @respx.mock
190
227
  def test_pages_create_with_path_parent(runner):
191
228
  data = {
@@ -245,6 +282,33 @@ def test_pages_update(runner):
245
282
  assert "Updated" in result.output
246
283
 
247
284
 
285
+ @respx.mock
286
+ def test_pages_update_with_body_richtext(runner):
287
+ """Update fetches the page type and sends richtext for RichTextField."""
288
+ # Mock GET to fetch the page (to learn its type)
289
+ page_data = {
290
+ "id": 42, "title": "Old", "body": "<p>old</p>",
291
+ "meta": {"type": "home.SimplePage", "live": True},
292
+ }
293
+ respx.get(f"{BASE_URL}/pages/42/").mock(return_value=Response(200, json=page_data))
294
+ # Mock schema lookup
295
+ schema = {"streamfield_blocks": {}}
296
+ respx.get(f"{BASE_URL}/schema/home.SimplePage/").mock(
297
+ return_value=Response(200, json=schema)
298
+ )
299
+ # Mock PATCH
300
+ updated = {**page_data, "body": "<p>new</p>"}
301
+ route = respx.patch(f"{BASE_URL}/pages/42/").mock(
302
+ return_value=Response(200, json=updated)
303
+ )
304
+ with mock.patch.dict("os.environ", ENV):
305
+ result = runner.invoke(cli, ["pages", "update", "42", "--body", "new content"])
306
+ assert result.exit_code == 0
307
+ request_body = json.loads(route.calls[0].request.content)
308
+ assert isinstance(request_body["body"], dict)
309
+ assert request_body["body"]["format"] == "markdown"
310
+
311
+
248
312
  @respx.mock
249
313
  def test_pages_delete(runner):
250
314
  respx.delete(f"{BASE_URL}/pages/42/").mock(return_value=Response(204))
@@ -0,0 +1 @@
1
+ __version__ = "0.2.3"
@@ -7,7 +7,7 @@ import click
7
7
 
8
8
  from wagapi.cli import Context, handle_api_errors, pass_ctx
9
9
  from wagapi.exceptions import UsageError
10
- from wagapi.formatting.markdown import markdown_to_streamfield
10
+ from wagapi.formatting.markdown import markdown_to_richtext, markdown_to_streamfield
11
11
  from wagapi.formatting.output import (
12
12
  format_page_created,
13
13
  format_page_deleted,
@@ -27,6 +27,15 @@ def _require_client(ctx: Context):
27
27
  )
28
28
 
29
29
 
30
+ def _is_streamfield(ctx: Context, page_type: str, field_name: str) -> bool:
31
+ """Check if a field is a StreamField by looking for it in streamfield_blocks."""
32
+ try:
33
+ schema = ctx.client.get_page_type_schema(page_type)
34
+ return field_name in schema.get("streamfield_blocks", {})
35
+ except Exception:
36
+ return True # default to StreamField if schema lookup fails
37
+
38
+
30
39
  def _parse_parent(value: str) -> int | str:
31
40
  """Parse parent as int ID or string path."""
32
41
  try:
@@ -175,7 +184,11 @@ def create(ctx: Context, page_type, parent, title, slug, fields, body, publish,
175
184
  except json.JSONDecodeError:
176
185
  data["body"] = body
177
186
  else:
178
- data["body"] = markdown_to_streamfield(body)
187
+ # Check if body is a StreamField or RichTextField
188
+ if _is_streamfield(ctx, page_type, "body"):
189
+ data["body"] = markdown_to_streamfield(body)
190
+ else:
191
+ data["body"] = markdown_to_richtext(body)
179
192
 
180
193
  if publish:
181
194
  data["action"] = "publish"
@@ -225,7 +238,13 @@ def update(ctx: Context, page_id, title, slug, fields, body, publish, raw):
225
238
  except json.JSONDecodeError:
226
239
  data["body"] = body
227
240
  else:
228
- data["body"] = markdown_to_streamfield(body)
241
+ # Fetch page to determine its type, then check schema
242
+ page_data = ctx.client.get_page(page_id)
243
+ page_type = page_data.get("meta", {}).get("type", "")
244
+ if _is_streamfield(ctx, page_type, "body"):
245
+ data["body"] = markdown_to_streamfield(body)
246
+ else:
247
+ data["body"] = markdown_to_richtext(body)
229
248
 
230
249
  if publish:
231
250
  data["action"] = "publish"
@@ -1 +0,0 @@
1
- __version__ = "0.2.2"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes