planvortex 0.4.0__tar.gz → 0.6.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 (109) hide show
  1. {planvortex-0.4.0 → planvortex-0.6.0}/CHANGELOG.md +60 -0
  2. {planvortex-0.4.0 → planvortex-0.6.0}/PKG-INFO +4 -4
  3. {planvortex-0.4.0 → planvortex-0.6.0}/README.md +3 -3
  4. {planvortex-0.4.0 → planvortex-0.6.0}/openapi/planvortex.openapi.json +39 -25
  5. {planvortex-0.4.0 → planvortex-0.6.0}/pyproject.toml +1 -1
  6. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/_core/errors.py +15 -6
  7. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/_generated/models.py +27 -11
  8. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/_shapes.py +6 -4
  9. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/_version.py +1 -1
  10. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/resources/accounts.py +1 -1
  11. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/resources_sync/accounts.py +1 -1
  12. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/types.py +14 -1
  13. {planvortex-0.4.0 → planvortex-0.6.0}/tests/test_core.py +12 -0
  14. {planvortex-0.4.0 → planvortex-0.6.0}/tests/test_example_connect.py +1 -1
  15. {planvortex-0.4.0 → planvortex-0.6.0}/tests/test_example_publish.py +1 -1
  16. {planvortex-0.4.0 → planvortex-0.6.0}/tests/test_organizations.py +2 -2
  17. {planvortex-0.4.0 → planvortex-0.6.0}/.gitignore +0 -0
  18. {planvortex-0.4.0 → planvortex-0.6.0}/LICENSE +0 -0
  19. {planvortex-0.4.0 → planvortex-0.6.0}/scripts/build_docs.py +0 -0
  20. {planvortex-0.4.0 → planvortex-0.6.0}/scripts/check_packaging.py +0 -0
  21. {planvortex-0.4.0 → planvortex-0.6.0}/scripts/generate_models.py +0 -0
  22. {planvortex-0.4.0 → planvortex-0.6.0}/scripts/generate_sync.py +0 -0
  23. {planvortex-0.4.0 → planvortex-0.6.0}/scripts/route_coverage.py +0 -0
  24. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/__init__.py +0 -0
  25. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/_client.py +0 -0
  26. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/_client_sync.py +0 -0
  27. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/_core/__init__.py +0 -0
  28. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/_core/auth.py +0 -0
  29. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/_core/auth_sync.py +0 -0
  30. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/_core/files.py +0 -0
  31. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/_core/http.py +0 -0
  32. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/_core/http_sync.py +0 -0
  33. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/_core/pagination.py +0 -0
  34. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/_core/query.py +0 -0
  35. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/_core/transport.py +0 -0
  36. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/_generated/__init__.py +0 -0
  37. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/py.typed +0 -0
  38. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/resources/__init__.py +0 -0
  39. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/resources/ai_plans.py +0 -0
  40. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/resources/apps.py +0 -0
  41. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/resources/base.py +0 -0
  42. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/resources/catalog.py +0 -0
  43. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/resources/clients.py +0 -0
  44. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/resources/comments.py +0 -0
  45. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/resources/contacts.py +0 -0
  46. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/resources/dashboard.py +0 -0
  47. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/resources/integrations.py +0 -0
  48. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/resources/messages.py +0 -0
  49. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/resources/organizations.py +0 -0
  50. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/resources/products.py +0 -0
  51. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/resources/publications.py +0 -0
  52. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/resources/uploads.py +0 -0
  53. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/resources_sync/__init__.py +0 -0
  54. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/resources_sync/ai_plans.py +0 -0
  55. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/resources_sync/apps.py +0 -0
  56. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/resources_sync/base.py +0 -0
  57. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/resources_sync/catalog.py +0 -0
  58. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/resources_sync/clients.py +0 -0
  59. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/resources_sync/comments.py +0 -0
  60. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/resources_sync/contacts.py +0 -0
  61. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/resources_sync/dashboard.py +0 -0
  62. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/resources_sync/integrations.py +0 -0
  63. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/resources_sync/messages.py +0 -0
  64. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/resources_sync/organizations.py +0 -0
  65. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/resources_sync/products.py +0 -0
  66. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/resources_sync/publications.py +0 -0
  67. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/resources_sync/uploads.py +0 -0
  68. {planvortex-0.4.0 → planvortex-0.6.0}/src/planvortex/webhooks.py +0 -0
  69. {planvortex-0.4.0 → planvortex-0.6.0}/tests/__init__.py +0 -0
  70. {planvortex-0.4.0 → planvortex-0.6.0}/tests/conftest.py +0 -0
  71. {planvortex-0.4.0 → planvortex-0.6.0}/tests/contrato.py +0 -0
  72. {planvortex-0.4.0 → planvortex-0.6.0}/tests/live/__init__.py +0 -0
  73. {planvortex-0.4.0 → planvortex-0.6.0}/tests/live/conftest.py +0 -0
  74. {planvortex-0.4.0 → planvortex-0.6.0}/tests/live/test_auth.py +0 -0
  75. {planvortex-0.4.0 → planvortex-0.6.0}/tests/live/test_catalog.py +0 -0
  76. {planvortex-0.4.0 → planvortex-0.6.0}/tests/live/test_connect_flow.py +0 -0
  77. {planvortex-0.4.0 → planvortex-0.6.0}/tests/live/test_errors.py +0 -0
  78. {planvortex-0.4.0 → planvortex-0.6.0}/tests/live/test_publish.py +0 -0
  79. {planvortex-0.4.0 → planvortex-0.6.0}/tests/live/test_read.py +0 -0
  80. {planvortex-0.4.0 → planvortex-0.6.0}/tests/test_accounts.py +0 -0
  81. {planvortex-0.4.0 → planvortex-0.6.0}/tests/test_ai_plans.py +0 -0
  82. {planvortex-0.4.0 → planvortex-0.6.0}/tests/test_apps.py +0 -0
  83. {planvortex-0.4.0 → planvortex-0.6.0}/tests/test_catalog.py +0 -0
  84. {planvortex-0.4.0 → planvortex-0.6.0}/tests/test_client.py +0 -0
  85. {planvortex-0.4.0 → planvortex-0.6.0}/tests/test_clients.py +0 -0
  86. {planvortex-0.4.0 → planvortex-0.6.0}/tests/test_comments.py +0 -0
  87. {planvortex-0.4.0 → planvortex-0.6.0}/tests/test_contacts.py +0 -0
  88. {planvortex-0.4.0 → planvortex-0.6.0}/tests/test_dashboard.py +0 -0
  89. {planvortex-0.4.0 → planvortex-0.6.0}/tests/test_example_comments.py +0 -0
  90. {planvortex-0.4.0 → planvortex-0.6.0}/tests/test_example_schedule.py +0 -0
  91. {planvortex-0.4.0 → planvortex-0.6.0}/tests/test_example_webhooks.py +0 -0
  92. {planvortex-0.4.0 → planvortex-0.6.0}/tests/test_files.py +0 -0
  93. {planvortex-0.4.0 → planvortex-0.6.0}/tests/test_generate_models.py +0 -0
  94. {planvortex-0.4.0 → planvortex-0.6.0}/tests/test_generate_sync.py +0 -0
  95. {planvortex-0.4.0 → planvortex-0.6.0}/tests/test_integrations.py +0 -0
  96. {planvortex-0.4.0 → planvortex-0.6.0}/tests/test_messages.py +0 -0
  97. {planvortex-0.4.0 → planvortex-0.6.0}/tests/test_models.py +0 -0
  98. {planvortex-0.4.0 → planvortex-0.6.0}/tests/test_openapi_freshness.py +0 -0
  99. {planvortex-0.4.0 → planvortex-0.6.0}/tests/test_packaging.py +0 -0
  100. {planvortex-0.4.0 → planvortex-0.6.0}/tests/test_pagination.py +0 -0
  101. {planvortex-0.4.0 → planvortex-0.6.0}/tests/test_products.py +0 -0
  102. {planvortex-0.4.0 → planvortex-0.6.0}/tests/test_publications.py +0 -0
  103. {planvortex-0.4.0 → planvortex-0.6.0}/tests/test_query_parity.py +0 -0
  104. {planvortex-0.4.0 → planvortex-0.6.0}/tests/test_readme.py +0 -0
  105. {planvortex-0.4.0 → planvortex-0.6.0}/tests/test_route_coverage.py +0 -0
  106. {planvortex-0.4.0 → planvortex-0.6.0}/tests/test_shapes_parity.py +0 -0
  107. {planvortex-0.4.0 → planvortex-0.6.0}/tests/test_types.py +0 -0
  108. {planvortex-0.4.0 → planvortex-0.6.0}/tests/test_uploads.py +0 -0
  109. {planvortex-0.4.0 → planvortex-0.6.0}/tests/test_webhooks.py +0 -0
