planvortex 0.2.0__tar.gz → 0.4.0__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 (111) hide show
  1. {planvortex-0.2.0 → planvortex-0.4.0}/CHANGELOG.md +108 -0
  2. {planvortex-0.2.0 → planvortex-0.4.0}/PKG-INFO +56 -2
  3. {planvortex-0.2.0 → planvortex-0.4.0}/README.md +55 -1
  4. {planvortex-0.2.0 → planvortex-0.4.0}/openapi/planvortex.openapi.json +493 -28
  5. {planvortex-0.2.0 → planvortex-0.4.0}/pyproject.toml +1 -1
  6. {planvortex-0.2.0 → planvortex-0.4.0}/scripts/route_coverage.py +1 -1
  7. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/_generated/models.py +275 -20
  8. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/_version.py +2 -2
  9. planvortex-0.4.0/src/planvortex/resources/ai_plans.py +373 -0
  10. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/resources/catalog.py +54 -3
  11. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/resources_sync/ai_plans.py +119 -9
  12. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/resources_sync/catalog.py +53 -2
  13. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/types.py +129 -1
  14. {planvortex-0.2.0 → planvortex-0.4.0}/tests/live/test_catalog.py +38 -0
  15. planvortex-0.4.0/tests/test_ai_plans.py +268 -0
  16. {planvortex-0.2.0 → planvortex-0.4.0}/tests/test_catalog.py +108 -1
  17. {planvortex-0.2.0 → planvortex-0.4.0}/tests/test_core.py +4 -0
  18. {planvortex-0.2.0 → planvortex-0.4.0}/tests/test_models.py +27 -2
  19. {planvortex-0.2.0 → planvortex-0.4.0}/tests/test_route_coverage.py +6 -2
  20. planvortex-0.2.0/src/planvortex/resources/ai_plans.py +0 -223
  21. planvortex-0.2.0/tests/test_ai_plans.py +0 -109
  22. {planvortex-0.2.0 → planvortex-0.4.0}/.gitignore +0 -0
  23. {planvortex-0.2.0 → planvortex-0.4.0}/LICENSE +0 -0
  24. {planvortex-0.2.0 → planvortex-0.4.0}/scripts/build_docs.py +0 -0
  25. {planvortex-0.2.0 → planvortex-0.4.0}/scripts/check_packaging.py +0 -0
  26. {planvortex-0.2.0 → planvortex-0.4.0}/scripts/generate_models.py +0 -0
  27. {planvortex-0.2.0 → planvortex-0.4.0}/scripts/generate_sync.py +0 -0
  28. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/__init__.py +0 -0
  29. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/_client.py +0 -0
  30. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/_client_sync.py +0 -0
  31. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/_core/__init__.py +0 -0
  32. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/_core/auth.py +0 -0
  33. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/_core/auth_sync.py +0 -0
  34. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/_core/errors.py +0 -0
  35. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/_core/files.py +0 -0
  36. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/_core/http.py +0 -0
  37. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/_core/http_sync.py +0 -0
  38. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/_core/pagination.py +0 -0
  39. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/_core/query.py +0 -0
  40. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/_core/transport.py +0 -0
  41. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/_generated/__init__.py +0 -0
  42. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/_shapes.py +0 -0
  43. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/py.typed +0 -0
  44. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/resources/__init__.py +0 -0
  45. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/resources/accounts.py +0 -0
  46. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/resources/apps.py +0 -0
  47. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/resources/base.py +0 -0
  48. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/resources/clients.py +0 -0
  49. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/resources/comments.py +0 -0
  50. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/resources/contacts.py +0 -0
  51. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/resources/dashboard.py +0 -0
  52. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/resources/integrations.py +0 -0
  53. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/resources/messages.py +0 -0
  54. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/resources/organizations.py +0 -0
  55. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/resources/products.py +0 -0
  56. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/resources/publications.py +0 -0
  57. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/resources/uploads.py +0 -0
  58. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/resources_sync/__init__.py +0 -0
  59. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/resources_sync/accounts.py +0 -0
  60. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/resources_sync/apps.py +0 -0
  61. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/resources_sync/base.py +0 -0
  62. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/resources_sync/clients.py +0 -0
  63. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/resources_sync/comments.py +0 -0
  64. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/resources_sync/contacts.py +0 -0
  65. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/resources_sync/dashboard.py +0 -0
  66. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/resources_sync/integrations.py +0 -0
  67. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/resources_sync/messages.py +0 -0
  68. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/resources_sync/organizations.py +0 -0
  69. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/resources_sync/products.py +0 -0
  70. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/resources_sync/publications.py +0 -0
  71. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/resources_sync/uploads.py +0 -0
  72. {planvortex-0.2.0 → planvortex-0.4.0}/src/planvortex/webhooks.py +0 -0
  73. {planvortex-0.2.0 → planvortex-0.4.0}/tests/__init__.py +0 -0
  74. {planvortex-0.2.0 → planvortex-0.4.0}/tests/conftest.py +0 -0
  75. {planvortex-0.2.0 → planvortex-0.4.0}/tests/contrato.py +0 -0
  76. {planvortex-0.2.0 → planvortex-0.4.0}/tests/live/__init__.py +0 -0
  77. {planvortex-0.2.0 → planvortex-0.4.0}/tests/live/conftest.py +0 -0
  78. {planvortex-0.2.0 → planvortex-0.4.0}/tests/live/test_auth.py +0 -0
  79. {planvortex-0.2.0 → planvortex-0.4.0}/tests/live/test_connect_flow.py +0 -0
  80. {planvortex-0.2.0 → planvortex-0.4.0}/tests/live/test_errors.py +0 -0
  81. {planvortex-0.2.0 → planvortex-0.4.0}/tests/live/test_publish.py +0 -0
  82. {planvortex-0.2.0 → planvortex-0.4.0}/tests/live/test_read.py +0 -0
  83. {planvortex-0.2.0 → planvortex-0.4.0}/tests/test_accounts.py +0 -0
  84. {planvortex-0.2.0 → planvortex-0.4.0}/tests/test_apps.py +0 -0
  85. {planvortex-0.2.0 → planvortex-0.4.0}/tests/test_client.py +0 -0
  86. {planvortex-0.2.0 → planvortex-0.4.0}/tests/test_clients.py +0 -0
  87. {planvortex-0.2.0 → planvortex-0.4.0}/tests/test_comments.py +0 -0
  88. {planvortex-0.2.0 → planvortex-0.4.0}/tests/test_contacts.py +0 -0
  89. {planvortex-0.2.0 → planvortex-0.4.0}/tests/test_dashboard.py +0 -0
  90. {planvortex-0.2.0 → planvortex-0.4.0}/tests/test_example_comments.py +0 -0
  91. {planvortex-0.2.0 → planvortex-0.4.0}/tests/test_example_connect.py +0 -0
  92. {planvortex-0.2.0 → planvortex-0.4.0}/tests/test_example_publish.py +0 -0
  93. {planvortex-0.2.0 → planvortex-0.4.0}/tests/test_example_schedule.py +0 -0
  94. {planvortex-0.2.0 → planvortex-0.4.0}/tests/test_example_webhooks.py +0 -0
  95. {planvortex-0.2.0 → planvortex-0.4.0}/tests/test_files.py +0 -0
  96. {planvortex-0.2.0 → planvortex-0.4.0}/tests/test_generate_models.py +0 -0
  97. {planvortex-0.2.0 → planvortex-0.4.0}/tests/test_generate_sync.py +0 -0
  98. {planvortex-0.2.0 → planvortex-0.4.0}/tests/test_integrations.py +0 -0
  99. {planvortex-0.2.0 → planvortex-0.4.0}/tests/test_messages.py +0 -0
  100. {planvortex-0.2.0 → planvortex-0.4.0}/tests/test_openapi_freshness.py +0 -0
  101. {planvortex-0.2.0 → planvortex-0.4.0}/tests/test_organizations.py +0 -0
  102. {planvortex-0.2.0 → planvortex-0.4.0}/tests/test_packaging.py +0 -0
  103. {planvortex-0.2.0 → planvortex-0.4.0}/tests/test_pagination.py +0 -0
  104. {planvortex-0.2.0 → planvortex-0.4.0}/tests/test_products.py +0 -0
  105. {planvortex-0.2.0 → planvortex-0.4.0}/tests/test_publications.py +0 -0
  106. {planvortex-0.2.0 → planvortex-0.4.0}/tests/test_query_parity.py +0 -0
  107. {planvortex-0.2.0 → planvortex-0.4.0}/tests/test_readme.py +0 -0
  108. {planvortex-0.2.0 → planvortex-0.4.0}/tests/test_shapes_parity.py +0 -0
  109. {planvortex-0.2.0 → planvortex-0.4.0}/tests/test_types.py +0 -0
  110. {planvortex-0.2.0 → planvortex-0.4.0}/tests/test_uploads.py +0 -0
  111. {planvortex-0.2.0 → planvortex-0.4.0}/tests/test_webhooks.py +0 -0
