pptx-cli 1.2.6__tar.gz → 1.3.1__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 (77) hide show
  1. pptx_cli-1.3.1/.claude/settings.local.json +7 -0
  2. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/PKG-INFO +1 -1
  3. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/src/pptx_cli/__init__.py +1 -1
  4. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/src/pptx_cli/cli.py +22 -0
  5. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/src/pptx_cli/commands/guide.py +10 -0
  6. pptx_cli-1.3.1/src/pptx_cli/commands/schema.py +227 -0
  7. pptx_cli-1.3.1/tests/test_schema.py +80 -0
  8. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/.editorconfig +0 -0
  9. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  10. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  11. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  12. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  13. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/.github/copilot-instructions.md +0 -0
  14. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/.github/instructions/backend.instructions.md +0 -0
  15. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/.github/instructions/testing.instructions.md +0 -0
  16. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/.github/skills/pptx/SKILL.md +0 -0
  17. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/.github/skills/pptx/references/deck-spec.md +0 -0
  18. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/.github/skills/pptx-deck-builder/SKILL.md +0 -0
  19. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/.github/skills/pptx-deck-builder/references/excal-diagrams.md +0 -0
  20. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/.github/skills/pptx-deck-builder/references/mckinsey-style.md +0 -0
  21. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/.github/skills/pptx-deck-builder/references/pptx-workflow.md +0 -0
  22. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/.github/workflows/ci.yml +0 -0
  23. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/.github/workflows/publish.yml +0 -0
  24. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/.gitignore +0 -0
  25. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/.python-version +0 -0
  26. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/AGENTS.md +0 -0
  27. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/ARCHITECTURE.md +0 -0
  28. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/CHANGELOG.md +0 -0
  29. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/CLI-MANIFEST.md +0 -0
  30. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/CONTRIBUTING.md +0 -0
  31. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/DECISIONS/ADR-0001-initial-architecture.md +0 -0
  32. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/LICENSE +0 -0
  33. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/PRD.md +0 -0
  34. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/PREVIEW-FUTURE.md +0 -0
  35. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/PROJECT.md +0 -0
  36. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/README.md +0 -0
  37. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/SCAFFOLD.md +0 -0
  38. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/SECURITY.md +0 -0
  39. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/TESTING.md +0 -0
  40. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/docs/DOMAIN.md +0 -0
  41. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/docs/GLOSSARY.md +0 -0
  42. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/docs/ROADMAP.md +0 -0
  43. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/docs/SCAFFOLDING-NOTES.md +0 -0
  44. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/guardrails/.gitignore +0 -0
  45. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/guardrails/guardrails.jsonl +0 -0
  46. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/guardrails/links.jsonl +0 -0
  47. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/guardrails/references.jsonl +0 -0
  48. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/guardrails/taxonomy.json +0 -0
  49. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/guardrails-explorer.html +0 -0
  50. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/pyproject.toml +0 -0
  51. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/pyrightconfig.json +0 -0
  52. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/scripts/bump_version.py +0 -0
  53. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/src/pptx_cli/__main__.py +0 -0
  54. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/src/pptx_cli/commands/__init__.py +0 -0
  55. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/src/pptx_cli/commands/compose.py +0 -0
  56. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/src/pptx_cli/commands/init.py +0 -0
  57. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/src/pptx_cli/commands/inspect.py +0 -0
  58. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/src/pptx_cli/commands/manifest_ops.py +0 -0
  59. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/src/pptx_cli/commands/validate.py +0 -0
  60. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/src/pptx_cli/commands/wrapper.py +0 -0
  61. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/src/pptx_cli/core/__init__.py +0 -0
  62. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/src/pptx_cli/core/composition.py +0 -0
  63. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/src/pptx_cli/core/ids.py +0 -0
  64. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/src/pptx_cli/core/io.py +0 -0
  65. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/src/pptx_cli/core/manifest_store.py +0 -0
  66. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/src/pptx_cli/core/markdown.py +0 -0
  67. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/src/pptx_cli/core/runtime.py +0 -0
  68. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/src/pptx_cli/core/template.py +0 -0
  69. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/src/pptx_cli/core/validation.py +0 -0
  70. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/src/pptx_cli/core/versioning.py +0 -0
  71. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/src/pptx_cli/models/__init__.py +0 -0
  72. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/src/pptx_cli/models/envelope.py +0 -0
  73. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/src/pptx_cli/models/manifest.py +0 -0
  74. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/tests/conftest.py +0 -0
  75. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/tests/test_cli.py +0 -0
  76. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/tests/test_versioning.py +0 -0
  77. {pptx_cli-1.2.6 → pptx_cli-1.3.1}/uv.lock +0 -0