@@ -4,6 +4,66 @@ 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.6.0] - 2026-09-03
8
+
9
+ **Publications are unlimited on every plan**, so the quota that said otherwise is gone from the
10
+ types.
11
+
12
+ It was never a number this package invented: `PlanData["publications"]` came from the spec, and the
13
+ server stopped charging for it on 02-09-2026. What stayed behind was worse than a wrong number — it
14
+ was a key mypy kept promising, so `limits["publications"]` type-checked and raised `KeyError` at
15
+ runtime.
16
+
17
+ ### Changed
18
+
19
+ - **BREAKING (types only): `PlanData` no longer has a `publications` key.** The count moved to the
20
+ new `PlanUseData`, which is what `actual_use` has always been in practice: the same shape as a
21
+ plan plus `publications`, and `NotRequired` because a brand-new organization has spent nothing.
22
+ Nothing changes on the wire.
23
+ - `OrganizationUse["actual_use"]` is a `PlanUseData`; `actual_asigned` stays a `PlanData`, which is
24
+ the real asymmetry: what a child organization was handed cannot include a metric.
25
+ - The `publish` example prints publications as what they are — a monthly count with no ceiling —
26
+ instead of `12 de 100`. What throttles publishing is rate (per hour and account, and the
27
+ per-network daily caps in `GET /social_limits`), not the plan.
28
+ - **The `publication` error family now reaches 979, not 960.** Everything above the old ceiling was
29
+ falling outside every range and arriving as a bare `PlanVortexError`: the Bluesky, Discord,
30
+ Telegram and Threads codes (961-977) and, more to the point, the two rate brakes that replaced the
31
+ monthly quota — **978** (publishing too fast on this account) and **979** (that network's daily
32
+ cap). `except PublicationError` now catches them.
33
+
34
+ ### Added
35
+
36
+ **The public API is on every plan now, the free one included** — it used to be a Custom-plan
37
+ feature — and the types follow the three things that came with opening it.
38
+
39
+ - `PlanData["apps"]`: how many apps the plan allows (1 on Free, 2 on Basic, 5 on Pro, 10 on Custom).
40
+ Each app is a `client_id` with a secret, so each one is a key to the whole API. Unlike accounts,
41
+ users or storage it is **not** split between organizations — an app belongs to the client.
42
+ - Three error codes, and the two ranges that were hiding them: **545** (the plan's API rate limit,
43
+ which arrives as a `429` with `Retry-After`) and **546** (the account's email must be verified to
44
+ create an app) are `AuthError`, so that range now runs to 546; **1308** (no more apps fit in this
45
+ plan) is a `PlanLimitError`, so that one runs to 1308.
46
+
47
+ ## [0.5.0] - 2026-09-02
48
+
49
+ **Threads is the twelfth network**, and the types now know it.
50
+
51
+ Nothing here removes anything: `SocialNetwork` gains a value, `PublishableNetwork` gains another,
52
+ and everything that type-checked before type-checks now. What was wrong until today was quieter than
53
+ an error — the committed copy of the spec predated Threads, so an account coming back from a real
54
+ API arrived with a `social_network` no `Literal` had ever heard of, and the prose still counted
55
+ eleven networks where the API had twelve.
56
+
57
+ ### Changed
58
+
59
+ - The OpenAPI copy and the generated models are rebuilt from the published document, so `threads`
60
+ is a `SocialNetwork`, a `CommentNetwork` and a publishable network.
61
+ - `PublishableNetwork` — hand-maintained, because generating it is not possible — includes
62
+ `threads`: **eleven of the twelve** networks have a feed, and `google_business` is still the one
63
+ that does not.
64
+ - The prose that counts networks: a connection link is a `redirect` on **ten of the twelve**, not
65
+ nine of eleven. WhatsApp and Telegram are still the two that are not.
66
+
7
67
  ## [0.4.0] - 2026-08-31
8
68
 
9
69
  An AI plan can now be **archived**, and deleting one finally means what people thought it meant.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: planvortex
3
- Version: 0.4.0
3
+ Version: 0.6.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/
@@ -242,15 +242,15 @@ memorising numbers:
242
242
 
243
243
  | Codes | Family | Exception |
244
244
  |---|---|---|
245
- | 500-544 | `auth` | `AuthError` |
245
+ | 500-546 | `auth` | `AuthError` |
246
246
  | 601-612 | `user` | `UserError` |
247
247
  | 700-715 | `account` | `AccountError` |
248
248
  | 800-810 | `file` | `FileError` |
249
- | 900-960 | `publication` | `PublicationError` |
249
+ | 900-979 | `publication` | `PublicationError` |
250
250
  | 1000-1003 | `general` | `PlanVortexError` |
251
251
  | 1100-1111 | `organization` | `OrganizationError` |
252
252
  | 1200-1207 | `role` | `PlanVortexError` |
253
- | 1300-1307, 1400-1408 | `plan_limit` | `PlanLimitError` |
253
+ | 1300-1308, 1400-1408 | `plan_limit` | `PlanLimitError` |
254
254
  | 1500-1512 | `messaging` | `MessagingError` |
255
255
  | 1600-1601 | `contact` | `ContactError` |
256
256
  | 1900-1906 | `payment` | `PlanVortexError` |
@@ -212,15 +212,15 @@ memorising numbers:
212
212
 
213
213
  | Codes | Family | Exception |
214
214
  |---|---|---|
215
- | 500-544 | `auth` | `AuthError` |
215
+ | 500-546 | `auth` | `AuthError` |
216
216
  | 601-612 | `user` | `UserError` |
217
217
  | 700-715 | `account` | `AccountError` |
218
218
  | 800-810 | `file` | `FileError` |
219
- | 900-960 | `publication` | `PublicationError` |
219
+ | 900-979 | `publication` | `PublicationError` |
220
220
  | 1000-1003 | `general` | `PlanVortexError` |
221
221
  | 1100-1111 | `organization` | `OrganizationError` |
222
222
  | 1200-1207 | `role` | `PlanVortexError` |
223
- | 1300-1307, 1400-1408 | `plan_limit` | `PlanLimitError` |
223
+ | 1300-1308, 1400-1408 | `plan_limit` | `PlanLimitError` |
224
224
  | 1500-1512 | `messaging` | `MessagingError` |
225
225
  | 1600-1601 | `contact` | `ContactError` |
226
226
  | 1900-1906 | `payment` | `PlanVortexError` |
@@ -107,7 +107,7 @@
107
107
  "accounts"
108
108
  ],
109
109
  "summary": "Get the connection links of every connectable network",
