planvortex 0.4.0__tar.gz → 0.5.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.5.0}/CHANGELOG.md +20 -0
  2. {planvortex-0.4.0 → planvortex-0.5.0}/PKG-INFO +1 -1
  3. {planvortex-0.4.0 → planvortex-0.5.0}/openapi/planvortex.openapi.json +15 -11
  4. {planvortex-0.4.0 → planvortex-0.5.0}/pyproject.toml +1 -1
  5. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/_generated/models.py +7 -2
  6. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/_shapes.py +4 -3
  7. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/_version.py +1 -1
  8. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/resources/accounts.py +1 -1
  9. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/resources_sync/accounts.py +1 -1
  10. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/types.py +1 -1
  11. {planvortex-0.4.0 → planvortex-0.5.0}/.gitignore +0 -0
  12. {planvortex-0.4.0 → planvortex-0.5.0}/LICENSE +0 -0
  13. {planvortex-0.4.0 → planvortex-0.5.0}/README.md +0 -0
  14. {planvortex-0.4.0 → planvortex-0.5.0}/scripts/build_docs.py +0 -0
  15. {planvortex-0.4.0 → planvortex-0.5.0}/scripts/check_packaging.py +0 -0
  16. {planvortex-0.4.0 → planvortex-0.5.0}/scripts/generate_models.py +0 -0
  17. {planvortex-0.4.0 → planvortex-0.5.0}/scripts/generate_sync.py +0 -0
  18. {planvortex-0.4.0 → planvortex-0.5.0}/scripts/route_coverage.py +0 -0
  19. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/__init__.py +0 -0
  20. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/_client.py +0 -0
  21. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/_client_sync.py +0 -0
  22. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/_core/__init__.py +0 -0
  23. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/_core/auth.py +0 -0
  24. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/_core/auth_sync.py +0 -0
  25. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/_core/errors.py +0 -0
  26. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/_core/files.py +0 -0
  27. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/_core/http.py +0 -0
  28. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/_core/http_sync.py +0 -0
  29. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/_core/pagination.py +0 -0
  30. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/_core/query.py +0 -0
  31. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/_core/transport.py +0 -0
  32. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/_generated/__init__.py +0 -0
  33. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/py.typed +0 -0
  34. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/resources/__init__.py +0 -0
  35. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/resources/ai_plans.py +0 -0
  36. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/resources/apps.py +0 -0
  37. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/resources/base.py +0 -0
  38. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/resources/catalog.py +0 -0
  39. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/resources/clients.py +0 -0
  40. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/resources/comments.py +0 -0
  41. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/resources/contacts.py +0 -0
  42. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/resources/dashboard.py +0 -0
  43. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/resources/integrations.py +0 -0
  44. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/resources/messages.py +0 -0
  45. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/resources/organizations.py +0 -0
  46. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/resources/products.py +0 -0
  47. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/resources/publications.py +0 -0
  48. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/resources/uploads.py +0 -0
  49. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/resources_sync/__init__.py +0 -0
  50. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/resources_sync/ai_plans.py +0 -0
  51. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/resources_sync/apps.py +0 -0
  52. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/resources_sync/base.py +0 -0
  53. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/resources_sync/catalog.py +0 -0
  54. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/resources_sync/clients.py +0 -0
  55. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/resources_sync/comments.py +0 -0
  56. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/resources_sync/contacts.py +0 -0
  57. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/resources_sync/dashboard.py +0 -0
  58. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/resources_sync/integrations.py +0 -0
  59. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/resources_sync/messages.py +0 -0
  60. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/resources_sync/organizations.py +0 -0
  61. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/resources_sync/products.py +0 -0
  62. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/resources_sync/publications.py +0 -0
  63. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/resources_sync/uploads.py +0 -0
  64. {planvortex-0.4.0 → planvortex-0.5.0}/src/planvortex/webhooks.py +0 -0
  65. {planvortex-0.4.0 → planvortex-0.5.0}/tests/__init__.py +0 -0
  66. {planvortex-0.4.0 → planvortex-0.5.0}/tests/conftest.py +0 -0
  67. {planvortex-0.4.0 → planvortex-0.5.0}/tests/contrato.py +0 -0
  68. {planvortex-0.4.0 → planvortex-0.5.0}/tests/live/__init__.py +0 -0
  69. {planvortex-0.4.0 → planvortex-0.5.0}/tests/live/conftest.py +0 -0
  70. {planvortex-0.4.0 → planvortex-0.5.0}/tests/live/test_auth.py +0 -0
  71. {planvortex-0.4.0 → planvortex-0.5.0}/tests/live/test_catalog.py +0 -0
  72. {planvortex-0.4.0 → planvortex-0.5.0}/tests/live/test_connect_flow.py +0 -0
  73. {planvortex-0.4.0 → planvortex-0.5.0}/tests/live/test_errors.py +0 -0
  74. {planvortex-0.4.0 → planvortex-0.5.0}/tests/live/test_publish.py +0 -0
  75. {planvortex-0.4.0 → planvortex-0.5.0}/tests/live/test_read.py +0 -0
  76. {planvortex-0.4.0 → planvortex-0.5.0}/tests/test_accounts.py +0 -0
  77. {planvortex-0.4.0 → planvortex-0.5.0}/tests/test_ai_plans.py +0 -0
  78. {planvortex-0.4.0 → planvortex-0.5.0}/tests/test_apps.py +0 -0
  79. {planvortex-0.4.0 → planvortex-0.5.0}/tests/test_catalog.py +0 -0
  80. {planvortex-0.4.0 → planvortex-0.5.0}/tests/test_client.py +0 -0
  81. {planvortex-0.4.0 → planvortex-0.5.0}/tests/test_clients.py +0 -0
  82. {planvortex-0.4.0 → planvortex-0.5.0}/tests/test_comments.py +0 -0
  83. {planvortex-0.4.0 → planvortex-0.5.0}/tests/test_contacts.py +0 -0
  84. {planvortex-0.4.0 → planvortex-0.5.0}/tests/test_core.py +0 -0
  85. {planvortex-0.4.0 → planvortex-0.5.0}/tests/test_dashboard.py +0 -0
  86. {planvortex-0.4.0 → planvortex-0.5.0}/tests/test_example_comments.py +0 -0
  87. {planvortex-0.4.0 → planvortex-0.5.0}/tests/test_example_connect.py +0 -0
  88. {planvortex-0.4.0 → planvortex-0.5.0}/tests/test_example_publish.py +0 -0
  89. {planvortex-0.4.0 → planvortex-0.5.0}/tests/test_example_schedule.py +0 -0
  90. {planvortex-0.4.0 → planvortex-0.5.0}/tests/test_example_webhooks.py +0 -0
  91. {planvortex-0.4.0 → planvortex-0.5.0}/tests/test_files.py +0 -0
  92. {planvortex-0.4.0 → planvortex-0.5.0}/tests/test_generate_models.py +0 -0
  93. {planvortex-0.4.0 → planvortex-0.5.0}/tests/test_generate_sync.py +0 -0
  94. {planvortex-0.4.0 → planvortex-0.5.0}/tests/test_integrations.py +0 -0
  95. {planvortex-0.4.0 → planvortex-0.5.0}/tests/test_messages.py +0 -0
  96. {planvortex-0.4.0 → planvortex-0.5.0}/tests/test_models.py +0 -0
  97. {planvortex-0.4.0 → planvortex-0.5.0}/tests/test_openapi_freshness.py +0 -0
  98. {planvortex-0.4.0 → planvortex-0.5.0}/tests/test_organizations.py +0 -0
  99. {planvortex-0.4.0 → planvortex-0.5.0}/tests/test_packaging.py +0 -0
  100. {planvortex-0.4.0 → planvortex-0.5.0}/tests/test_pagination.py +0 -0
  101. {planvortex-0.4.0 → planvortex-0.5.0}/tests/test_products.py +0 -0
  102. {planvortex-0.4.0 → planvortex-0.5.0}/tests/test_publications.py +0 -0
  103. {planvortex-0.4.0 → planvortex-0.5.0}/tests/test_query_parity.py +0 -0
  104. {planvortex-0.4.0 → planvortex-0.5.0}/tests/test_readme.py +0 -0
  105. {planvortex-0.4.0 → planvortex-0.5.0}/tests/test_route_coverage.py +0 -0
  106. {planvortex-0.4.0 → planvortex-0.5.0}/tests/test_shapes_parity.py +0 -0
  107. {planvortex-0.4.0 → planvortex-0.5.0}/tests/test_types.py +0 -0
  108. {planvortex-0.4.0 → planvortex-0.5.0}/tests/test_uploads.py +0 -0
  109. {planvortex-0.4.0 → planvortex-0.5.0}/tests/test_webhooks.py +0 -0
