wagapi 0.2.0__tar.gz → 0.2.2__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 (28) hide show
  1. {wagapi-0.2.0 → wagapi-0.2.2}/PKG-INFO +12 -12
  2. {wagapi-0.2.0 → wagapi-0.2.2}/README.md +11 -11
  3. {wagapi-0.2.0 → wagapi-0.2.2}/pyproject.toml +1 -1
  4. {wagapi-0.2.0 → wagapi-0.2.2}/tests/test_commands.py +22 -0
  5. wagapi-0.2.2/wagapi/__init__.py +1 -0
  6. {wagapi-0.2.0 → wagapi-0.2.2}/wagapi/cli.py +12 -1
  7. {wagapi-0.2.0 → wagapi-0.2.2}/wagapi/commands/pages.py +2 -2
  8. wagapi-0.2.2/wagapi/commands/schema.py +87 -0
  9. {wagapi-0.2.0 → wagapi-0.2.2}/wagapi/formatting/output.py +25 -2
  10. wagapi-0.2.0/wagapi/__init__.py +0 -1
  11. wagapi-0.2.0/wagapi/commands/schema.py +0 -43
  12. {wagapi-0.2.0 → wagapi-0.2.2}/.github/workflows/publish.yml +0 -0
  13. {wagapi-0.2.0 → wagapi-0.2.2}/.gitignore +0 -0
  14. {wagapi-0.2.0 → wagapi-0.2.2}/tests/__init__.py +0 -0
  15. {wagapi-0.2.0 → wagapi-0.2.2}/tests/conftest.py +0 -0
  16. {wagapi-0.2.0 → wagapi-0.2.2}/tests/test_client.py +0 -0
  17. {wagapi-0.2.0 → wagapi-0.2.2}/tests/test_config.py +0 -0
  18. {wagapi-0.2.0 → wagapi-0.2.2}/tests/test_markdown.py +0 -0
  19. {wagapi-0.2.0 → wagapi-0.2.2}/wagapi/__main__.py +0 -0
  20. {wagapi-0.2.0 → wagapi-0.2.2}/wagapi/client.py +0 -0
  21. {wagapi-0.2.0 → wagapi-0.2.2}/wagapi/commands/__init__.py +0 -0
  22. {wagapi-0.2.0 → wagapi-0.2.2}/wagapi/commands/images.py +0 -0
  23. {wagapi-0.2.0 → wagapi-0.2.2}/wagapi/commands/init.py +0 -0
  24. {wagapi-0.2.0 → wagapi-0.2.2}/wagapi/config.py +0 -0
  25. {wagapi-0.2.0 → wagapi-0.2.2}/wagapi/exceptions.py +0 -0
  26. {wagapi-0.2.0 → wagapi-0.2.2}/wagapi/formatting/__init__.py +0 -0
  27. {wagapi-0.2.0 → wagapi-0.2.2}/wagapi/formatting/markdown.py +0 -0
  28. {wagapi-0.2.0 → wagapi-0.2.2}/wagapi/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wagapi
3
- Version: 0.2.0
3
+ Version: 0.2.2
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
@@ -121,13 +121,13 @@ wagapi schema testapp.BlogPage
121
121
 
122
122
  ```bash
123
123
  wagapi pages list
124
- wagapi pages get 6
124
+ wagapi pages get 3 # use an ID from the list above
125
125
  ```
126
126
 
127
127
  ### 5. Create a page
128
128
 
129
129
  ```bash
130
- wagapi pages create testapp.BlogPage --parent 5 \
130
+ wagapi pages create testapp.BlogPage --parent /blog/ \
131
131
  --title "Iris Murdoch" \
132
132
  --body "## A Philosopher and Novelist
133
133
 
@@ -142,10 +142,10 @@ The `--body` flag accepts markdown, which is auto-converted to StreamField block
142
142
 
143
143
  ```bash
144
144
  # Publish a draft (use the page ID returned by create)