@@ -4,6 +4,114 @@ All notable changes to this package are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
5
5
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.4.0] - 2026-08-31
8
+
9
+ An AI plan can now be **archived**, and deleting one finally means what people thought it meant.
10
+
11
+ The two look alike on a screen and have nothing in common underneath: archiving takes the plan out
12
+ of the listing and touches nothing else, while deleting takes the publications that had not gone out
13
+ with it. Almost every time someone reaches for "remove" they wanted the first one.
14
+
15
+ Upgrading from `0.3.0` needs no changes and `MIGRATION.md` gains no entry — everything added here is
16
+ additive. **Read the note on `remove()` anyway**: what it deletes changed in the server, so it has
17
+ been happening to your `0.3.0` calls for a while and the docs that shipped with them say otherwise.
18
+
19
+ ### Added
20
+
21
+ - **`ai_plans.archive()` and `ai_plans.unarchive()`**, in both clients. Archiving is **visibility
22
+ only**: the plan leaves the listing and moves to the archived one, no publication is touched —
23
+ anything scheduled keeps publishing — no credits come back, nothing is cancelled. It therefore
24
+ works in **any state**, `generating` included, and it is undone. On the object it is
25
+ `archived_date`: a field, **not** a value of `state`, so reading the state to find out whether a
26
+ plan is archived will never work. Absent means active, which is how every plan created before the
27
+ field existed comes back.
28
+ - **`archived` on `ai_plans.list()` and `ai_plans.aiterate()`/`iterate()`.** `True` returns the
29
+ archived plans instead of the active ones, never both at once. A `False` is not sent: the server
30
+ switches the filter on with the literal `"true"`, so it would be noise — and it would suggest a
31
+ third mode with both cupboards open that does not exist.
32
+
33
+ ### Changed
34
+
35
+ - **`ai_plans.remove()` deletes the plan's publications that have not gone out yet**, not only the
36
+ drafts: if the plan had already been validated, whatever was still scheduled goes too. This is a
37
+ change **in the server**, so it reached your existing calls the day it shipped — before it, a
38
+ validated plan you deleted kept publishing by itself the following week. Two states are
39
+ deliberately left alone: the already published one, because deleting it here would not take it off
40
+ the network and would only lose its history, and the one being published at that very moment.
41
+ If you wanted the plan out of the way without losing anything, that is `archive()`.
42
+
43
+ ## [0.3.0] - 2026-08-31
44
+
45
+ The AI planner stops being one thing. A plan can now be generated from **your own photos**, from an
46
+ **article**, from the **products of a connected catalogue** or as a **countdown towards a date**, and
47
+ the package publishes the catalogue that says what each of those accepts and what each of them
48
+ costs.
49
+
50
+ Everything here is additive: `template` and `source` are optional, and a plan created without them
51
+ is a `standard` one — exactly what every plan was before templates existed. Upgrading from `0.2.0`
52
+ needs no changes and `MIGRATION.md` gains no entry.
53
+
54
+ **The headline is a price, and it is worth saying out loud before your user creates the plan.**
55
+ Images are 94 % of what a plan costs. A template whose pictures come from the source generates none,
56
+ so the same week — 7 publications with a picture on each, one account — goes from **519 credits to
57
+ 48**. That is not a rounding: it is the difference between a feature a customer uses once a month
58
+ and one they use every week.
59
+
60
+ ### Added
61
+
62
+ - **`catalog.planner_templates()`**, in both clients and cached like the rest of the catalogue. It
63
+ answers what a plan can be generated FROM and what each source allows: `allows_shared`,
64
+ `allows_gallery`, `generates_images`, the `regenerate` matrix, `orchestration_cost` and
65
+ `orchestration_cost_per_source_item`, `max_source_items`, and the `source_fields` the source step
66
+ is made of. **Read it, do not copy it** — it is the one catalogue entry that carries prices, and a
67
+ table written by hand in your interface would show a cost the server no longer charges. It is also
68
+ the only route of the catalogue that **wraps its answer** (`{"templates": [...]}`), which is the
69
+ kind of thing that fails without an error: iterating the envelope gives you the string
70
+ `"templates"` and nothing complains.
71
+ - **`template` and `source` on `ai_plans.create()`**, with the five templates: `standard`,
72
+ `from_images`, `from_text`, `from_catalog` and `campaign`. A template is the **source** of the
73
+ content and not a different flow — publish days, language, tone, `shared` and the images stay
74
+ cross-cutting options, and each template declares which of them it accepts. Sending one it does
75
+ not accept is a 2106, not a silent ignore.
76
+ - **`PlannerTemplateName` and `PLANNER_TEMPLATES`**, plus `PlannerTemplate`,
77
+ `PlannerTemplateField`, `PlannerTemplateFieldType`, `AiPlanSourceInput`,
78
+ `AiPlanSourceImageInput`, `AiPlanSource`, `AiPlanSourceProduct` and `AiPlanNotice`. The
79
+ `Literal` is **closed**, unlike the Node library's open enumeration and for the same reason the
80
+ networks are closed here: a template you have not heard of should go red where you branch on it
81
+ rather than slip through. `AiPlan` now carries `template` — always, a plan created before
82
+ templates existed reads `standard` — and `source`.
83
+ - **Errors 2111 to 2117 arrive as `AiPlanError`**, with nothing registered to make them: they fall
84
+ in the 2100-2199 range on their own, and there is now a test that says so rather than assuming it.
85
+ They come back from `create()`, which is the part that surprises: **the source is validated when
86
+ the plan is created, not when it is generated.** The article is downloaded, the catalogue is read
87
+ live and the product pictures are copied inside that call, so a source that does not work fails
88
+ while your user is still in front of it. What gets stored is a snapshot — a `retry` three days
89
+ later does not depend on the article still being online or the product still being in the
90
+ catalogue.
91
+
92
+ ### Changed
93
+
94
+ - `ai_plans.regenerate` documents that **`"image"` depends on the plan's template**, not only on
95
+ whether the plan allowed images. The template that did not generate the picture cannot regenerate
96
+ it: read `regenerate["image"]` from the catalogue before you draw that button, because on
97
+ `from_images` it charges 70 credits to replace the user's own photo with an invented one.
98
+ - `ai_plans.get` documents **`warnings`**, which is a notice on a plan that generated **fine** and
99
+ not an error of the response — the place nobody looks. Today it carries one: **2117**, part of the
100
+ source did not fit in the plan week. A plan is weekly and the source does not extend it, so twelve
101
+ photos with six slots left publish six; `data` brings `{"source_items": ..., "capacity": ...}`,
102
+ and the slots are your publish days times your accounts, so it can be said before creating the
103
+ plan rather than after charging for it.
104
+ - Two traps are now written on the types that carry them, because neither has an error code and both
105
+ fail silently. **`source["text"]` wins over `source["url"]`** when both arrive — pasting the
106
+ article is what a user does when the download did not work, so re-downloading to ignore what they
107
+ wrote would take away their only way out. And **`event_date` is a calendar day, `YYYY-MM-DD`**,
108
+ never an ISO instant: it is read in the plan's timezone, and `2026-09-15T00:00:00Z` is the 14th in
109
+ the afternoon in New York — a whole day off in a countdown, for half of America.
110
+ - `AiPlanSourceProduct["price"]` says why the price is **never converted**: it comes exactly as the
111
+ network returned it, the same field is a number elsewhere in Meta's API, and dividing by 100 "just
112
+ in case" is precisely how a 10 EUR product gets advertised at 0,10 EUR.
113
+ - The route census is **113 of 113**. The new one is `GET /planner_templates`.
114
+
7
115
  ## [0.2.0] - 2026-08-30