@@ -4,6 +4,26 @@ 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.5.0] - 2026-09-02
8
+
9
+ **Threads is the twelfth network**, and the types now know it.
10
+
11
+ Nothing here removes anything: `SocialNetwork` gains a value, `PublishableNetwork` gains another,
12
+ and everything that type-checked before type-checks now. What was wrong until today was quieter than
13
+ an error — the committed copy of the spec predated Threads, so an account coming back from a real
14
+ API arrived with a `social_network` no `Literal` had ever heard of, and the prose still counted
15
+ eleven networks where the API had twelve.
16
+
17
+ ### Changed
18
+
19
+ - The OpenAPI copy and the generated models are rebuilt from the published document, so `threads`
20
+ is a `SocialNetwork`, a `CommentNetwork` and a publishable network.
21
+ - `PublishableNetwork` — hand-maintained, because generating it is not possible — includes
22
+ `threads`: **eleven of the twelve** networks have a feed, and `google_business` is still the one
23
+ that does not.
24
+ - The prose that counts networks: a connection link is a `redirect` on **ten of the twelve**, not
25
+ nine of eleven. WhatsApp and Telegram are still the two that are not.
26
+
7
27
  ## [0.4.0] - 2026-08-31
8
28
 
9
29
  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.5.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/
@@ -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
  }