145
- wagapi pages publish 15
145
+ wagapi pages publish <ID>
146
146
 
147
147
  # Update a page
148
- wagapi pages update 15 --title "Iris Murdoch: The Sovereignty of Good"
148
+ wagapi pages update <ID> --title "Iris Murdoch: The Sovereignty of Good"
149
149
 
150
150
  # Create and publish in one step, using a URL path as parent
151
151
  wagapi pages create testapp.BlogPage --parent /blog/ \
@@ -155,20 +155,20 @@ wagapi pages create testapp.BlogPage --parent /blog/ \
155
155
  --publish
156
156
 
157
157
  # Unpublish
158
- wagapi pages unpublish 15
158
+ wagapi pages unpublish <ID>
159
159
 
160
160
  # Delete (prompts for confirmation)
161
- wagapi pages delete 15
161
+ wagapi pages delete <ID>
162
162
  ```
163
163
 
164
164
  ### 7. Inspect requests
165
165
 
166
166
  ```bash
167
167
  # See HTTP request/response details
168
- wagapi -v pages get 6
168
+ wagapi -v pages get <ID>
169
169
 
170
170
  # Preview without executing
171
- wagapi --dry-run pages create testapp.SimplePage --parent 3 --title "Test"
171
+ wagapi --dry-run pages create testapp.SimplePage --parent /home/ --title "Test"
172
172
  ```
173
173
 
174
174
  ### 8. Pipe-friendly JSON output
@@ -184,7 +184,7 @@ Force a format with `--json` or `--human`:
184
184
 
185
185
  ```bash
186
186
  wagapi --human pages list
187
- wagapi --json pages get 6
187
+ wagapi --json pages get 42
188
188
  ```
189
189
 
190
190
  ## Configuration
@@ -305,14 +305,14 @@ She argued that moral progress comes from **attention**."
305
305
  **With extra fields:**
306
306
 
307
307
  ```bash
308
- wagapi pages create testapp.BlogPage --parent 5 \
308
+ wagapi pages create testapp.BlogPage --parent /blog/ \
309
309
  --title "Iris Murdoch" --field published_date:2026-04-06
310
310
  ```
311
311
 
312
312
  **Raw mode for full StreamField control:**
313
313
 
314
314
  ```bash
315
- wagapi pages create testapp.BlogPage --parent 5 \
315
+ wagapi pages create testapp.BlogPage --parent /blog/ \
316
316
  --title "Iris Murdoch" --raw \
317
317
  --field 'body:[{"type":"paragraph","value":"<p>Hello</p>","id":"abc123"}]'
318
318
  ```
@@ -91,13 +91,13 @@ wagapi schema testapp.BlogPage
91
91
 
92
92
  ```bash
93
93
  wagapi pages list
94
- wagapi pages get 6
94
+ wagapi pages get 3 # use an ID from the list above
95
95
  ```
96
96
 
97
97
  ### 5. Create a page
98
98
 
99
99
  ```bash
100
- wagapi pages create testapp.BlogPage --parent 5 \
100
+ wagapi pages create testapp.BlogPage --parent /blog/ \
101
101
  --title "Iris Murdoch" \
102
102
  --body "## A Philosopher and Novelist
103
103
 
@@ -112,10 +112,10 @@ The `--body` flag accepts markdown, which is auto-converted to StreamField block
112
112
 
113
113
  ```bash
114
114
  # Publish a draft (use the page ID returned by create)
115
- wagapi pages publish 15
115
+ wagapi pages publish <ID>
116
116
 
117
117
  # Update a page
118
- wagapi pages update 15 --title "Iris Murdoch: The Sovereignty of Good"
118
+ wagapi pages update <ID> --title "Iris Murdoch: The Sovereignty of Good"
119
119
 
120
120
  # Create and publish in one step, using a URL path as parent
121
121
  wagapi pages create testapp.BlogPage --parent /blog/ \
@@ -125,20 +125,20 @@ wagapi pages create testapp.BlogPage --parent /blog/ \
125
125
  --publish
126
126
 
127
127
  # Unpublish
128
- wagapi pages unpublish 15
128
+ wagapi pages unpublish <ID>
129
129
 
130
130
  # Delete (prompts for confirmation)
131
- wagapi pages delete 15
131
+ wagapi pages delete <ID>
132
132
  ```