110
- "description": "The authorization URL of each network, so the user can connect an account to this organization.\n\n**A network that cannot produce a link simply does not appear.** That is a legitimate answer, not a failure: it is what happens with `discord` in an organization that has not saved its own bot credentials yet (see `PUT /organizations/{id_organization}/social_credentials/{social_network}`).\n\n**An app cannot call this.** Connecting a social account is an OAuth flow with a person in front of it, so this endpoint only accepts a user token or a temporal connect token; with app credentials it answers error 519. The way an integration does it is to issue a temporal connect token with `GET /organizations/{id_organization}/temporal_connect_token` and hand it to its end user.\n\n**Read `authorization`, not `link`.** Nine of the eleven networks are `redirect` and you send the person to `link`. Two are not, and neither of them fails visibly if you treat it as one:\n\n• **WhatsApp is not a URL at all.** Its sign-up is Meta's Embedded Signup, a popup you raise with the Facebook JavaScript SDK, so its `link` is an empty string and everything you need to open that popup travels in `authorization`. A client that loops over the list and redirects to `link` sends its user to its own page.\n• **Telegram has a link and still is not a redirect.** It opens a chat with the PlanVortex bot, and nobody comes back from it: the account is born minutes later, from the bot being added to a channel, and it is announced over the WebSocket. Open it in another tab and keep listening; redirect to it and there is nobody left to tell.",
110
+ "description": "The authorization URL of each network, so the user can connect an account to this organization.\n\n**A network that cannot produce a link simply does not appear.** That is a legitimate answer, not a failure: it is what happens with `discord` in an organization that has not saved its own bot credentials yet (see `PUT /organizations/{id_organization}/social_credentials/{social_network}`).\n\n**An app cannot call this.** Connecting a social account is an OAuth flow with a person in front of it, so this endpoint only accepts a user token or a temporal connect token; with app credentials it answers error 519. The way an integration does it is to issue a temporal connect token with `GET /organizations/{id_organization}/temporal_connect_token` and hand it to its end user.\n\n**Read `authorization`, not `link`.** Ten of the twelve networks are `redirect` and you send the person to `link`. Two are not, and neither of them fails visibly if you treat it as one:\n\n• **WhatsApp is not a URL at all.** Its sign-up is Meta's Embedded Signup, a popup you raise with the Facebook JavaScript SDK, so its `link` is an empty string and everything you need to open that popup travels in `authorization`. A client that loops over the list and redirects to `link` sends its user to its own page.\n• **Telegram has a link and still is not a redirect.** It opens a chat with the PlanVortex bot, and nobody comes back from it: the account is born minutes later, from the bot being added to a channel, and it is announced over the WebSocket. Open it in another tab and keep listening; redirect to it and there is nobody left to tell.",
111
111
  "operationId": "getConnectLinks",
112
112
  "x-planvortex-identity": [
113
113
  "current_user",
@@ -1872,7 +1872,8 @@
1872
1872
  "google_business",
1873
1873
  "bluesky",
1874
1874
  "discord",
1875
- "telegram"
1875
+ "telegram",
1876
+ "threads"
1876
1877
  ]
1877
1878
  }
1878
1879
  }
@@ -2448,7 +2449,6 @@
2448
2449
  "name": "Client X",