@@ -3597,7 +3598,7 @@
3597
3598
  "comments"
3598
3599
  ],
3599
3600
  "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.",
3601
+ "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
3602
  "operationId": "getComments",
3602
3603
  "x-planvortex-identity": [
3603
3604
  "current_user",
@@ -4086,7 +4087,7 @@
4086
4087
  "catalog"
4087
4088
  ],
4088
4089
  "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.",
4090
+ "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
4091
  "operationId": "getSocialCapabilities",
4091
4092
  "x-planvortex-identity": [
4092
4093
  "current_user",
@@ -4144,7 +4145,7 @@
4144
4145
  "catalog"
4145
4146
  ],
4146
4147
  "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.",
4148
+ "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
4149
  "operationId": "getSocialCommentActions",
4149
4150
  "x-planvortex-identity": [
4150
4151
  "current_user",
@@ -9510,7 +9511,7 @@
9510
9511
  "required": [
9511
9512
  "type"
9512
9513
  ],
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.",
9514
+ "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
9515
  "properties": {
9515
9516
  "type": {
9516
9517
  "type": "string",
@@ -10514,7 +10515,7 @@
10514
10515
  "$ref": "#/components/schemas/CatalogSocialLimitsMap"
10515
10516
  }
10516
10517
  ],
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."
10518
+ "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
10519
  },
10519
10520
  "max_post_bytes": {
10520
10521
  "allOf": [
@@ -11257,7 +11258,8 @@
11257
11258
  "google_business",
11258
11259
  "bluesky",
11259
11260
  "discord",
11260
- "telegram"
11261
+ "telegram",
11262
+ "threads"
11261
11263
  ]
11262
11264
  },
11263
11265
  "CommentsCommentThread": {
@@ -13725,7 +13727,7 @@
13725
13727
  },