133
133
 
134
134
  ### 7. Inspect requests
135
135
 
136
136
  ```bash
137
137
  # See HTTP request/response details
138
- wagapi -v pages get 6
138
+ wagapi -v pages get <ID>
139
139
 
140
140
  # Preview without executing
141
- wagapi --dry-run pages create testapp.SimplePage --parent 3 --title "Test"
141
+ wagapi --dry-run pages create testapp.SimplePage --parent /home/ --title "Test"
142
142
  ```
143
143
 
144
144
  ### 8. Pipe-friendly JSON output
@@ -154,7 +154,7 @@ Force a format with `--json` or `--human`:
154
154
 
155
155
  ```bash
156
156
  wagapi --human pages list
157
- wagapi --json pages get 6
157
+ wagapi --json pages get 42
158
158
  ```
159
159
 
160
160
  ## Configuration
@@ -275,14 +275,14 @@ She argued that moral progress comes from **attention**."
275
275
  **With extra fields:**
276
276
 
277
277
  ```bash
278
- wagapi pages create testapp.BlogPage --parent 5 \
278
+ wagapi pages create testapp.BlogPage --parent /blog/ \
279
279
  --title "Iris Murdoch" --field published_date:2026-04-06
280
280
  ```
281
281
 
282
282
  **Raw mode for full StreamField control:**
283
283
 
284
284
  ```bash
285
- wagapi pages create testapp.BlogPage --parent 5 \
285
+ wagapi pages create testapp.BlogPage --parent /blog/ \
286
286
  --title "Iris Murdoch" --raw \
287
287
  --field 'body:[{"type":"paragraph","value":"<p>Hello</p>","id":"abc123"}]'
288
288
  ```
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "wagapi"
7
- version = "0.2.0"
7
+ version = "0.2.2"
8
8
  description = "CLI client for the Wagtail Write API, optimised for LLM orchestration"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -97,6 +97,28 @@ def test_pages_list(runner):
97
97
  assert "Hello" in result.output
98
98
 
99
99
 
100
+ @respx.mock
101
+ def test_pages_list_with_parent(runner):
102
+ data = {
103
+ "meta": {"total_count": 1},
104
+ "items": [
105
+ {
106
+ "id": 42,
107
+ "title": "Hello",
108
+ "meta": {"type": "blog.BlogPage", "live": True},
109
+ }
110
+ ],
111
+ }
112
+ route = respx.get(f"{BASE_URL}/pages/").mock(
113
+ return_value=Response(200, json=data)
114
+ )
115
+ with mock.patch.dict("os.environ", ENV):
116
+ result = runner.invoke(cli, ["pages", "list", "--parent", "5"])
117
+ assert result.exit_code == 0
118
+ assert route.called
119
+ assert "parent" in str(route.calls[0].request.url)
120
+
121
+
100
122
  @respx.mock
101
123
  def test_pages_get(runner):