8
116
 
9
117
  Telegram, the eleventh network, reaches the package. No endpoint was added and no signature moved:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: planvortex
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: Official Python client for the PlanVortex API
5
5
  Project-URL: Homepage, https://planvortex.com/developers
6
6
  Project-URL: Documentation, https://taliasoftworks.github.io/PlanVortexPython/
@@ -180,6 +180,60 @@ if (pv.comments.actions_for("linkedin") or {}).get("hide"):
180
180
  ...
181
181
  ```
182
182
 
183
+ ## AI plans, and what they are generated from
184
+
185
+ A plan is a **week** of drafts written by a model: you queue it, a job generates it, and what comes
186
+ out are ordinary publications in `draft` that you edit and validate. `template` says what the
187
+ content is generated FROM, and it is the only thing that changes between one plan and another —
188
+ publish days, language, tone and images stay cross-cutting options, and each template declares which
189
+ of them it accepts.
190
+
191
+ ```python
192
+ # The list, the prices and the fields of the source step. Cached, like the rest of the catalogue.
193
+ templates = pv.catalog.planner_templates()
194
+
195
+ # A week written from the customer's own photos, in the order that tells the story.
196
+ queued = pv.ai_plans.create(
197
+ client_id,
198
+ org_id,
199
+ {
200
+ "prompt": "Our autumn menu",
201
+ "accounts": [account_id],
202
+ "template": "from_images",
203
+ "source": {
204
+ "images": [
205
+ {"id_upload": first, "description": "Dough resting on the bench"},
206
+ {"id_upload": second, "description": "The loaf coming out of the oven"},
207
+ ]
208
+ },
209
+ },
210
+ )
211
+ queued["estimate"]["images_target"] # 0 — the pictures come from the source
212
+ ```
213
+
214
+ Four things worth knowing before you build the screen:
215
+
216
+ - **The template that does not generate images does not spend image credits, and images are 94 % of
217
+ a plan.** The same week — 7 publications with a picture on each, one account — costs 519 credits
218
+ as `standard` and **48** as `from_images`. Say it before the plan is created, not after it is
219
+ charged.
220
+ - **`regenerate(..., "image")` is per template**, not just per plan: the one that did not generate
221
+ the picture cannot regenerate it. Read `regenerate["image"]` from the catalogue before you draw
222
+ the button — on `from_images` it would charge 70 credits to replace the user's own photo with an
223
+ invented one.
224
+ - **The source is validated when the plan is CREATED**, not when it is generated: the article is
225
+ downloaded, the catalogue is read live and the product pictures are copied inside that call. So a
226
+ broken source fails while your user is still there — errors 2111 to 2116, all of them
227
+ `AiPlanError` — and what gets stored is a snapshot: a `retry` three days later does not depend on
228
+ the article still being online.
229
+ - **A plan is weekly and the source does not extend it.** Twelve photos with six slots left publish
230
+ six, and the plan carries warning **2117** in `ai_plan["warnings"]` — a notice on a plan that
231
+ generated fine, not an error. The slots are your publish days times your accounts, so you can say
232
+ it in advance.
233
+
234
+ Do not hardcode the list, the costs or the field limits: `GET /planner_templates` publishes them
235
+ because the server is what charges them.
236
+
183
237
  ## Errors
184
238
 
185
239
  Errors are classified by `code`, **never by the HTTP status** — every domain error in this API
@@ -294,7 +348,7 @@ you missed.
294
348
  · `pv.comments` · `pv.messages` · `pv.contacts` · `pv.products` · `pv.integrations` · `pv.ai_plans`
295
349
  · `pv.dashboard` · `pv.apps`
296
350
 
297
- That is **112 of the 112 operations** the specification documents — everything except the 19 routes
351
+ That is **113 of the 113 operations** the specification documents — everything except the 19 routes
298
352
  of roles and invitations, which are out of scope. A script walks the OpenAPI bundle on every test
299
353
  run and fails if a route is left without a method, so the sentence above stays true.
300
354
 
@@ -150,6 +150,60 @@ if (pv.comments.actions_for("linkedin") or {}).get("hide"):
150
150
  ...
151
151
  ```