2449
2450
  "actual_plan": {
2450
2451
  "accounts": 4,
2451
- "publications": 150,
2452
2452
  "users": 1,
2453
2453
  "space": 0.25,
2454
2454
  "integrations": 1,
@@ -2624,7 +2624,6 @@
2624
2624
  "value": {
2625
2625
  "actual_plan": {
2626
2626
  "accounts": 8,
2627
- "publications": 300,
2628
2627
  "users": 3,
2629
2628
  "space": 1,
2630
2629
  "integrations": 3,
@@ -3597,7 +3596,7 @@
3597
3596
  "comments"
3598
3597
  ],
3599
3598
  "summary": "The inbox: first-level comments across the whole organization",
3600
- "description": "Served from PlanVortex's database, so it costs nothing and calls no social network. It is a **snapshot**: `collected_date` says when each row was last read. Open a thread to see what the network says right now.\n\nOrdered by `creation_date` descending — the date on the network, not the date we collected it — so rows from nine networks interleave correctly.\n\n**On `telegram` the inbox starts the day the channel was connected.** The Bot API has no way to read the past: a bot only learns what happens while it is inside, so nothing written before the connection exists here and never will. Say so in your UI — an inbox that opens empty on a busy channel reads like a failure.\n\n**Your own replies are not in here.** Anything with `author.is_own: true` is filtered out: what you wrote is not incoming mail. They are still stored, and they do show up in the thread.\n\nRequires the `comments:read` permission (`client_organization_comments:read` for apps) and a paid plan.",
3599
+ "description": "Served from PlanVortex's database, so it costs nothing and calls no social network. It is a **snapshot**: `collected_date` says when each row was last read. Open a thread to see what the network says right now.\n\nOrdered by `creation_date` descending — the date on the network, not the date we collected it — so rows from ten networks interleave correctly.\n\n**On `telegram` the inbox starts the day the channel was connected.** The Bot API has no way to read the past: a bot only learns what happens while it is inside, so nothing written before the connection exists here and never will. Say so in your UI — an inbox that opens empty on a busy channel reads like a failure.\n\n**Your own replies are not in here.** Anything with `author.is_own: true` is filtered out: what you wrote is not incoming mail. They are still stored, and they do show up in the thread.\n\nRequires the `comments:read` permission (`client_organization_comments:read` for apps) and a paid plan.",
3601
3600
  "operationId": "getComments",
3602
3601
  "x-planvortex-identity": [
3603
3602
  "current_user",
@@ -4086,7 +4085,7 @@
4086
4085
  "catalog"
4087
4086
  ],
4088
4087
  "summary": "What every network supports",
4089
- "description": "The full matrix network → capabilities. It is the **single source of truth** about what a network can do: the catalogue publishes it, the backend checks it before acting, and your integration should filter its account pickers with it rather than keeping a list of its own.\n\n`comments` is the coarse gate — whether the network has comments at all. Which *actions* it allows on one is a finer question and lives in `GET /social_comment_actions`, because the shape here is `{[capability]: boolean}` and nesting an object inside would break it.\n\nToday nine networks answer `comments: true`: Facebook, Instagram, LinkedIn, X, YouTube, Google Business, Bluesky, Discord and Telegram. TikTok and WhatsApp answer `false`, for reasons of theirs and not ours.\n\nA `true` here is about the **network**, not about one account of it: a Telegram channel with no linked discussion group answers 965 on its comments even though the network has them.\n\nOnly needs authentication.",
4088
+ "description": "The full matrix network → capabilities. It is the **single source of truth** about what a network can do: the catalogue publishes it, the backend checks it before acting, and your integration should filter its account pickers with it rather than keeping a list of its own.\n\n`comments` is the coarse gate — whether the network has comments at all. Which *actions* it allows on one is a finer question and lives in `GET /social_comment_actions`, because the shape here is `{[capability]: boolean}` and nesting an object inside would break it.\n\nToday ten networks answer `comments: true`: Facebook, Instagram, Threads, LinkedIn, X, YouTube, Google Business, Bluesky, Discord and Telegram. TikTok and WhatsApp answer `false`, for reasons of theirs and not ours.\n\nA `true` here is about the **network**, not about one account of it: a Telegram channel with no linked discussion group answers 965 on its comments even though the network has them.\n\nOnly needs authentication.",
4090
4089
  "operationId": "getSocialCapabilities",
4091
4090
  "x-planvortex-identity": [
4092
4091
  "current_user",
@@ -4144,7 +4143,7 @@
4144
4143
  "catalog"
4145
4144
  ],
4146
4145
  "summary": "What each network lets you do to a comment",
4147
- "description": "The fine-grained matrix: reply, hide, delete your own, delete somebody else's — network by network. Read it before painting controls. Networks without comments answer `false` to all four.\n\nThe differences are real and each has a reason in the network's own API:\n\n| Network | reply | hide | delete_own | delete_others |\n| --- | --- | --- | --- | --- |\n| `facebook` | yes | yes | yes | yes |\n| `instagram` | yes | yes | yes | **no** — Instagram only lets you hide someone else's |\n| `youtube` | yes | yes | yes | yes — the channel owner really does moderate |\n| `linkedin` | yes | **no** — there is no hide endpoint | yes | yes |\n| `twitter` | yes | yes | yes | **no** — you cannot delete another account's post |\n| `google_business` | yes | **no** | yes — **your reply**, never the review | **no** |\n| `bluesky` | yes | yes — through the post's `threadgate` | yes | **no** — the reply lives in somebody else's repository |\n| `discord` | yes | **no** — Discord has no hide, only delete | yes | yes |\n| `telegram` | yes | **no** — Telegram has no hide either | yes | yes — with the bot as an admin of the discussion group, otherwise error 969 |\n| `tiktok`, `whatsapp` | no | no | no | no |\n\nOnly needs authentication.",
4146
+ "description": "The fine-grained matrix: reply, hide, delete your own, delete somebody else's — network by network. Read it before painting controls. Networks without comments answer `false` to all four.\n\nThe differences are real and each has a reason in the network's own API:\n\n| Network | reply | hide | delete_own | delete_others |\n| --- | --- | --- | --- | --- |\n| `facebook` | yes | yes | yes | yes |\n| `instagram` | yes | yes | yes | **no** — Instagram only lets you hide someone else's |\n| `youtube` | yes | yes | yes | yes — the channel owner really does moderate |\n| `linkedin` | yes | **no** — there is no hide endpoint | yes | yes |\n| `twitter` | yes | yes | yes | **no** — you cannot delete another account's post |\n| `google_business` | yes | **no** | yes — **your reply**, never the review | **no** |\n| `bluesky` | yes | yes — through the post's `threadgate` | yes | **no** — the reply lives in somebody else's repository |\n| `discord` | yes | **no** — Discord has no hide, only delete | yes | yes |\n| `telegram` | yes | **no** — Telegram has no hide either | yes | yes — with the bot as an admin of the discussion group, otherwise error 969 |\n| `threads` | yes | yes | yes | **no** — deleting somebody else's reply is not a permission you lack, it is a call that does not exist in the API |\n| `tiktok`, `whatsapp` | no | no | no | no |\n\nOnly needs authentication.",
4148
4147
  "operationId": "getSocialCommentActions",
4149
4148
  "x-planvortex-identity": [
4150
4149
  "current_user",
@@ -6023,7 +6022,6 @@
6023
6022
  "value": {
6024
6023
  "actual_plan": {
6025
6024
  "accounts": 8,
6026
- "publications": 300,
6027
6025
  "users": 3,
6028
6026
  "space": 1,
6029
6027
  "integrations": 3,
@@ -6342,7 +6340,6 @@
6342
6340
  "name": "Client X - Spain",
6343
6341
  "actual_plan": {
6344
6342
  "accounts": 3,
6345
- "publications": 100,
6346
6343
  "users": 1,
6347
6344
  "space": 0.25,
6348
6345
  "integrations": 0,
@@ -9510,7 +9507,7 @@
9510
9507
  "required": [
9511
9508
  "type"
9512
9509
  ],
9513
- "description": "**How** an account of this network is authorized, which is not always \"send the user to this URL\".\n\nNine of the eleven networks are `redirect`: open `link` and the network sends the person back to PlanVortex with a code. Two are not:\n\n• **WhatsApp.** Its sign-up is Meta's *Embedded Signup*: a popup raised by the Facebook JavaScript SDK from your own page, which returns — over `postMessage` — session data (`waba_id`, `phone_number_id`) that no query string carries. Its `link` is therefore an empty string.\n• **Telegram.** There is no OAuth here: no consent screen, no `code`, no account token. `link` opens a private chat with the PlanVortex bot, the person then adds that bot to their channel, and **the account is created from that event**, not from any request of yours. Which means the connection cannot be finished by calling `GET /organizations/{id_organization}/account-connect/telegram` — see that endpoint.\n\nBranch on `authorization.type`, never on whether `link` is empty.",
9510
+ "description": "**How** an account of this network is authorized, which is not always \"send the user to this URL\".\n\nTen of the twelve networks are `redirect`: open `link` and the network sends the person back to PlanVortex with a code. Two are not:\n\n• **WhatsApp.** Its sign-up is Meta's *Embedded Signup*: a popup raised by the Facebook JavaScript SDK from your own page, which returns — over `postMessage` — session data (`waba_id`, `phone_number_id`) that no query string carries. Its `link` is therefore an empty string.\n• **Telegram.** There is no OAuth here: no consent screen, no `code`, no account token. `link` opens a private chat with the PlanVortex bot, the person then adds that bot to their channel, and **the account is created from that event**, not from any request of yours. Which means the connection cannot be finished by calling `GET /organizations/{id_organization}/account-connect/telegram` — see that endpoint.\n\nBranch on `authorization.type`, never on whether `link` is empty.",
9514
9511
  "properties": {
9515
9512
  "type": {
9516
9513
  "type": "string",
@@ -10514,7 +10511,7 @@
10514
10511
  "$ref": "#/components/schemas/CatalogSocialLimitsMap"
10515
10512
  }
10516
10513
  ],
10517
- "description": "Maximum length of a publication's text. Bluesky counts graphemes, everyone else counts characters — Telegram included, where `String.length` is exactly the right unit.\n\nOn Telegram there are **two numbers for the same field**: `telegram` (4.096) while the publication is text only, and `telegram_media` (1.024) the moment it carries an image or a video, because then the text is a media caption and not a message. Switch the counter when the file is attached, not when publish is pressed."
10514
+ "description": "Maximum length of a publication's text. **Bluesky and Threads count GRAPHEMES**, everyone else counts characters — Telegram included, where `String.length` is exactly the right unit. The difference is not academic: an emoji is one grapheme and two `String.length` units, so counting a Threads post with `.length` rejects at 250 emojis what the network publishes happily at 500.\n\nOn Telegram there are **two numbers for the same field**: `telegram` (4.096) while the publication is text only, and `telegram_media` (1.024) the moment it carries an image or a video, because then the text is a media caption and not a message. Switch the counter when the file is attached, not when publish is pressed."
10518
10515
  },
10519
10516
  "max_post_bytes": {
10520
10517
  "allOf": [
@@ -10701,7 +10698,7 @@
10701
10698
  "actual_use": {
10702
10699
  "allOf": [
10703
10700
  {
10704
- "$ref": "#/components/schemas/PlanData"
10701
+ "$ref": "#/components/schemas/PlanUseData"
10705
10702
  }
10706
10703
  ],
10707
10704
  "description": "Current consumption across every organization of the client. **Only present with `getUse=true`.** `twitter_credits` and `ai_credits` are what has been spent this calendar month."
@@ -11257,7 +11254,8 @@
11257
11254
  "google_business",
11258
11255
  "bluesky",
11259
11256
  "discord",
11260
- "telegram"
11257
+ "telegram",
11258
+ "threads"
11261
11259
  ]
11262
11260
  },
11263
11261
  "CommentsCommentThread": {
@@ -12077,7 +12075,7 @@
12077
12075
  "actual_use": {
12078
12076
  "allOf": [
12079
12077
  {
12080
- "$ref": "#/components/schemas/PlanData"
12078
+ "$ref": "#/components/schemas/PlanUseData"
12081
12079
  }
12082
12080
  ],
12083
12081
  "description": "What the organization and its children are consuming right now."
@@ -12263,7 +12261,7 @@
12263
12261
  },
12264
12262
  "code": {
12265
12263
  "type": "integer",
12266
- "description": "PlanVortex error code. Ranges: 500-544 auth, tokens and client apps · 601-612 user · 700-715 social accounts · 800-810 files · 900-960 publications · 1000-1003 general · 1100-1111 organizations · 1200-1207 roles · 1300-1307 client plan · 1400-1408 organization plan · 1500-1512 messaging · 1600-1601 contacts · 1900-1906 payments · 2000-2099 products · 2100-2199 AI plans · 2200-2299 integrations."
12264
+ "description": "PlanVortex error code. Ranges: 500-546 auth, tokens and client apps · 601-612 user · 700-715 social accounts · 800-810 files · 900-979 publications · 1000-1003 general · 1100-1111 organizations · 1200-1207 roles · 1300-1308 client plan · 1400-1408 organization plan · 1500-1512 messaging · 1600-1601 contacts · 1900-1906 payments · 2000-2099 products · 2100-2199 AI plans · 2200-2299 integrations."
12267
12265
  },
12268
12266
  "data": {
12269
12267
  "additionalProperties": true,
@@ -12941,7 +12939,7 @@
12941
12939
  "actual_use": {
12942
12940
  "allOf": [
12943
12941
  {
12944
- "$ref": "#/components/schemas/PlanData"
12942
+ "$ref": "#/components/schemas/PlanUseData"
12945
12943
  }
12946
12944
  ],
12947
12945
  "description": "Current consumption. **Only present with `getUse=true`.** `twitter_credits` and `ai_credits` are what has been spent in the current calendar month; the rest is what exists right now."
@@ -13383,7 +13381,6 @@
13383
13381
  "description": "The resources a plan grants. On a client it is what was contracted; on an organization, the slice of it that was assigned. The sum across all the organizations of a client can never exceed what the client has contracted.",
13384
13382
  "required": [
13385
13383
  "accounts",
13386
- "publications",
13387
13384
  "users",
13388
13385
  "space",
13389
13386
  "integrations"
@@ -13393,10 +13390,6 @@
13393
13390
  "type": "integer",
13394
13391
  "description": "Social accounts that may be connected."
13395
13392
  },
13396
- "publications": {
13397
- "type": "integer",
13398
- "description": "Publications that may be sent per month."
13399
- },
13400
13393
  "users": {
13401
13394
  "type": "integer",
13402
13395
  "description": "Users with access."
@@ -13407,7 +13400,11 @@
13407
13400
  },
13408
13401
  "integrations": {
13409
13402
  "type": "integer",
13410
- "description": "Connections to a third-party tool material is pulled from (Google Drive, an RSS feed). Not the same thing as an app: an app is API access, and it is a Custom-plan feature of its own."
13403
+ "description": "Connections to a third-party tool material is pulled from (Google Drive, an RSS feed). Not the same thing as an app: an app is API access, and it has its own allowance in `apps`."
13404
+ },
13405
+ "apps": {
13406
+ "type": "integer",
13407
+ "description": "Apps this plan allows: 1 on Free, 2 on Basic, 5 on Pro, 10 on Custom. Each one is a `client_id` with a secret, so each one is a key to the whole public API — which every plan has, Free included. Unlike accounts, users or storage this is NOT split between organizations: an app belongs to the client. On a use payload it is how many exist right now."
13411
13408
  },
13412
13409
  "twitter_credits": {
13413
13410
  "type": "integer",
@@ -13431,6 +13428,21 @@
13431
13428
  }
13432
13429
  }
13433
13430
  },
13431
+ "PlanUseData": {
13432
+ "type": "object",
13433
+ "description": "What is being consumed right now. Same shape as PlanData plus `publications`, which is counted but neither charged nor split between organizations.",
13434
+ "allOf": [
13435
+ {
13436
+ "$ref": "#/components/schemas/PlanData"
13437
+ }
13438
+ ],
13439
+ "properties": {
13440
+ "publications": {
13441
+ "type": "integer",
13442
+ "description": "Publications created in the current calendar month. A METRIC, not a quota: publications are unlimited on every plan, so there is no limit to compare it against. What throttles publishing is rate, not plan — see the per-hour and per-network daily caps."
13443
+ }
13444
+ }
13445
+ },
13434
13446
  "ProductsProduct": {
13435
13447
  "allOf": [
13436
13448
  {
@@ -13725,7 +13737,7 @@
13725
13737
  },
13726
13738
  "PublicationStats": {
13727
13739
  "type": "object",
13728
- "description": "Raw, per-network metrics for a publication. Only the fields that belong to the publication's own social network are returned.\n\n**An absent field is not a zero.** A field is present only when the network actually reported it; `0` means the network measured zero. This matters most on X (Twitter), where metrics are split into groups with different access levels: `public_metrics` (likes, replys, retwets, quotes, bookmarks, impressions) is always available, while clicks, the pre-computed `engagement` and the video playback quartiles come from X's non-public metrics — only for your own posts, within 30 days of publishing, and only if the app is entitled to them. When they are unavailable they are omitted rather than returned as `0`. Do not default missing fields to zero when displaying them.\n\nOn `discord` there are only two: `likes` (the reactions on the message) and `comments` (the messages in its thread). There is no impressions figure anywhere in Discord's API, so engagement is computed over the server's member count.\n\nOn `bluesky` there are no impressions and no reach either — only the public counters — so engagement is computed over followers.\n\nOn `telegram` there are two as well, and **neither of them is asked for**: the Bot API has no method that returns a message's metrics, so `reactions` arrives on its own through the bot and `comments` is counted in PlanVortex's own inbox. There are no impressions, no reach, no views and no forwards to be had anywhere in it, so engagement is computed over followers.",
13740
+ "description": "Raw, per-network metrics for a publication. Only the fields that belong to the publication's own social network are returned.\n\n**An absent field is not a zero.** A field is present only when the network actually reported it; `0` means the network measured zero. This matters most on X (Twitter), where metrics are split into groups with different access levels: `public_metrics` (likes, replys, retwets, quotes, bookmarks, impressions) is always available, while clicks, the pre-computed `engagement` and the video playback quartiles come from X's non-public metrics — only for your own posts, within 30 days of publishing, and only if the app is entitled to them. When they are unavailable they are omitted rather than returned as `0`. Do not default missing fields to zero when displaying them.\n\nOn `discord` there are only two: `likes` (the reactions on the message) and `comments` (the messages in its thread). There is no impressions figure anywhere in Discord's API, so engagement is computed over the server's member count.\n\nOn `bluesky` there are no impressions and no reach either — only the public counters — so engagement is computed over followers.\n\nOn `threads` there are six, and the one that matters is `views`: it is the only one of the four newest networks with something like impressions, so its engagement rate is computed over a real base and not over followers. Sharing arrives split in three — `reposts`, `quotes` and `shares` (outside Threads) — and the three are added into one figure, the same criterion as on X; the breakdown stays in `raw`. A reply is what every other network calls a comment.\n\nOn `telegram` there are two as well, and **neither of them is asked for**: the Bot API has no method that returns a message's metrics, so `reactions` arrives on its own through the bot and `comments` is counted in PlanVortex's own inbox. There are no impressions, no reach, no views and no forwards to be had anywhere in it, so engagement is computed over followers.",
13729
13741
  "properties": {
13730
13742
  "likes": {
13731
13743
  "type": "integer"
@@ -13864,7 +13876,8 @@
13864
13876
  "youtube",
13865
13877
  "bluesky",
13866
13878
  "discord",
13867
- "telegram"
13879
+ "telegram",
13880
+ "threads"
13868
13881
  ],
13869
13882
  "description": "Network the publication targets. **Required when creating**: the request fails with error 702 if it is missing or not one of these values. It must match the network of the account in the path.\n\nNot every connectable network publishes — a local business listing receives reviews, not posts — so this list is shorter than the one in `GET /social_networks`. Ask `GET /allowed_social_publications` rather than hardcoding it, because it grows."
13870
13883
  },
@@ -14207,7 +14220,8 @@
14207
14220
  "google_business",
14208
14221
  "bluesky",
14209
14222
  "discord",
14210
- "telegram"
14223
+ "telegram",
14224
+ "threads"
14211
14225
  ],
14212
14226
  "type": "string",
14213
14227
  "description": "A social network supported by PlanVortex.\n\n**This list grows.** Treat it as an open enumeration: a client that rejects an unknown value breaks the day a network is added, which happens several times a year. Not every network does everything — ask `GET /social_capabilities`."
@@ -9,7 +9,7 @@ name = "planvortex"
9
9
  # entonces `project.version` desapareceria del pyproject y el paso del workflow de release que
10
10
  # compara el tag con la version —que lo lee con `tomllib`— dejaria de encontrarla. Se quedan las dos
11
11
  # y las ata `tests/test_packaging.py`, que falla si se separan.
12
- version = "0.4.0"
12
+ version = "0.6.0"
13
13
  description = "Official Python client for the PlanVortex API"
14
14
  readme = "README.md"
15
15
  requires-python = ">=3.10"
@@ -27,15 +27,22 @@ class ErrorRange(NamedTuple):
27
27
  # Los mismos 16 rangos que publica `swagger/common.json`, `PLANVORTEX_ERROR_RANGES` del paquete de
28
28
  # Node y el apendice A del roadmap. SI SE TOCA UN RANGO, SE TOCAN LOS CINCO SITIOS.
29
29
  PLANVORTEX_ERROR_RANGES: tuple[ErrorRange, ...] = (
30
- ErrorRange(500, 544, "auth"),
30
+ # Hasta 546, no 544: la API publica se abrio a todos los planes y trajo dos codigos nuevos
31
+ # —545 (ritmo por plan, que sale 429) y 546 (correo sin verificar al crear una app)—.
32
+ ErrorRange(500, 546, "auth"),
31
33
  ErrorRange(601, 612, "user"),
32
34
  ErrorRange(700, 715, "account"),
33
35
  ErrorRange(800, 810, "file"),
34
- ErrorRange(900, 960, "publication"),
36
+ # El techo sube con el catalogo del servidor, y subir tarde no da un error: da un consejo
37
+ # equivocado. Los codigos de Bluesky, Discord, Telegram y Threads (961-977) y los dos frenos
38
+ # de ritmo de la fase de publicaciones ilimitadas (978, 979) nacieron por encima de 960 y
39
+ # caian fuera de toda familia.
40
+ ErrorRange(900, 979, "publication"),
35
41
  ErrorRange(1000, 1003, "general"),
36
42
  ErrorRange(1100, 1111, "organization"),
37
43
  ErrorRange(1200, 1207, "role"),
38
- ErrorRange(1300, 1307, "plan_limit"),
44
+ # 1308 es el tope de APPS del plan, que nacio al quitar el candado de precio de la API.
45
+ ErrorRange(1300, 1308, "plan_limit"),
39
46
  ErrorRange(1400, 1408, "plan_limit"),
40
47
  ErrorRange(1500, 1512, "messaging"),
41
48
  ErrorRange(1600, 1601, "contact"),
@@ -101,7 +108,7 @@ class PlanVortexError(Exception):
101
108
 
102
109
 
103
110
  class AuthError(PlanVortexError):
104
- """500-544 — tokens, client apps, permissions. Includes 501 and 522, the expired-token ones."""
111
+ """500-546 — tokens, client apps, permissions. Includes 501 and 522, the expired-token ones."""
105
112
 
106
113
 
107
114
  class UserError(PlanVortexError):
@@ -117,7 +124,9 @@ class FileError(PlanVortexError):
117
124
 
118
125
 
119
126
  class PublicationError(PlanVortexError):
120
- """900-960 — publications, including per-network limits (characters, images, duration)."""
127
+ """900-979 — publications, including per-network limits (characters, images, duration) and the
128
+ two rate brakes: 978 (publishing too fast on this account) and 979 (that network's daily cap).
129
+ """
121
130
 
122
131
 
123
132
  class OrganizationError(PlanVortexError):
@@ -125,7 +134,7 @@ class OrganizationError(PlanVortexError):
125
134
 
126
135
 
127
136
  class PlanLimitError(PlanVortexError):
128
- """1300-1307 and 1400-1408 — the plan's quota, on the client or on the organization.
137
+ """1300-1308 and 1400-1408 — the plan's quota, on the client or on the organization.
129
138
 
130
139
  This is the error an integrator actually wants to tell apart: it is not fixed by retrying, it is
131
140
  fixed by changing plan. That is why both ranges share a class.
@@ -137,7 +137,7 @@ class AccountsSocialAuthorizationMethod(TypedDict):
137
137
  """
138
138
  **How** an account of this network is authorized, which is not always "send the user to this URL".
139
139
 
140
- Nine of the eleven networks are `redirect`: open `link` and the network sends the person back to PlanVortex with a code. Two are not:
140
+ Ten of the twelve networks are `redirect`: open `link` and the network sends the person back to PlanVortex with a code. Two are not:
141
141
 
142
142
  • **WhatsApp.** Its sign-up is Meta's *Embedded Signup*: a popup raised by the Facebook JavaScript SDK from your own page, which returns — over `postMessage` — session data (`waba_id`, `phone_number_id`) that no query string carries. Its `link` is therefore an empty string.
143
143
  • **Telegram.** There is no OAuth here: no consent screen, no `code`, no account token. `link` opens a private chat with the PlanVortex bot, the person then adds that bot to their channel, and **the account is created from that event**, not from any request of yours. Which means the connection cannot be finished by calling `GET /organizations/{id_organization}/account-connect/telegram` — see that endpoint.
@@ -855,6 +855,7 @@ CommentsCommentNetworkName: TypeAlias = Literal[
855
855
  "bluesky",
856
856
  "discord",
857
857
  "telegram",
858
+ "threads",
858
859
  ]
859
860
  """
860
861
  A network that has comments. Treat it as an open list: a new one is added before your integration hears about it.
@@ -1071,7 +1072,7 @@ class Error1(TypedDict):
1071
1072
  """
1072
1073
  code: int
1073
1074
  """
1074
- PlanVortex error code. Ranges: 500-544 auth, tokens and client apps · 601-612 user · 700-715 social accounts · 800-810 files · 900-960 publications · 1000-1003 general · 1100-1111 organizations · 1200-1207 roles · 1300-1307 client plan · 1400-1408 organization plan · 1500-1512 messaging · 1600-1601 contacts · 1900-1906 payments · 2000-2099 products · 2100-2199 AI plans · 2200-2299 integrations.
1075
+ PlanVortex error code. Ranges: 500-546 auth, tokens and client apps · 601-612 user · 700-715 social accounts · 800-810 files · 900-979 publications · 1000-1003 general · 1100-1111 organizations · 1200-1207 roles · 1300-1308 client plan · 1400-1408 organization plan · 1500-1512 messaging · 1600-1601 contacts · 1900-1906 payments · 2000-2099 products · 2100-2199 AI plans · 2200-2299 integrations.
1075
1076
  """
1076
1077
  data: NotRequired[dict[str, Any]]
1077
1078
  """
@@ -1552,10 +1553,6 @@ class PlanData(TypedDict):
1552
1553
  """
1553
1554
  Social accounts that may be connected.
1554
1555
  """
1555
- publications: int
1556
- """
1557
- Publications that may be sent per month.
1558
- """
1559
1556
  users: int
1560
1557
  """
1561
1558
  Users with access.
@@ -1566,7 +1563,11 @@ class PlanData(TypedDict):
1566
1563
  """
1567
1564
  integrations: int
1568
1565
  """
1569
- Connections to a third-party tool material is pulled from (Google Drive, an RSS feed). Not the same thing as an app: an app is API access, and it is a Custom-plan feature of its own.
1566
+ Connections to a third-party tool material is pulled from (Google Drive, an RSS feed). Not the same thing as an app: an app is API access, and it has its own allowance in `apps`.
1567
+ """
1568
+ apps: NotRequired[int]
1569
+ """
1570
+ Apps this plan allows: 1 on Free, 2 on Basic, 5 on Pro, 10 on Custom. Each one is a `client_id` with a secret, so each one is a key to the whole public API — which every plan has, Free included. Unlike accounts, users or storage this is NOT split between organizations: an app belongs to the client. On a use payload it is how many exist right now.
1570
1571
  """
1571
1572
  twitter_credits: NotRequired[int]
1572
1573
  """
@@ -1590,6 +1591,17 @@ class PlanData(TypedDict):
1590
1591
  """
1591
1592
 
1592
1593
 
1594
+ class PlanUseData(PlanData):
1595
+ """
1596
+ What is being consumed right now. Same shape as PlanData plus `publications`, which is counted but neither charged nor split between organizations.
1597
+ """
1598
+
1599
+ publications: NotRequired[int]
1600
+ """
1601
+ Publications created in the current calendar month. A METRIC, not a quota: publications are unlimited on every plan, so there is no limit to compare it against. What throttles publishing is rate, not plan — see the per-hour and per-network daily caps.
1602
+ """
1603
+
1604
+
1593
1605
  class ProductsProductCatalog(TypedDict):
1594
1606
  """
1595
1607
  A Meta commerce catalogue, as the network returns it.
@@ -1668,6 +1680,8 @@ class PublicationStats(TypedDict):
1668
1680
 
1669
1681
  On `bluesky` there are no impressions and no reach either — only the public counters — so engagement is computed over followers.
1670
1682
 
1683
+ On `threads` there are six, and the one that matters is `views`: it is the only one of the four newest networks with something like impressions, so its engagement rate is computed over a real base and not over followers. Sharing arrives split in three — `reposts`, `quotes` and `shares` (outside Threads) — and the three are added into one figure, the same criterion as on X; the breakdown stays in `raw`. A reply is what every other network calls a comment.
1684
+
1671
1685
  On `telegram` there are two as well, and **neither of them is asked for**: the Bot API has no method that returns a message's metrics, so `reactions` arrives on its own through the bot and `comments` is counted in PlanVortex's own inbox. There are no impressions, no reach, no views and no forwards to be had anywhere in it, so engagement is computed over followers.
1672
1686
  """
1673
1687
 
@@ -1758,6 +1772,7 @@ class PublicationsPublicationInput(TypedDict):
1758
1772
  "bluesky",
1759
1773
  "discord",
1760
1774
  "telegram",
1775
+ "threads",
1761
1776
  ]
1762
1777
  ]
1763
1778
  """
@@ -1907,6 +1922,7 @@ SocialNetwork: TypeAlias = Literal[
1907
1922
  "bluesky",
1908
1923
  "discord",
1909
1924
  "telegram",
1925
+ "threads",
1910
1926
  ]
1911
1927
  """
1912
1928
  A social network supported by PlanVortex.
@@ -2175,7 +2191,7 @@ class CatalogPlannerTemplate(TypedDict):
2175
2191
  class CatalogSocialLimits(TypedDict):
2176
2192
  characters: CatalogSocialLimitsMap
2177
2193
  """
2178
- Maximum length of a publication's text. Bluesky counts graphemes, everyone else counts characters — Telegram included, where `String.length` is exactly the right unit.
2194
+ Maximum length of a publication's text. **Bluesky and Threads count GRAPHEMES**, everyone else counts characters — Telegram included, where `String.length` is exactly the right unit. The difference is not academic: an emoji is one grapheme and two `String.length` units, so counting a Threads post with `.length` rejects at 250 emojis what the network publishes happily at 500.
2179
2195
 
2180
2196
  On Telegram there are **two numbers for the same field**: `telegram` (4.096) while the publication is text only, and `telegram_media` (1.024) the moment it carries an image or a video, because then the text is a media caption and not a message. Switch the counter when the file is attached, not when publish is pressed.
2181
2197
  """
@@ -2394,7 +2410,7 @@ class DashboardDashboardPublicationRef(TypedDict):
2394
2410
 
2395
2411
 
2396
2412
  class DashboardPlanUse(TypedDict):
2397
- actual_use: PlanData
2413
+ actual_use: PlanUseData
2398
2414
  """
2399
2415
  What the organization and its children are consuming right now.
2400
2416
  """
@@ -2636,7 +2652,7 @@ class Organization(TypedDict):
2636
2652
  """
2637
2653
  The slice of the client's plan assigned to this organization. **Absent when nothing was assigned**, and then the organization shares whatever its nearest parent with a plan has — or, failing that, the client's unassigned remainder. Ask `GET /organizations/{id_organization}/limits` for the effective numbers instead of reading this.
2638
2654
  """
2639
- actual_use: NotRequired[PlanData]
2655
+ actual_use: NotRequired[PlanUseData]
2640
2656
  """
2641
2657
  Current consumption. **Only present with `getUse=true`.** `twitter_credits` and `ai_credits` are what has been spent in the current calendar month; the rest is what exists right now.
2642
2658
  """
@@ -2952,7 +2968,7 @@ class ClientsClient(TypedDict):
2952
2968
  """
2953
2969
  Read-only view of the client's own AI provider configuration (BYOK). Written via `PUT /clients/{id_client}/ai-settings`. API keys are stored encrypted and are NEVER returned: each scope only exposes provider, model and has_api_key.
2954
2970
  """
2955
- actual_use: NotRequired[PlanData]
2971
+ actual_use: NotRequired[PlanUseData]
2956
2972
  """
2957
2973
  Current consumption across every organization of the client. **Only present with `getUse=true`.** `twitter_credits` and `ai_credits` are what has been spent this calendar month.
2958
2974
  """
@@ -31,6 +31,7 @@ from planvortex._generated.models import (
31
31
  DashboardPublicationsSummary,
32
32
  Organization,
33
33
  PlanData,
34
+ PlanUseData,
34
35
  Publication,
35
36
  Upload,
36
37
  )
@@ -118,7 +119,7 @@ class OrganizationUse(TypedDict):
118
119
  with ``get_use=True``.
119
120
  """
120
121
 
121
- actual_use: NotRequired[PlanData]
122
+ actual_use: NotRequired[PlanUseData]
122
123
  actual_asigned: NotRequired[PlanData]
123
124
 
124
125
 
@@ -137,7 +138,7 @@ class AccountUpdate(TypedDict):
137
138
 
138
139
 
139
140
  class RedirectAuthorization(TypedDict):
140
- """Send the person to the entry's ``link``. Nine of the eleven networks.
141
+ """Send the person to the entry's ``link``. Ten of the twelve networks.
141
142
 
142
143
  One of the THREE halves of the ``authorization`` block of a connection link. The generated type
143
144
  is a single flat ``TypedDict`` with every field ``NotRequired``, because that is the only way
@@ -255,6 +256,7 @@ class EnableResult(TypedDict):
255
256
  PublishableNetwork: TypeAlias = Literal[
256
257
  "facebook",
257
258
  "instagram",
259
+ "threads",
258
260
  "twitter",
259
261
  "linkedin",
260
262
  "tiktok",
@@ -264,12 +266,12 @@ PublishableNetwork: TypeAlias = Literal[
264
266
  "discord",
265
267
  "telegram",
266
268
  ]
267
- """A network that accepts publications: the ten of the eleven that have a feed.
269
+ """A network that accepts publications: the eleven of the twelve that have a feed.
268
270
 
269
271
  ``google_business`` is the one missing, and it is not an oversight: a local listing receives
270
272
  reviews, not posts. Sending it raises error 702. It is a **narrower** type than
271
273
  ``types.SocialNetwork`` and that is the whole point — an account's ``social_network`` is one of
272
- eleven and this is one of ten, so handing one straight to the other is a type error even when a
274
+ twelve and this is one of eleven, so handing one straight to the other is a type error even when a
273
275
  ``capability="publications"`` filter has already made it impossible at runtime.
274
276
  ``types.is_publishable_network`` is what bridges it.
275
277
 
@@ -12,7 +12,7 @@ from __future__ import annotations
12
12
 
13
13
  import platform
14
14
 
15
- VERSION = "0.4.0"
15
+ VERSION = "0.6.0"
16
16
 
17
17
  # Sin barra final, y con el `/v1.0.0` dentro: la version de la API es parte de la ruta, no una
18
18
  # cabecera, asi que una `base_url` sin ella da 404 en todo.
@@ -56,7 +56,7 @@ class AsyncAccountsResource(AsyncResource):
56
56
  rather than a failure: it is what happens to Discord in an organization that has not saved
57
57
  its own bot credentials yet.
58
58
 
59
- **LOOK AT ``authorization``, NOT AT WHETHER ``link`` IS EMPTY.** Nine of the eleven networks
59
+ **LOOK AT ``authorization``, NOT AT WHETHER ``link`` IS EMPTY.** Ten of the twelve networks
60
60
  are ``redirect`` and the person is sent to ``link``. The other two are not, and neither of them
61
61
  fails visibly if you walk the list redirecting to ``link``:
62
62
 
@@ -49,7 +49,7 @@ class AccountsResource(Resource):
49
49
  rather than a failure: it is what happens to Discord in an organization that has not saved
50
50
  its own bot credentials yet.
51
51
 
52
- **LOOK AT ``authorization``, NOT AT WHETHER ``link`` IS EMPTY.** Nine of the eleven networks
52
+ **LOOK AT ``authorization``, NOT AT WHETHER ``link`` IS EMPTY.** Ten of the twelve networks
53
53
  are ``redirect`` and the person is sent to ``link``. The other two are not, and neither of them
54
54
  fails visibly if you walk the list redirecting to ``link``:
55
55
 
@@ -289,6 +289,18 @@ CAREFUL WITH THE SUM: what is assigned to a client's organizations can never exc
289
289
  has contracted — asking for more comes back as a 1400-1408.
290
290
  """
291
291
 
292
+ PlanUseData: TypeAlias = _models.PlanUseData
293
+ """What is being CONSUMED right now: a :data:`PlanData` plus ``publications``.
294
+
295
+ ``publications`` is a METRIC, not a quota — publications are unlimited on every plan — so there is
296
+ no ceiling to compare it against: do not draw it as a bar. What throttles whoever publishes too
297
+ much is RATE (a per-hour cap per account, and the per-network daily caps in ``GET /social_limits``),
298
+ not the plan.
299
+
300
+ Not to be confused with :data:`PlanUse`, which is the dashboard's whole block: consumption, what
301
+ was handed down, and the limits together.
302
+ """
303
+
292
304
  Organization: TypeAlias = _models.Organization
293
305
  """An organization: the container of accounts, publications and files. They nest.
294
306
 
@@ -502,7 +514,7 @@ for whoever is in Docker.
502
514
 
503
515
  That ``datetime`` is why this is the one shape written by hand on top of the generated type: OpenAPI
504
516
  has a ``string`` with ``format: date-time`` and no way to say "or the language's own date". Its
505
- ``social_network`` is :data:`PublishableNetwork`, which is **nine** networks and not ten.
517
+ ``social_network`` is :data:`PublishableNetwork`, which is **eleven** networks and not twelve.
506
518
  """
507
519
 
508
520
  PublicationErrorDetail: TypeAlias = _models.PublicationError
@@ -1115,6 +1127,7 @@ __all__ = [
1115
1127
  "PersistentMenu",
1116
1128
  "PlanData",
1117
1129
  "PlanUse",
1130
+ "PlanUseData",
1118
1131
  "PlannerTemplate",
1119
1132
  "PlannerTemplateField",
1120
1133
  "PlannerTemplateFieldType",
@@ -305,7 +305,19 @@ def test_cada_rango_del_catalogo_cae_en_su_clase() -> None:
305
305
  """
306
306
  assert error_class_for_code(500) is error_class_for_code(544)
307
307
  assert error_class_for_code(900).__name__ == "PublicationError"
308
+ # El rango llega hasta 979, no hasta 960: por encima del 960 estan las cuatro ultimas redes
309
+ # (Bluesky, Discord, Telegram y Threads) y los DOS FRENOS DE RITMO que sustituyeron al cupo
310
+ # mensual de publicaciones. Fuera de familia, el 978 llegaba como un error generico y quien lo
311
+ # recibia no tenia forma de saber que la respuesta es esperar.
312
+ assert error_class_for_code(975).__name__ == "PublicationError"
313
+ assert error_class_for_code(978).__name__ == "PublicationError"
314
+ assert error_class_for_code(979).__name__ == "PublicationError"
308
315
  assert error_class_for_code(1300).__name__ == "PlanLimitError"
316
+ # 1308 es el tope de APPS del plan, y 545/546 los dos codigos que trajo abrir la API publica
317
+ # a todos los planes. Los tres nacieron por encima del techo que tenian sus rangos.
318
+ assert error_class_for_code(1308).__name__ == "PlanLimitError"
319
+ assert error_class_for_code(545).__name__ == "AuthError"
320
+ assert error_class_for_code(546).__name__ == "AuthError"
309
321
  assert error_class_for_code(1408).__name__ == "PlanLimitError"
310
322
  assert error_class_for_code(2299).__name__ == "IntegrationError"
311
323
  # Los siete de las plantillas del planificador. No hay nada que registrar para que caigan aqui:
@@ -92,7 +92,7 @@ def _camino(
92
92
  url=f"{BASE_URL}/clients/cli1/organizations?limit=1",
93
93
  json={"organizations": [ORGANIZACION], "total": 1},
94
94
  )
95
- httpx_mock.add_response(url=f"{ORG}/limits", json={"accounts": 5, "publications": 100})
95
+ httpx_mock.add_response(url=f"{ORG}/limits", json={"accounts": 5, "users": 3})
96
96
  httpx_mock.add_response(
97
97
  url=f"{ORG}?getUse=true",
98
98
  json={
@@ -77,7 +77,7 @@ def _camino(httpx_mock: HTTPXMock, *, publicacion: dict[str, Any] | None = None)
77
77
  url=f"{BASE_URL}/clients/cli1/organizations?limit=1",
78
78
  json={"organizations": [ORGANIZACION], "total": 1},
79
79
  )
80
- httpx_mock.add_response(url=f"{ORG}/limits", json={"accounts": 5, "publications": 100})
80
+ httpx_mock.add_response(url=f"{ORG}/limits", json={"accounts": 5, "users": 3})
81
81
  httpx_mock.add_response(
82
82
  url=f"{ORG}?getUse=true",
83
83
  json={"organization": {**ORGANIZACION, "actual_use": {"accounts": 1, "publications": 12}}},
@@ -73,9 +73,9 @@ def test_las_hijas_se_listan_se_iteran_y_se_crean(cliente: ClienteDePrueba, http
73
73
 
74
74
  def test_limits_llega_sin_sobre(cliente: ClienteDePrueba, httpx_mock: HTTPXMock) -> None:
75
75
  """La cascada ya resuelta, y en crudo. Es lo que hay que mirar, no `actual_plan`."""
76
- httpx_mock.add_response(url=f"{ORG}/limits", json={"accounts": 5, "publications": 100})
76
+ httpx_mock.add_response(url=f"{ORG}/limits", json={"accounts": 5, "users": 3})
77
77
 
78
- assert cliente.esperar(cliente.pv.organizations.limits("org1")) == {"accounts": 5, "publications": 100}
78
+ assert cliente.esperar(cliente.pv.organizations.limits("org1")) == {"accounts": 5, "users": 3}
79
79
  assert ruta(unica(httpx_mock)) == "/organizations/org1/limits"
80
80
 
81
81
 
File without changes
File without changes
File without changes
File without changes
File without changes