@@ -0,0 +1,7 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(uv run pyright src/pptx_cli/commands/schema.py tests/test_schema.py)"
5
+ ]
6
+ }
7
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pptx-cli
3
- Version: 1.2.6
3
+ Version: 1.3.1
4
4
  Summary: Template-bound PowerPoint generation for enterprise decks
5
5
  Author: Thomas Rohde
6
6
  License: MIT License
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "1.2.6"
5
+ __version__ = "1.3.1"
@@ -20,6 +20,7 @@ from pptx_cli.commands.inspect import (
20
20
  show_theme,
21
21
  )
22
22
  from pptx_cli.commands.manifest_ops import manifest_diff, manifest_schema
23
+ from pptx_cli.commands.schema import build_schema_document, copy_to_clipboard
23
24
  from pptx_cli.commands.validate import validate_command
24
25
  from pptx_cli.commands.wrapper import wrapper_generate
25
26
  from pptx_cli.core.composition import CompositionError
@@ -238,6 +239,27 @@ def guide(format: FormatOption = None) -> None:
238
239
  execute("guide.show", format, lambda: build_guide_document().model_dump(mode="json"))
239
240
 
240
241
 
242
+ @app.command("schema")
243
+ def schema_command(
244
+ template: Annotated[
245
+ Path | None,
246
+ typer.Option("--template", help="Path to a manifest package directory."),
247
+ ] = None,
248
+ no_copy: Annotated[
249
+ bool,
250
+ typer.Option("--no-copy", help="Skip copying output to the clipboard."),
251
+ ] = False,
252
+ ) -> None:
253
+ """Print the deck-spec YAML reference (for pasting into LLM prompts)."""
254
+ text = build_schema_document(template)
255
+ typer.echo(text)
256
+ if not no_copy:
257
+ if copy_to_clipboard(text):
258
+ typer.echo("(copied to clipboard)", err=True)
259
+ else:
260
+ typer.echo("(clipboard copy failed – install xclip or pipe manually)", err=True)
261
+
262
+
241
263
  @app.command("init")
242
264
  def init_command(
243
265
  template: Annotated[Path, typer.Argument(help="Path to the source .pptx template")],
@@ -17,6 +17,16 @@ def build_guide_document() -> GuideDocument:
17
17
  mutates=False,
18
18
  examples=["pptx guide --format json"],
19
19
  ),
20
+ GuideCommand(
21
+ id="schema.show",
22
+ summary="Print the deck-spec YAML reference for pasting into LLM prompts",
23
+ mutates=False,
24
+ examples=[
25
+ "pptx schema",
26
+ "pptx schema --template ./corp-template",
27
+ "pptx schema --no-copy",
28
+ ],
29
+ ),
20
30
  GuideCommand(
21
31
  id="template.init",
22
32
  summary="Initialize a manifest package from a source template",
@@ -0,0 +1,227 @@
1
+ """Build a human-readable YAML reference for deck specs.
2
+
3
+ Designed to be pasted into LLM prompts so the model knows how to
4
+ author valid ``pptx deck build`` input.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import subprocess
10
+ import sys
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ import yaml
15
+
16
+ from pptx_cli.core.manifest_store import load_effective_manifest
17
+ from pptx_cli.models.manifest import LayoutContract, ManifestDocument
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # Clipboard helper
21
+ # ---------------------------------------------------------------------------
22
+
23
+
24
+ def copy_to_clipboard(text: str) -> bool:
25
+ """Copy *text* to the system clipboard. Returns True on success."""
26
+ if sys.platform == "win32":
27
+ cmd = ["clip"]
28
+ elif sys.platform == "darwin":
29
+ cmd = ["pbcopy"]
30
+ else:
31
+ cmd = ["xclip", "-selection", "clipboard"]
32
+ try:
33
+ subprocess.run(
34
+ cmd,
35
+ input=text.encode("utf-8"),
36
+ check=True,
37
+ timeout=5,
38
+ )
39
+ except (FileNotFoundError, subprocess.SubprocessError):
40
+ return False
41
+ return True
42
+
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # Generic (template-free) schema
46
+ # ---------------------------------------------------------------------------
47
+
48
+ _GENERIC_SCHEMA = """\
49
+ <slide-schema>
50
+ Use this schema to draft presentation content. Layout assignment and
51
+ template binding happen later, so focus on the slides themselves.
52
+
53
+ metadata:
54
+ title: "<deck title>"
55
+ author: "<author name>"
56
+
57
+ slides:
58
+ - title: "Slide heading"
59
+ body: |
60
+ - Bullet one
61
+ - Bullet two
62
+ notes: |
63
+ Talk track for this slide.
64
+
65
+ - title: "Data slide"
66
+ body: |
67
+ Key observations from the quarter.
68
+ table:
69
+ kind: table
70
+ columns: [Col A, Col B]
71
+ rows:
72
+ - [val1, val2]
73
+ - [val3, val4]
74
+ notes: Additional context.
75
+
76
+ - title: "Trend slide"
77
+ body: |
78
+ Revenue grew 50% year-over-year.
79
+ chart:
80
+ kind: chart
81
+ chart_type: column_clustered
82
+ categories: [Q1, Q2, Q3]
83
+ series:
84
+ - name: Revenue
85
+ values: [12, 15, 18]
86
+
87
+ - title: "Visual slide"
88
+ image:
89
+ kind: image
90
+ path: path/to/image.png
91
+ image_fit: fit
92
+ </slide-schema>
93
+
94
+ <content-rules>
95
+ - Each slide MUST have a "title".
96
+ - Use "body" for the main text area. Markdown is auto-detected
97
+ (headings, bullets, **bold**, *italic*).
98
+ - Use "table", "chart", or "image" keys for structured content.
99
+ chart_type accepts: column_clustered, bar_clustered, line, pie.
100
+ image_fit accepts: "fit" (default) or "cover".
101
+ - "notes" is optional speaker-notes text (markdown ok).
102
+ - Do NOT include a "layout" key - layout is assigned later when
103
+ binding to a corporate template.
104
+ </content-rules>
105
+
106
+ <style-guide>
107
+ Follow these principles to produce decision-grade slides.
108
+
109
+ Structure
110
+ - Lead with the answer. Do not build up to the conclusion.
111
+ - Pyramid Principle: governing thought -> supporting arguments -> evidence.
112
+ - Arguments must be MECE (mutually exclusive, collectively exhaustive).
113
+ - One slide = one message. Two insights -> two slides.
114
+
115
+ Titles
116
+ - Use action titles, not topic labels. The title states the takeaway.
117
+ Bad: "Market overview"
118
+ Good: "Nordic retail banking margins will remain under pressure through 2027"
119
+ - Title storyline reads on its own - skim only titles and understand the
120
+ full argument.
121
+ - Formulas:
122
+ Insight: "[What happened] because [driver]"
123
+ Comparison: "[A] outperforms [B] on [criterion]"
124
+ Implication: "[Fact] puts [objective] at risk"
125
+ Recommendation: "[Org] should [action] to achieve [outcome]"
126
+
127
+ Executive summary slide
128
+ - Situation: what context everyone agrees on.
129
+ - Complication: what changed or created urgency.
130
+ - Answer: the recommended response.
131
+ - Support: 2-3 reasons the answer is correct.
132
+
133
+ Deck sequence (typical)
134
+ 1. Title page
135
+ 2. Executive summary (situation -> complication -> resolution)
136
+ 3. Context / problem framing (only enough to orient)
137
+ 4. Analysis body (current state -> root causes -> options -> recommendation)
138
+ 5. Recommendation (explicit, decision-ready)
139
+ 6. Implementation / roadmap
140
+ 7. Risks and mitigations
141
+ 8. Appendix (source data, benchmarks, methodology)
142
+
143
+ Body content
144
+ - Every claim backed by evidence. The body proves the headline.
145
+ - Quantify. Replace vague adjectives with measured claims.
146
+ - Keep text tight: verbs, short bullets, no filler.
147
+ - Move backup detail to appendix - main story stays focused.
148
+ </style-guide>
149
+ """
150
+
151
+
152
+ # ---------------------------------------------------------------------------
153
+ # Template-enriched schema
154
+ # ---------------------------------------------------------------------------
155
+
156
+
157
+ def _placeholder_summary(ph: Any) -> dict[str, Any]:
158
+ """Compact summary of a placeholder contract."""
159
+ entry: dict[str, Any] = {
160
+ "types": ph.supported_content_types,
161
+ }
162
+ if ph.required:
163
+ entry["required"] = True
164
+ if ph.guidance_text:
165
+ entry["guidance"] = ph.guidance_text
166
+ cap = ph.estimated_text_capacity
167
+ if cap is not None:
168
+ entry["max_lines"] = cap.max_lines
169
+ return entry
170
+
171
+
172
+ def _layout_section(layout: LayoutContract) -> dict[str, Any]:
173
+ """Build a dict describing one layout for the reference doc."""
174
+ section: dict[str, Any] = {}
175
+ if layout.description:
176
+ section["description"] = layout.description
177
+ if layout.aliases:
178
+ section["aliases"] = layout.aliases
179
+ placeholders: dict[str, Any] = {}
180
+ for ph in layout.placeholders:
181
+ placeholders[ph.logical_name] = _placeholder_summary(ph)
182
+ section["placeholders"] = placeholders
183
+ return section
184
+
185
+
186
+ def _example_slide(layout: LayoutContract) -> dict[str, Any]:
187
+ """Generate an example slide entry using real placeholder names."""
188
+ content: dict[str, str] = {}
189
+ for ph in layout.placeholders:
190
+ if "image" in ph.supported_content_types and "text" not in ph.supported_content_types:
191
+ content[ph.logical_name] = "{ kind: image, path: path/to/image.png }"
192
+ else:
193
+ content[ph.logical_name] = f"<{ph.logical_name} text>"
194
+ slide: dict[str, Any] = {"layout": layout.id, "content": content}
195
+ return slide
196
+
197
+
198
+ def _build_template_section(manifest: ManifestDocument) -> str:
199
+ """Return the template-specific portion of the reference doc."""
200
+ layouts_ref: dict[str, Any] = {}
201
+ examples: list[dict[str, Any]] = []
202
+
203
+ for layout in manifest.layouts:
204
+ layouts_ref[layout.id] = _layout_section(layout)
205
+ examples.append(_example_slide(layout))
206
+
207
+ doc: dict[str, Any] = {
208
+ "template": manifest.template.name,
209
+ "layouts": layouts_ref,
210
+ "example_slides": examples,
211
+ }
212
+ return yaml.safe_dump(doc, sort_keys=False, allow_unicode=True, width=120)
213
+
214
+
215
+ # ---------------------------------------------------------------------------
216
+ # Public API
217
+ # ---------------------------------------------------------------------------
218
+
219
+
220
+ def build_schema_document(template_dir: Path | None = None) -> str:
221
+ """Return the full reference document as a string."""
222
+ parts = [_GENERIC_SCHEMA]
223
+ if template_dir is not None:
224
+ manifest = load_effective_manifest(template_dir)
225
+ parts.append("\n# ── Template-specific layouts ──────────────────────────────────\n")
226
+ parts.append(_build_template_section(manifest))
227
+ return "\n".join(parts)
@@ -0,0 +1,80 @@
1
+ from __future__ import annotations
2
+
3
+ from unittest.mock import patch
4
+
5
+ from typer.testing import CliRunner
6
+
7
+ from pptx_cli.cli import app
8
+ from pptx_cli.commands.schema import build_schema_document
9
+
10
+ runner = CliRunner()
11
+
12
+
13
+ def test_generic_schema_contains_deck_structure() -> None:
14
+ text = build_schema_document()
15
+ assert "<slide-schema>" in text
16
+ assert "metadata:" in text
17
+ assert "slides:" in text
18
+ assert "title:" in text
19
+ assert "body:" in text
20
+ assert "notes:" in text
21
+
22
+
23
+ def test_generic_schema_documents_content_types() -> None:
24
+ text = build_schema_document()
25
+ assert "kind: image" in text
26
+ assert "kind: table" in text
27
+ assert "kind: chart" in text
28
+
29
+
30
+ def test_generic_schema_uses_xml_sections() -> None:
31
+ text = build_schema_document()
32
+ assert "<slide-schema>" in text
33
+ assert "</slide-schema>" in text
34
+ assert "<content-rules>" in text
35
+ assert "</content-rules>" in text
36
+ assert "<style-guide>" in text
37
+ assert "</style-guide>" in text
38
+
39
+
40
+ def test_generic_schema_omits_layout() -> None:
41
+ text = build_schema_document()
42
+ assert "Do NOT include" in text
43
+ assert "layout is assigned later" in text
44
+
45
+
46
+ def test_schema_command_outputs_text(tmp_path: str) -> None:
47
+ result = runner.invoke(app, ["schema", "--no-copy"])
48
+ assert result.exit_code == 0
49
+ assert "metadata:" in result.stdout
50
+ assert "slides:" in result.stdout
51
+
52
+
53
+ def test_schema_command_copies_to_clipboard() -> None:
54
+ with patch("pptx_cli.cli.copy_to_clipboard", return_value=True) as mock_copy:
55
+ result = runner.invoke(app, ["schema"])
56
+ assert result.exit_code == 0
57
+ mock_copy.assert_called_once()
58
+ assert "copied to clipboard" in result.stderr
59
+
60
+
61
+ def test_schema_command_no_copy_skips_clipboard() -> None:
62
+ with patch("pptx_cli.cli.copy_to_clipboard") as mock_copy:
63
+ result = runner.invoke(app, ["schema", "--no-copy"])
64
+ assert result.exit_code == 0
65
+ mock_copy.assert_not_called()
66
+
67
+
68
+ def test_schema_with_template(template_path: str, manifest_dir: str) -> None:
69
+ # First init a manifest so we can point --template at it
70
+ init_result = runner.invoke(
71
+ app,
72
+ ["init", str(template_path), "--out", str(manifest_dir), "--format", "json"],
73
+ )
74
+ assert init_result.exit_code == 0
75
+
76
+ result = runner.invoke(app, ["schema", "--template", str(manifest_dir), "--no-copy"])
77
+ assert result.exit_code == 0
78
+ assert "template:" in result.stdout
79
+ assert "layouts:" in result.stdout
80
+ assert "placeholders:" in result.stdout
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
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