152
152
 
153
+ ## AI plans, and what they are generated from
154
+
155
+ A plan is a **week** of drafts written by a model: you queue it, a job generates it, and what comes
156
+ out are ordinary publications in `draft` that you edit and validate. `template` says what the
157
+ content is generated FROM, and it is the only thing that changes between one plan and another —
158
+ publish days, language, tone and images stay cross-cutting options, and each template declares which
159
+ of them it accepts.
160
+
161
+ ```python
162
+ # The list, the prices and the fields of the source step. Cached, like the rest of the catalogue.
163
+ templates = pv.catalog.planner_templates()
164
+
165
+ # A week written from the customer's own photos, in the order that tells the story.
166
+ queued = pv.ai_plans.create(
167
+ client_id,
168
+ org_id,
169
+ {
170
+ "prompt": "Our autumn menu",
171
+ "accounts": [account_id],
172
+ "template": "from_images",
173
+ "source": {
174
+ "images": [
175
+ {"id_upload": first, "description": "Dough resting on the bench"},
176
+ {"id_upload": second, "description": "The loaf coming out of the oven"},
177
+ ]
178
+ },
179
+ },
180
+ )
181
+ queued["estimate"]["images_target"] # 0 — the pictures come from the source
182
+ ```
183
+
184
+ Four things worth knowing before you build the screen:
185
+
186
+ - **The template that does not generate images does not spend image credits, and images are 94 % of
187
+ a plan.** The same week — 7 publications with a picture on each, one account — costs 519 credits
188
+ as `standard` and **48** as `from_images`. Say it before the plan is created, not after it is
189
+ charged.
190
+ - **`regenerate(..., "image")` is per template**, not just per plan: the one that did not generate
191
+ the picture cannot regenerate it. Read `regenerate["image"]` from the catalogue before you draw
192
+ the button — on `from_images` it would charge 70 credits to replace the user's own photo with an
193
+ invented one.
194
+ - **The source is validated when the plan is CREATED**, not when it is generated: the article is
195
+ downloaded, the catalogue is read live and the product pictures are copied inside that call. So a
196
+ broken source fails while your user is still there — errors 2111 to 2116, all of them
197
+ `AiPlanError` — and what gets stored is a snapshot: a `retry` three days later does not depend on
198
+ the article still being online.
199
+ - **A plan is weekly and the source does not extend it.** Twelve photos with six slots left publish
200
+ six, and the plan carries warning **2117** in `ai_plan["warnings"]` — a notice on a plan that
201
+ generated fine, not an error. The slots are your publish days times your accounts, so you can say
202
+ it in advance.
203
+
204
+ Do not hardcode the list, the costs or the field limits: `GET /planner_templates` publishes them
205
+ because the server is what charges them.
206
+
153
207
  ## Errors
154
208
 
155
209
  Errors are classified by `code`, **never by the HTTP status** — every domain error in this API
@@ -264,7 +318,7 @@ you missed.
264
318
  · `pv.comments` · `pv.messages` · `pv.contacts` · `pv.products` · `pv.integrations` · `pv.ai_plans`
265
319
  · `pv.dashboard` · `pv.apps`
266
320
 
267
- That is **112 of the 112 operations** the specification documents — everything except the 19 routes
321
+ That is **113 of the 113 operations** the specification documents — everything except the 19 routes
268
322
  of roles and invitations, which are out of scope. A script walks the OpenAPI bundle on every test
269
323
  run and fails if a route is left without a method, so the sentence above stays true.
270
324