102
124
  data = {
@@ -0,0 +1 @@
1
+ __version__ = "0.2.2"
@@ -35,7 +35,18 @@ def handle_api_errors(fn):
35
35
  except WagapiError as exc:
36
36
  click.echo(f"Error: {exc}", err=True)
37
37
  if hasattr(exc, "details") and exc.details:
38
- click.echo(f"Details: {exc.details}", err=True)
38
+ details = exc.details
39
+ # Format field-level validation errors readably
40
+ if isinstance(details, dict) and "details" in details:
41
+ for item in details["details"]:
42
+ field = item.get("field", "")
43
+ msg = item.get("message", str(item))
44
+ if field:
45
+ click.echo(f" {field}: {msg}", err=True)
46
+ else:
47
+ click.echo(f" {msg}", err=True)
48
+ else:
49
+ click.echo(f"Details: {details}", err=True)
39
50
  sys.exit(exc.exit_code)
40
51
 
41
52
  return wrapper
@@ -91,7 +91,7 @@ def list_pages(
91
91
  if page_type:
92
92
  params["type"] = page_type
93
93
  if parent:
94
- params["child_of"] = parent
94
+ params["parent"] = parent
95
95
  if descendant_of:
96
96
  params["descendant_of"] = descendant_of
97
97
  if status:
@@ -259,7 +259,7 @@ def delete(ctx: Context, page_id: int, yes: bool):
259
259
 
260
260
  ctx.client.delete_page(page_id)
261
261
  result = output(
262
- page_id,
262
+ {"id": page_id, "deleted": True},
263
263
  format_page_deleted,
264
264
  force_json=ctx.force_json,
265
265
  force_human=ctx.force_human,
@@ -0,0 +1,87 @@
1
+ from __future__ import annotations
2
+
3
+ import click
4
+
5
+ from wagapi.cli import Context, handle_api_errors, pass_ctx
6
+ from wagapi.exceptions import UsageError
7
+ from wagapi.formatting.output import (
8
+ format_json,
9
+ format_schema_detail,
10
+ format_schema_list,
11
+ output,
12
+ )
13
+
14
+
15
+ def _build_example_command(page_type: str, data: dict) -> str:
16
+ """Generate an example wagapi create command from schema data."""
17
+ create_schema = data.get("create_schema", {})
18
+ properties = create_schema.get("properties", {})
19
+ required = set(create_schema.get("required", []))
20
+ has_streamfield = bool(data.get("streamfield_blocks"))
21
+
22
+ # CLI handles type and parent itself; action is internal
23
+ skip = {"type", "parent", "action"}
24
+
25
+ parts = [f"wagapi pages create {page_type}", "--parent <ID_OR_PATH>"]
26
+
27
+ for field_name in properties:
28
+ if field_name in skip:
29
+ continue
30
+ if field_name not in required:
31
+ continue
32
+ prop = properties[field_name]
33
+
34
+ # StreamField body → use --body flag
35
+ if field_name == "body" and prop.get("type") == "array":
36
+ parts.append('--body "Your content here (markdown)"')
37
+ continue
38
+
39
+ # Derive a placeholder from the field type
40
+ ftype = prop.get("type", "")
41
+ if ftype == "string":
42
+ parts.append(f'--field {field_name}:"..."')
43
+ elif ftype == "integer":
44
+ parts.append(f"--field {field_name}:<ID>")
45
+ elif ftype == "array":
46
+ parts.append(f"--field {field_name}:[]")
47
+ else:
48
+ parts.append(f'--field {field_name}:"..."')
49
+
50
+ # title is always a CLI flag, not --field
51
+ parts = [p for p in parts if not p.startswith("--field title:")]
52
+ if "title" in required:
53
+ parts.insert(2, '--title "Your Title"')
54
+
55
+ return " \\\n ".join(parts)
56
+
57
+
58
+ @click.command()
59
+ @click.argument("page_type", required=False, default=None)
60
+ @pass_ctx
61
+ @handle_api_errors
62
+ def schema(ctx: Context, page_type: str | None) -> None:
63
+ """List page types, or show the schema for a specific type."""
64
+ if not ctx.client:
65
+ raise UsageError(
66
+ "Not configured. Run 'wagapi init' or set WAGAPI_URL and WAGAPI_TOKEN."
67
+ )
68
+
69
+ if page_type:
70
+ data = ctx.client.get_page_type_schema(page_type)
71
+ data["example_cli"] = _build_example_command(page_type, data)
72
+ result = output(
73
+ data,
74
+ format_schema_detail,
75
+ force_json=ctx.force_json,
76
+ force_human=ctx.force_human,
77
+ )
78
+ else:
79
+ data = ctx.client.list_page_types()
80
+ result = output(
81
+ data,
82
+ format_schema_list,
83
+ force_json=ctx.force_json,
84
+ force_human=ctx.force_human,
85
+ )
86
+
87
+ click.echo(result)
@@ -53,8 +53,8 @@ def format_page_updated(data: dict) -> str:
53
53
  return f'✓ Updated page {data["id"]} "{data.get("title", "")}" ({status})'
54
54
 
55
55
 
56
- def format_page_deleted(page_id: int) -> str:
57
- return f"✓ Deleted page {page_id}"
56
+ def format_page_deleted(data: dict) -> str:
57
+ return f'✓ Deleted page {data["id"]}'
58
58
 
59
59
 
60
60
  def format_page_published(data: dict) -> str:
@@ -75,6 +75,25 @@ def format_page_detail(data: dict) -> str:
75
75
  ]
76
76
  if meta.get("html_url"):
77
77
  lines.append(f' URL: {meta["html_url"]}')
78
+ if meta.get("parent_id"):
79
+ lines.append(f' Parent: {meta["parent_id"]}')
80
+
81
+ # Show custom fields (skip meta, id, title, slug which are already shown)
82
+ skip = {"id", "title", "slug", "meta", "alias_of"}
83
+ for key, value in data.items():
84
+ if key in skip or value is None:
85
+ continue
86
+ if isinstance(value, list) and value and isinstance(value[0], dict) and "type" in value[0]:
87
+ # StreamField — summarise block types
88
+ block_types = [b.get("type", "?") for b in value]
89
+ lines.append(f" {key}: [{', '.join(block_types)}] ({len(value)} blocks)")
90
+ elif isinstance(value, list) and value:
91
+ lines.append(f" {key}: {len(value)} items")
92
+ elif isinstance(value, list):
93
+ pass # skip empty lists
94
+ else:
95
+ lines.append(f" {key}: {value}")
96
+
78
97
  return "\n".join(lines)
79
98
 
80
99
 
@@ -170,6 +189,10 @@ def format_schema_detail(data: dict) -> str:
170
189
  block_names = [b.get("type", b.get("name", "?")) for b in blocks] if isinstance(blocks, list) else list(blocks.keys()) if isinstance(blocks, dict) else []
171
190
  lines.append(f" {field_name}: {', '.join(block_names)}")
172
191
 
192
+ if data.get("example_cli"):
193
+ lines.append("")
194
+ lines.append(f" Example:\n {data['example_cli'].replace(chr(10), chr(10) + ' ')}")
195
+
173
196
  return "\n".join(lines)
174
197
 
175
198
 
@@ -1 +0,0 @@
1
- __version__ = "0.1.0"
@@ -1,43 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import click
4
-
5
- from wagapi.cli import Context, handle_api_errors, pass_ctx
6
- from wagapi.exceptions import UsageError
7
- from wagapi.formatting.output import (
8
- format_json,
9
- format_schema_detail,
10
- format_schema_list,
11
- output,
12
- )
13
-
14
-
15
- @click.command()
16
- @click.argument("page_type", required=False, default=None)
17
- @pass_ctx
18
- @handle_api_errors
19
- def schema(ctx: Context, page_type: str | None) -> None:
20
- """List page types, or show the schema for a specific type."""
21
- if not ctx.client:
22
- raise UsageError(
23
- "Not configured. Run 'wagapi init' or set WAGAPI_URL and WAGAPI_TOKEN."
24
- )
25
-
26
- if page_type:
27
- data = ctx.client.get_page_type_schema(page_type)
28
- result = output(
29
- data,
30
- format_schema_detail,
31
- force_json=ctx.force_json,
32
- force_human=ctx.force_human,
33
- )
34
- else:
35
- data = ctx.client.list_page_types()
36
- result = output(
37
- data,
38
- format_schema_list,
39
- force_json=ctx.force_json,
40
- force_human=ctx.force_human,
41
- )
42
-
43
- click.echo(result)
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