13726
13728
  "PublicationStats": {
13727
13729
  "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.",
13730
+ "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
13731
  "properties": {
13730
13732
  "likes": {
13731
13733
  "type": "integer"
@@ -13864,7 +13866,8 @@
13864
13866
  "youtube",
13865
13867
  "bluesky",
13866
13868
  "discord",
13867
- "telegram"
13869
+ "telegram",
13870
+ "threads"
13868
13871
  ],
13869
13872
  "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
13873
  },
@@ -14207,7 +14210,8 @@
14207
14210
  "google_business",
14208
14211
  "bluesky",
14209
14212
  "discord",
14210
- "telegram"
14213
+ "telegram",
14214
+ "threads"
14211
14215
  ],
14212
14216
  "type": "string",
14213
14217
  "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.5.0"
13
13
  description = "Official Python client for the PlanVortex API"
14
14
  readme = "README.md"
15
15
  requires-python = ">=3.10"
@@ -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.
@@ -1668,6 +1669,8 @@ class PublicationStats(TypedDict):
1668
1669
 
1669
1670
  On `bluesky` there are no impressions and no reach either — only the public counters — so engagement is computed over followers.
1670
1671
 
1672
+ 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.
1673
+
1671
1674
  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
1675
  """
1673
1676
 
@@ -1758,6 +1761,7 @@ class PublicationsPublicationInput(TypedDict):
1758
1761
  "bluesky",
1759
1762
  "discord",
1760
1763
  "telegram",
1764
+ "threads",
1761
1765
  ]
1762
1766
  ]
1763
1767
  """
@@ -1907,6 +1911,7 @@ SocialNetwork: TypeAlias = Literal[
1907
1911
  "bluesky",
1908
1912
  "discord",
1909
1913
  "telegram",
1914
+ "threads",
1910
1915
  ]
1911
1916
  """
1912
1917
  A social network supported by PlanVortex.
@@ -2175,7 +2180,7 @@ class CatalogPlannerTemplate(TypedDict):
2175
2180
  class CatalogSocialLimits(TypedDict):
2176
2181
  characters: CatalogSocialLimitsMap
2177
2182
  """
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.
2183
+ 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
2184
 
2180
2185
  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
2186
  """
@@ -137,7 +137,7 @@ class AccountUpdate(TypedDict):
137
137
 
138
138
 
139
139
  class RedirectAuthorization(TypedDict):
140
- """Send the person to the entry's ``link``. Nine of the eleven networks.
140
+ """Send the person to the entry's ``link``. Ten of the twelve networks.
141
141
 
142
142
  One of the THREE halves of the ``authorization`` block of a connection link. The generated type
143
143
  is a single flat ``TypedDict`` with every field ``NotRequired``, because that is the only way
@@ -255,6 +255,7 @@ class EnableResult(TypedDict):
255
255
  PublishableNetwork: TypeAlias = Literal[
256
256
  "facebook",
257
257
  "instagram",
258
+ "threads",
258
259
  "twitter",
259
260
  "linkedin",
260
261
  "tiktok",
@@ -264,12 +265,12 @@ PublishableNetwork: TypeAlias = Literal[
264
265
  "discord",
265
266
  "telegram",
266
267
  ]
267
- """A network that accepts publications: the ten of the eleven that have a feed.
268
+ """A network that accepts publications: the eleven of the twelve that have a feed.
268
269
 
269
270
  ``google_business`` is the one missing, and it is not an oversight: a local listing receives
270
271
  reviews, not posts. Sending it raises error 702. It is a **narrower** type than
271
272
  ``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
273
+ twelve and this is one of eleven, so handing one straight to the other is a type error even when a
273
274
  ``capability="publications"`` filter has already made it impossible at runtime.
274
275
  ``types.is_publishable_network`` is what bridges it.
275
276
 
@@ -12,7 +12,7 @@ from __future__ import annotations
12
12
 
13
13
  import platform
14
14
 
15
- VERSION = "0.4.0"
15
+ VERSION = "0.5.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
 
@@ -502,7 +502,7 @@ for whoever is in Docker.
502
502
 
503
503
  That ``datetime`` is why this is the one shape written by hand on top of the generated type: OpenAPI
504
504
  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.
505
+ ``social_network`` is :data:`PublishableNetwork`, which is **eleven** networks and not twelve.
506
506
  """
507
507
 
508
508
  PublicationErrorDetail: TypeAlias = _models.PublicationError
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes