planvortex 0.1.0__tar.gz → 0.3.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.3.0/CHANGELOG.md +249 -0
  2. {planvortex-0.1.0 → planvortex-0.3.0}/PKG-INFO +56 -2
  3. {planvortex-0.1.0 → planvortex-0.3.0}/README.md +55 -1
  4. {planvortex-0.1.0 → planvortex-0.3.0}/openapi/planvortex.openapi.json +437 -58
  5. {planvortex-0.1.0 → planvortex-0.3.0}/pyproject.toml +1 -1
  6. {planvortex-0.1.0 → planvortex-0.3.0}/scripts/route_coverage.py +1 -1
  7. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/_generated/models.py +330 -35
  8. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/_shapes.py +41 -8
  9. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/_version.py +2 -2
  10. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/resources/accounts.py +17 -5
  11. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/resources/ai_plans.py +76 -2
  12. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/resources/catalog.py +66 -5
  13. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/resources/comments.py +20 -1
  14. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/resources/messages.py +2 -2
  15. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/resources/products.py +2 -2
  16. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/resources/publications.py +8 -0
  17. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/resources_sync/accounts.py +17 -5
  18. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/resources_sync/ai_plans.py +42 -2
  19. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/resources_sync/catalog.py +65 -4
  20. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/resources_sync/comments.py +14 -0
  21. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/resources_sync/products.py +2 -2
  22. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/resources_sync/publications.py +8 -0
  23. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/types.py +176 -11
  24. {planvortex-0.1.0 → planvortex-0.3.0}/tests/live/conftest.py +1 -1
  25. {planvortex-0.1.0 → planvortex-0.3.0}/tests/live/test_catalog.py +38 -0
  26. {planvortex-0.1.0 → planvortex-0.3.0}/tests/live/test_connect_flow.py +17 -1
  27. planvortex-0.3.0/tests/test_ai_plans.py +224 -0
  28. {planvortex-0.1.0 → planvortex-0.3.0}/tests/test_catalog.py +108 -1
  29. {planvortex-0.1.0 → planvortex-0.3.0}/tests/test_core.py +4 -0
  30. {planvortex-0.1.0 → planvortex-0.3.0}/tests/test_models.py +41 -4
  31. {planvortex-0.1.0 → planvortex-0.3.0}/tests/test_route_coverage.py +2 -2
  32. {planvortex-0.1.0 → planvortex-0.3.0}/tests/test_shapes_parity.py +40 -28
  33. {planvortex-0.1.0 → planvortex-0.3.0}/tests/test_types.py +40 -7
  34. planvortex-0.1.0/CHANGELOG.md +0 -114
  35. planvortex-0.1.0/tests/test_ai_plans.py +0 -109
  36. {planvortex-0.1.0 → planvortex-0.3.0}/.gitignore +0 -0
  37. {planvortex-0.1.0 → planvortex-0.3.0}/LICENSE +0 -0
  38. {planvortex-0.1.0 → planvortex-0.3.0}/scripts/build_docs.py +0 -0
  39. {planvortex-0.1.0 → planvortex-0.3.0}/scripts/check_packaging.py +0 -0
  40. {planvortex-0.1.0 → planvortex-0.3.0}/scripts/generate_models.py +0 -0
  41. {planvortex-0.1.0 → planvortex-0.3.0}/scripts/generate_sync.py +0 -0
  42. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/__init__.py +0 -0
  43. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/_client.py +0 -0
  44. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/_client_sync.py +0 -0
  45. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/_core/__init__.py +0 -0
  46. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/_core/auth.py +0 -0
  47. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/_core/auth_sync.py +0 -0
  48. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/_core/errors.py +0 -0
  49. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/_core/files.py +0 -0
  50. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/_core/http.py +0 -0
  51. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/_core/http_sync.py +0 -0
  52. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/_core/pagination.py +0 -0
  53. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/_core/query.py +0 -0
  54. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/_core/transport.py +0 -0
  55. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/_generated/__init__.py +0 -0
  56. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/py.typed +0 -0
  57. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/resources/__init__.py +0 -0
  58. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/resources/apps.py +0 -0
  59. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/resources/base.py +0 -0
  60. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/resources/clients.py +0 -0
  61. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/resources/contacts.py +0 -0
  62. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/resources/dashboard.py +0 -0
  63. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/resources/integrations.py +0 -0
  64. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/resources/organizations.py +0 -0
  65. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/resources/uploads.py +0 -0
  66. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/resources_sync/__init__.py +0 -0
  67. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/resources_sync/apps.py +0 -0
  68. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/resources_sync/base.py +0 -0
  69. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/resources_sync/clients.py +0 -0
  70. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/resources_sync/contacts.py +0 -0
  71. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/resources_sync/dashboard.py +0 -0
  72. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/resources_sync/integrations.py +0 -0
  73. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/resources_sync/messages.py +0 -0
  74. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/resources_sync/organizations.py +0 -0
  75. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/resources_sync/uploads.py +0 -0
  76. {planvortex-0.1.0 → planvortex-0.3.0}/src/planvortex/webhooks.py +0 -0
  77. {planvortex-0.1.0 → planvortex-0.3.0}/tests/__init__.py +0 -0
  78. {planvortex-0.1.0 → planvortex-0.3.0}/tests/conftest.py +0 -0
  79. {planvortex-0.1.0 → planvortex-0.3.0}/tests/contrato.py +0 -0
  80. {planvortex-0.1.0 → planvortex-0.3.0}/tests/live/__init__.py +0 -0
  81. {planvortex-0.1.0 → planvortex-0.3.0}/tests/live/test_auth.py +0 -0
  82. {planvortex-0.1.0 → planvortex-0.3.0}/tests/live/test_errors.py +0 -0
  83. {planvortex-0.1.0 → planvortex-0.3.0}/tests/live/test_publish.py +0 -0
  84. {planvortex-0.1.0 → planvortex-0.3.0}/tests/live/test_read.py +0 -0
  85. {planvortex-0.1.0 → planvortex-0.3.0}/tests/test_accounts.py +0 -0
  86. {planvortex-0.1.0 → planvortex-0.3.0}/tests/test_apps.py +0 -0
  87. {planvortex-0.1.0 → planvortex-0.3.0}/tests/test_client.py +0 -0
  88. {planvortex-0.1.0 → planvortex-0.3.0}/tests/test_clients.py +0 -0
  89. {planvortex-0.1.0 → planvortex-0.3.0}/tests/test_comments.py +0 -0
  90. {planvortex-0.1.0 → planvortex-0.3.0}/tests/test_contacts.py +0 -0
  91. {planvortex-0.1.0 → planvortex-0.3.0}/tests/test_dashboard.py +0 -0
  92. {planvortex-0.1.0 → planvortex-0.3.0}/tests/test_example_comments.py +0 -0
  93. {planvortex-0.1.0 → planvortex-0.3.0}/tests/test_example_connect.py +0 -0
  94. {planvortex-0.1.0 → planvortex-0.3.0}/tests/test_example_publish.py +0 -0
  95. {planvortex-0.1.0 → planvortex-0.3.0}/tests/test_example_schedule.py +0 -0
  96. {planvortex-0.1.0 → planvortex-0.3.0}/tests/test_example_webhooks.py +0 -0
  97. {planvortex-0.1.0 → planvortex-0.3.0}/tests/test_files.py +0 -0
  98. {planvortex-0.1.0 → planvortex-0.3.0}/tests/test_generate_models.py +0 -0
  99. {planvortex-0.1.0 → planvortex-0.3.0}/tests/test_generate_sync.py +0 -0
  100. {planvortex-0.1.0 → planvortex-0.3.0}/tests/test_integrations.py +0 -0
  101. {planvortex-0.1.0 → planvortex-0.3.0}/tests/test_messages.py +0 -0
  102. {planvortex-0.1.0 → planvortex-0.3.0}/tests/test_openapi_freshness.py +0 -0
  103. {planvortex-0.1.0 → planvortex-0.3.0}/tests/test_organizations.py +0 -0
  104. {planvortex-0.1.0 → planvortex-0.3.0}/tests/test_packaging.py +0 -0
  105. {planvortex-0.1.0 → planvortex-0.3.0}/tests/test_pagination.py +0 -0
  106. {planvortex-0.1.0 → planvortex-0.3.0}/tests/test_products.py +0 -0
  107. {planvortex-0.1.0 → planvortex-0.3.0}/tests/test_publications.py +0 -0
  108. {planvortex-0.1.0 → planvortex-0.3.0}/tests/test_query_parity.py +0 -0
  109. {planvortex-0.1.0 → planvortex-0.3.0}/tests/test_readme.py +0 -0
  110. {planvortex-0.1.0 → planvortex-0.3.0}/tests/test_uploads.py +0 -0
  111. {planvortex-0.1.0 → planvortex-0.3.0}/tests/test_webhooks.py +0 -0
@@ -0,0 +1,249 @@
1
+ # Changelog
2
+
3
+ All notable changes to this package are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [0.3.0] - 2026-08-31
8
+
9
+ The AI planner stops being one thing. A plan can now be generated from **your own photos**, from an
10
+ **article**, from the **products of a connected catalogue** or as a **countdown towards a date**, and
11
+ the package publishes the catalogue that says what each of those accepts and what each of them
12
+ costs.
13
+
14
+ Everything here is additive: `template` and `source` are optional, and a plan created without them
15
+ is a `standard` one — exactly what every plan was before templates existed. Upgrading from `0.2.0`
16
+ needs no changes and `MIGRATION.md` gains no entry.
17
+
18
+ **The headline is a price, and it is worth saying out loud before your user creates the plan.**
19
+ Images are 94 % of what a plan costs. A template whose pictures come from the source generates none,
20
+ so the same week — 7 publications with a picture on each, one account — goes from **519 credits to
21
+ 48**. That is not a rounding: it is the difference between a feature a customer uses once a month
22
+ and one they use every week.
23
+
24
+ ### Added
25
+
26
+ - **`catalog.planner_templates()`**, in both clients and cached like the rest of the catalogue. It
27
+ answers what a plan can be generated FROM and what each source allows: `allows_shared`,
28
+ `allows_gallery`, `generates_images`, the `regenerate` matrix, `orchestration_cost` and
29
+ `orchestration_cost_per_source_item`, `max_source_items`, and the `source_fields` the source step
30
+ is made of. **Read it, do not copy it** — it is the one catalogue entry that carries prices, and a
31
+ table written by hand in your interface would show a cost the server no longer charges. It is also
32
+ the only route of the catalogue that **wraps its answer** (`{"templates": [...]}`), which is the
33
+ kind of thing that fails without an error: iterating the envelope gives you the string
34
+ `"templates"` and nothing complains.
35
+ - **`template` and `source` on `ai_plans.create()`**, with the five templates: `standard`,
36
+ `from_images`, `from_text`, `from_catalog` and `campaign`. A template is the **source** of the
37
+ content and not a different flow — publish days, language, tone, `shared` and the images stay
38
+ cross-cutting options, and each template declares which of them it accepts. Sending one it does
39
+ not accept is a 2106, not a silent ignore.
40
+ - **`PlannerTemplateName` and `PLANNER_TEMPLATES`**, plus `PlannerTemplate`,
41
+ `PlannerTemplateField`, `PlannerTemplateFieldType`, `AiPlanSourceInput`,
42
+ `AiPlanSourceImageInput`, `AiPlanSource`, `AiPlanSourceProduct` and `AiPlanNotice`. The
43
+ `Literal` is **closed**, unlike the Node library's open enumeration and for the same reason the
44
+ networks are closed here: a template you have not heard of should go red where you branch on it
45
+ rather than slip through. `AiPlan` now carries `template` — always, a plan created before
46
+ templates existed reads `standard` — and `source`.
47
+ - **Errors 2111 to 2117 arrive as `AiPlanError`**, with nothing registered to make them: they fall
48
+ in the 2100-2199 range on their own, and there is now a test that says so rather than assuming it.
49
+ They come back from `create()`, which is the part that surprises: **the source is validated when
50
+ the plan is created, not when it is generated.** The article is downloaded, the catalogue is read
51
+ live and the product pictures are copied inside that call, so a source that does not work fails
52
+ while your user is still in front of it. What gets stored is a snapshot — a `retry` three days
53
+ later does not depend on the article still being online or the product still being in the
54
+ catalogue.
55
+
56
+ ### Changed
57
+
58
+ - `ai_plans.regenerate` documents that **`"image"` depends on the plan's template**, not only on
59
+ whether the plan allowed images. The template that did not generate the picture cannot regenerate
60
+ it: read `regenerate["image"]` from the catalogue before you draw that button, because on
61
+ `from_images` it charges 70 credits to replace the user's own photo with an invented one.
62
+ - `ai_plans.get` documents **`warnings`**, which is a notice on a plan that generated **fine** and
63
+ not an error of the response — the place nobody looks. Today it carries one: **2117**, part of the
64
+ source did not fit in the plan week. A plan is weekly and the source does not extend it, so twelve
65
+ photos with six slots left publish six; `data` brings `{"source_items": ..., "capacity": ...}`,
66
+ and the slots are your publish days times your accounts, so it can be said before creating the
67
+ plan rather than after charging for it.
68
+ - Two traps are now written on the types that carry them, because neither has an error code and both
69
+ fail silently. **`source["text"]` wins over `source["url"]`** when both arrive — pasting the
70
+ article is what a user does when the download did not work, so re-downloading to ignore what they
71
+ wrote would take away their only way out. And **`event_date` is a calendar day, `YYYY-MM-DD`**,
72
+ never an ISO instant: it is read in the plan's timezone, and `2026-09-15T00:00:00Z` is the 14th in
73
+ the afternoon in New York — a whole day off in a countdown, for half of America.
74
+ - `AiPlanSourceProduct["price"]` says why the price is **never converted**: it comes exactly as the
75
+ network returned it, the same field is a number elsewhere in Meta's API, and dividing by 100 "just
76
+ in case" is precisely how a 10 EUR product gets advertised at 0,10 EUR.
77
+ - The route census is **113 of 113**. The new one is `GET /planner_templates`.
78
+
79
+ ## [0.2.0] - 2026-08-30
80
+
81
+ Telegram, the eleventh network, reaches the package. No endpoint was added and no signature moved:
82
+ what changes is what the types know — and unlike in the Node library, where an open enumeration
83
+ absorbs an unknown network in silence, here the closed `Literal`s go red until somebody looks. Which
84
+ is the point of them.
85
+
86
+ Upgrading from `0.1.0` needs no changes, and `MIGRATION.md` gains no entry.
87
+
88
+ ### Added
89
+
90
+ - **A third authorization shape: `TelegramBotAuthorization`, with its predicate
91
+ `is_telegram_bot_authorization`.** This is the part that is genuinely new contract rather than one
92
+ more name in a list. Telegram is the one entry that carries a `link` and still is not somewhere to
93
+ redirect: it opens a private chat with the PlanVortex bot, with no OAuth behind it — no consent
94
+ screen, no `code`, no `redirect_uri` — and the account is born minutes later, when the person
95
+ drops that bot into their channel. It is announced over the WebSocket and the `new_account`
96
+ webhook, never as the answer to a call of yours, and `accounts.connect` cannot finish it: for
97
+ `telegram` that endpoint always answers 700. The block carries `bot_username` (the bot's `@name`,
98
+ without the at sign) and `add_to_group_link`, the second step, which turns comments on by adding
99
+ the bot to the channel's linked discussion group. **Branch on `authorization["type"]`, never on
100
+ whether `link` is empty** — that test was already wrong for WhatsApp and it is worse here, because
101
+ a filled-in `link` makes it look like it worked.
102
+ - **`"telegram"` in `PublishableNetwork`** — the one hand-written union in `_shapes.py` — and
103
+ therefore in `PUBLISHABLE_NETWORKS`, `COMMENT_NETWORKS` and `SOCIAL_NETWORKS`, which derive from
104
+ their `Literal` and needed no edit. It is deliberately **not** in `CONTACT_CHANNELS`: Telegram has
105
+ no direct messages, and putting it there would promise a chat inbox that does not exist. That the
106
+ contact channels are now a proper subset of the networks, rather than all of them plus `email`, is
107
+ written down in the test that used to assert the equality.
108
+ - **`PublicationStats.reactions` and `reactions_by_emoji`**, which are Telegram's only publication
109
+ metric — and neither of them is asked for. `reactions` is the COMPLETE STATE and not an increment,
110
+ so it goes down when somebody takes theirs back.
111
+ - **`Publication.extra_data`**, for what one network has to remember about one publication and that
112
+ has no common field. Today only `telegram_message_ids` writes there: on Telegram an album is one
113
+ publication that is several messages, `external_identifier` holds the first and the rest live
114
+ here, because deleting the album means deleting all of them.
115
+
116
+ ### Changed
117
+
118
+ - The network-counting prose, which had quietly stopped being true and that no test watches:
119
+ `accounts.connect_links`, `SocialAuthorizationMethod`, `ConnectLink`, `PublishableNetwork` and
120
+ `is_publishable_network` all said "nine of the ten" or "one of ten and one of nine".
121
+ - Two warnings about this network are now on the methods that would otherwise surprise you, because
122
+ neither has an error code to announce it. **The comment inbox starts the day the channel was
123
+ connected** — the Bot API cannot read the past, so `comments.list` has nothing earlier and never
124
+ will, and `comments.thread`, which is a live read on every other network, is not live here at all.
125
+ And **there are no impressions and no reach**: engagement is computed over followers, the only
126
+ audience figure the Bot API publishes being the channel's member count.
127
+ - `catalog.social_limits` explains Telegram's **two numbers for the same field** — 4.096 characters
128
+ while the publication is text only and 1.024 the moment it carries an image or a video, because
129
+ then the text is a media caption. The counter switches when the file is attached, not when publish
130
+ is pressed.
131
+ - `publications.remove` documents Telegram's **48-hour window** (error 966, with `published_date`
132
+ and `max_hours` in `data`), which is a button to grey out rather than to offer and fail.
133
+ - `examples/connect.py` grows the third branch, and its `else` — the one that skips an authorization
134
+ method this version does not know — is no longer hypothetical: `telegram_bot` was exactly that.
135
+
136
+ ### Fixed
137
+
138
+ - The synchronous twin of `products.get` had been left behind by the previous release: its docstring
139
+ still said the Node library documented that endpoint as broken. Regenerating `resources_sync`
140
+ brings it back in line.
141
+
142
+ ## [0.1.0] - 2026-08-28
143
+
144
+ The first release with code in it. Every documented endpoint of the PlanVortex API has a method -
145
+ **112 of the 112 operations** - in two clients that cannot drift apart, the types come from the same
146
+ OpenAPI document the API publishes, the three test layers are in place (the third against a real
147
+ PlanVortex) and the reference is published. It is a `0.x` on purpose: the shape of the client is
148
+ settled and pinned by tests, but nobody outside has used it yet, so a break before `1.0.0` is
149
+ possible and would arrive written down in `MIGRATION.md`.
150
+
151
+ The `0.0.1` already on PyPI carries no code. It was published on 2026-08-26 to reserve the name and
152
+ to prove the release pipeline before there was anything to lose.
153
+
154
+ ### Added
155
+
156
+ - The **types**, generated from the OpenAPI document PlanVortex publishes and exposed with readable
157
+ names in `planvortex.types`: `Publication`, `Account`, `Upload`, `Comment`, `Message`, `Contact`
158
+ and the rest of the API's shapes, as `TypedDict`. The API's field names survive untranslated, so
159
+ it is `publication["_id"]` and not `publication.id`.
160
+ - The value lists, at the package root and at runtime: `SOCIAL_NETWORKS`, `PUBLICATION_STATES`,
161
+ `PUBLICATION_TYPES`, `COMMENT_NETWORKS`, `CONTACT_CHANNELS`, `MESSAGE_TYPES`,
162
+ `INTEGRATION_PROVIDERS` and `AI_PLAN_STATES`. Each one is derived from the same `Literal` the type
163
+ checker reads, so the two cannot disagree. **The lists grow**: a value you do not recognise is one
164
+ this release had not heard of, not an error.
165
+ - Helpers for the fields the API returns in two shapes: `account_id()`, `account()`,
166
+ `publication_id()`, `publication()`, `message_direction()`, `message_contact_id()`,
167
+ `message_contact()`, `message_files()` and `message_file_ids()`. `id_account` arrives resolved in
168
+ some operations and as a string in others, and these cover it without an `isinstance` in every
169
+ call site.
170
+ - The asynchronous and synchronous cores: HTTP with timeouts, backoff and retry rules, the
171
+ `client_credentials` token with its cache and its lock, the error hierarchy, and the query encoder
172
+ with the camelCase map.
173
+ - **The two clients**, `PlanVortex` and `AsyncPlanVortex`, with **fourteen resources covering the
174
+ whole public API**: `catalog`, `clients`, `organizations`, `accounts`, `uploads`, `publications`,
175
+ `comments`, `messages`, `contacts`, `products`, `integrations`, `ai_plans`, `dashboard` and
176
+ `apps` — **112 of the 112 operations** the specification documents, everything except the 19
177
+ routes of roles and invitations, which are out of scope. The synchronous client is generated from
178
+ the asynchronous one, so they cannot drift apart, and `scripts/route_coverage.py` walks the
179
+ OpenAPI bundle on every test run so that "no route is missing" keeps being true.
180
+ - Three things that **used to be documented as broken and work now**, fixed in the server on
181
+ 2026-08-24 and exposed here for the first time: filtering contacts by `social_network`, asking for
182
+ one product by `product_id` (`products.get()`, which also tolerates the single-object shape the
183
+ network answers with), and `in_response_external_id` in `messages.send()`, which is what makes
184
+ `comment_message` and `publication_message` reachable from the public API.
185
+ - `contacts.merge()`, which reads the contact before updating it so that its `extra_data` is not
186
+ wiped: it is the one field the server overwrites with whatever the body carries.
187
+ - `organizations.users()`, the one read of the roles section the library covers, because a custom
188
+ panel needs to paint the team.
189
+ - `Page(data, total)` for every listing, with the API's envelope already opened, plus `iterate()` /
190
+ `aiterate()` to chain pages — with a page cap so that a server ignoring the `offset` fails instead
191
+ of hanging the process.
192
+ - Uploading from a path, `bytes`, an open binary file or an iterable of `bytes`. A path is the one
193
+ that neither goes through memory nor asks you to close anything. A body that cannot be rewound
194
+ forbids the retry outright, because a second attempt over a spent stream uploads zero bytes.
195
+ - `publish_date` accepts a `datetime` and is serialized with its offset. A naive one raises instead
196
+ of guessing: assuming UTC publishes at the wrong time for whoever is in Madrid, and assuming the
197
+ process's zone does it for whoever is in Docker.
198
+ - `as_temporal_token()`, for the account-connection flow: an app cannot connect an account, so it
199
+ issues a temporal token and hands it to the person who can.
200
+ - `is_publishable_network()`, a `TypeGuard` narrowing a network down to the **nine** that accept
201
+ publications, and `PUBLISHABLE_NETWORKS` beside it. An account's `social_network` is one of ten
202
+ and a publication's is one of nine — `google_business` receives reviews, not posts — so handing
203
+ one to the other was a type error with no way out that did not throw away a real guarantee.
204
+ - **Five runnable examples**, each with a test of its own: `publish.py` (credentials to a scheduled
205
+ publication), `schedule.py` (the calendar, moving a post, rescuing a failed one), `comments.py`
206
+ (inbox, actions matrix, live thread and replying — read-only unless `PLANVORTEX_ALLOW_REPLY=1`),
207
+ `webhooks.py` (a receiver with no dependencies, and a `--self-test` that signs a delivery to
208
+ itself) and `connect.py` (the connection flow, and what the browser has to do).
209
+ - **The published reference**, generated with `pdoc` from the docstrings and deployed to GitHub
210
+ Pages on every push to `main`: <https://taliasoftworks.github.io/PlanVortexPython/>.
211
+ - **`planvortex.webhooks`**, in a module of its own because whoever receives deliveries is rarely
212
+ the process that publishes: `verify_webhook_signature()` over the **raw** body with a constant-time
213
+ comparison, `parse_webhook_body()` (the body is an **array** of changes), and
214
+ `handle_webhook_request()`, which does both and finds the signature header whatever your framework
215
+ calls it. No framework middleware is shipped: the README carries the Flask, FastAPI and Django
216
+ recipes, each with the one line that gives you the raw body.
217
+ - The webhook event types, split by `field` so they can be narrowed — `AccountStateChange`,
218
+ `MessageChange`, `CommentChange` and `IntegrationErrorChange` — with the predicates
219
+ `is_comment_change()` and friends, and `UnknownWebhookChange` for the events a future server sends
220
+ and this release has never heard of. `WebhookSignatureError` and `WebhookBodyError` are
221
+ `PlanVortexError` of family `webhook`.
222
+
223
+ - **The live test suite** (`tests/live/`), which talks to a real PlanVortex and is the only layer
224
+ that can notice the *server* changing under the library: a renamed list envelope, an error code
225
+ that moved, a tenth social network. It ships in the sdist like the rest of the tests. It is not in
226
+ `uv run pytest` — it carries the `live` marker and is asked for with `uv run pytest -m live` — it
227
+ skips itself whole without `.env.live`, and everything that writes is behind `LIVE_ALLOW_PUBLISH`,
228
+ `LIVE_ALLOW_SOCIAL_PUBLISH` and `LIVE_ALLOW_PRODUCTION`. See `.env.live.example`.
229
+
230
+ ### Changed
231
+
232
+ - `PublicationInput["publish_date"]` now **accepts a `datetime` in the type**, not only at runtime.
233
+ The generated type said `str`, so the usage the library documents did not type-check. It is the
234
+ one shape written by hand on top of the generated one, and it has a parity test against the
235
+ specification that allows exactly that one difference.
236
+ - `examples/` is inside mypy's scope. It was the only code in the repository nobody checked, and
237
+ the two failures it turned up were holes in the library, not in the examples.
238
+ - `typing-extensions` is now a dependency on **Python 3.10 only** (`python_version < "3.11"`), where
239
+ `NotRequired` is not yet in `typing`. Nothing is installed from 3.11 upwards.
240
+ - The coverage floor is now enforced (`fail_under = 97`), measured over layers 1 and 2 — the ones
241
+ CI runs.
242
+
243
+ ## [0.0.1] - 2026-08-26
244
+
245
+ Placeholder release. It reserves the `planvortex` name on PyPI and proves the trusted-publishing
246
+ pipeline end to end before there is anything to lose. It contains no client.
247
+
248
+ [0.1.0]: https://github.com/taliasoftworks/PlanVortexPython/compare/v0.0.1...v0.1.0
249
+ [0.0.1]: https://github.com/taliasoftworks/PlanVortexPython/releases/tag/v0.0.1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: planvortex
3
- Version: 0.1.0
3
+ Version: 0.3.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