planvortex 0.13.0__tar.gz → 0.14.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.
- {planvortex-0.13.0 → planvortex-0.14.0}/CHANGELOG.md +27 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/PKG-INFO +2 -2
- {planvortex-0.13.0 → planvortex-0.14.0}/README.md +1 -1
- {planvortex-0.13.0 → planvortex-0.14.0}/openapi/planvortex.openapi.json +110 -9
- {planvortex-0.13.0 → planvortex-0.14.0}/pyproject.toml +1 -1
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/_core/errors.py +8 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/_generated/models.py +85 -1
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/_shapes.py +12 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/_version.py +1 -1
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/resources/accounts.py +3 -1
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/resources/comments.py +4 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/resources_sync/accounts.py +3 -1
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/types.py +6 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/test_core.py +5 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/.gitignore +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/LICENSE +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/scripts/build_docs.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/scripts/check_packaging.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/scripts/generate_models.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/scripts/generate_sync.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/scripts/route_coverage.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/__init__.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/_client.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/_client_sync.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/_core/__init__.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/_core/auth.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/_core/auth_sync.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/_core/files.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/_core/http.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/_core/http_sync.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/_core/pagination.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/_core/query.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/_core/transport.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/_generated/__init__.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/py.typed +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/resources/__init__.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/resources/ai_plans.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/resources/apps.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/resources/base.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/resources/catalog.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/resources/clients.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/resources/contacts.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/resources/dashboard.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/resources/integrations.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/resources/messages.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/resources/organizations.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/resources/products.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/resources/publications.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/resources/uploads.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/resources_sync/__init__.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/resources_sync/ai_plans.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/resources_sync/apps.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/resources_sync/base.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/resources_sync/catalog.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/resources_sync/clients.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/resources_sync/comments.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/resources_sync/contacts.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/resources_sync/dashboard.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/resources_sync/integrations.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/resources_sync/messages.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/resources_sync/organizations.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/resources_sync/products.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/resources_sync/publications.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/resources_sync/uploads.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/src/planvortex/webhooks.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/__init__.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/conftest.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/contrato.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/live/__init__.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/live/conftest.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/live/test_auth.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/live/test_catalog.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/live/test_connect_flow.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/live/test_errors.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/live/test_publish.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/live/test_read.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/test_accounts.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/test_ai_plans.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/test_apps.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/test_catalog.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/test_client.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/test_clients.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/test_comments.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/test_contacts.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/test_dashboard.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/test_example_comments.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/test_example_connect.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/test_example_publish.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/test_example_schedule.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/test_example_webhooks.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/test_files.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/test_generate_models.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/test_generate_sync.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/test_integrations.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/test_messages.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/test_models.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/test_openapi_freshness.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/test_organizations.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/test_packaging.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/test_pagination.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/test_products.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/test_publications.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/test_query_parity.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/test_readme.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/test_route_coverage.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/test_shapes_parity.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/test_types.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/test_uploads.py +0 -0
- {planvortex-0.13.0 → planvortex-0.14.0}/tests/test_webhooks.py +0 -0
|
@@ -4,6 +4,33 @@ 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.14.0] - 2026-10-07
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **LinkedIn personal profiles.** Connecting LinkedIn now returns the personal profile of whoever
|
|
12
|
+
authorizes as well as the pages they manage, and the profile can be enabled like any other
|
|
13
|
+
account. It publishes in that person's name and has statistics, but **no comment inbox**:
|
|
14
|
+
LinkedIn does not let any app read the comments on a member's posts, so every `comments` call on
|
|
15
|
+
a profile raises error 2600. `Account` now types `extra_data["is_personal_profile"]` (`True` on
|
|
16
|
+
the profile, `False` on a page, absent on pages connected before profiles existed), and
|
|
17
|
+
`accounts.list(..., capability="comments")` leaves profiles out.
|
|
18
|
+
- **`visibility` and `made_for_kids` on publications**, today on YouTube only: who can see the
|
|
19
|
+
video when it goes out (`public`, `unlisted` or `private`; absent is `public`) and whether it
|
|
20
|
+
is made for kids (COPPA; absent is `false`). Both are typed on `PublicationInput` and on
|
|
21
|
+
`Publication`. They are the user's choice, so ask them if your software publishes for someone. A
|
|
22
|
+
value the network does not take does not fail the request: the publication is created in
|
|
23
|
+
`withErrors` with code 2500 (visibility) or 2501 (not a boolean). On every other network the
|
|
24
|
+
fields are deleted on save; ask `catalog.social_capabilities()` for `visibility` and
|
|
25
|
+
`made_for_kids` rather than checking for YouTube.
|
|
26
|
+
- **`publication_warnings` on `Publication`**: what still has to be done by hand on the network
|
|
27
|
+
for a publication that did go out. Same shape as `publication_errors`, but it is not a failure:
|
|
28
|
+
the publication stays `sended` and **must not be retried**, or it goes out twice. Today the only
|
|
29
|
+
code is 2400: YouTube accepted the video but left it as a draft or private, and `data.studio_url`
|
|
30
|
+
is the YouTube Studio page where it is finished.
|
|
31
|
+
- Error 2600 is raised as a `PublicationError`, the same class as 945 ("this network has no
|
|
32
|
+
comments"), of which it is the per-account version. It used to fall back to the base class.
|
|
33
|
+
|
|
7
34
|
## [0.13.0] - 2026-10-03
|
|
8
35
|
|
|
9
36
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: planvortex
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.0
|
|
4
4
|
Summary: Official Python client for the PlanVortex API
|
|
5
5
|
Project-URL: Homepage, https://planvortex.com/en/developers
|
|
6
6
|
Project-URL: Documentation, https://taliasoftworks.github.io/PlanVortexPython/
|
|
@@ -280,7 +280,7 @@ memorising numbers:
|
|
|
280
280
|
| 601-612 | `user` | `UserError` |
|
|
281
281
|
| 700-716 | `account` | `AccountError` |
|
|
282
282
|
| 800-810 | `file` | `FileError` |
|
|
283
|
-
| 900-996 | `publication` | `PublicationError` |
|
|
283
|
+
| 900-996, 2600-2699 | `publication` | `PublicationError` |
|
|
284
284
|
| 1000-1003 | `general` | `PlanVortexError` |
|
|
285
285
|
| 1100-1111 | `organization` | `OrganizationError` |
|
|
286
286
|
| 1200-1207 | `role` | `PlanVortexError` |
|
|
@@ -250,7 +250,7 @@ memorising numbers:
|
|
|
250
250
|
| 601-612 | `user` | `UserError` |
|
|
251
251
|
| 700-716 | `account` | `AccountError` |
|
|
252
252
|
| 800-810 | `file` | `FileError` |
|
|
253
|
-
| 900-996 | `publication` | `PublicationError` |
|
|
253
|
+
| 900-996, 2600-2699 | `publication` | `PublicationError` |
|
|
254
254
|
| 1000-1003 | `general` | `PlanVortexError` |
|
|
255
255
|
| 1100-1111 | `organization` | `OrganizationError` |
|
|
256
256
|
| 1200-1207 | `role` | `PlanVortexError` |
|
|
@@ -273,7 +273,7 @@
|
|
|
273
273
|
{
|
|
274
274
|
"name": "capability",
|
|
275
275
|
"in": "query",
|
|
276
|
-
"description": "Only accounts whose network can do this. It is the same matrix `GET /social_capabilities` publishes, applied server-side: the way to ask for 'the accounts I can publish with' without keeping your own table of which network does what.",
|
|
276
|
+
"description": "Only accounts whose network can do this. It is the same matrix `GET /social_capabilities` publishes, applied server-side: the way to ask for 'the accounts I can publish with' without keeping your own table of which network does what.\n\n`comments` also looks at each account, not only at its network: a LinkedIn **personal profile** is left out, because only LinkedIn pages have a comment inbox. `total` counts the same way.",
|
|
277
277
|
"required": false,
|
|
278
278
|
"explode": true,
|
|
279
279
|
"schema": {
|
|
@@ -5627,7 +5627,9 @@
|
|
|
5627
5627
|
"comments": true,
|
|
5628
5628
|
"destinations": false,
|
|
5629
5629
|
"link": false,
|
|
5630
|
-
"reply_control": false
|
|
5630
|
+
"reply_control": false,
|
|
5631
|
+
"visibility": false,
|
|
5632
|
+
"made_for_kids": false
|
|
5631
5633
|
},
|
|
5632
5634
|
"google_business": {
|
|
5633
5635
|
"publications": false,
|
|
@@ -5638,7 +5640,9 @@
|
|
|
5638
5640
|
"comments": true,
|
|
5639
5641
|
"destinations": false,
|
|
5640
5642
|
"link": false,
|
|
5641
|
-
"reply_control": false
|
|
5643
|
+
"reply_control": false,
|
|
5644
|
+
"visibility": false,
|
|
5645
|
+
"made_for_kids": false
|
|
5642
5646
|
},
|
|
5643
5647
|
"pinterest": {
|
|
5644
5648
|
"publications": true,
|
|
@@ -5649,7 +5653,9 @@
|
|
|
5649
5653
|
"comments": false,
|
|
5650
5654
|
"destinations": true,
|
|
5651
5655
|
"link": true,
|
|
5652
|
-
"reply_control": false
|
|
5656
|
+
"reply_control": false,
|
|
5657
|
+
"visibility": false,
|
|
5658
|
+
"made_for_kids": false
|
|
5653
5659
|
},
|
|
5654
5660
|
"threads": {
|
|
5655
5661
|
"publications": true,
|
|
@@ -5660,7 +5666,9 @@
|
|
|
5660
5666
|
"comments": true,
|
|
5661
5667
|
"destinations": false,
|
|
5662
5668
|
"link": false,
|
|
5663
|
-
"reply_control": true
|
|
5669
|
+
"reply_control": true,
|
|
5670
|
+
"visibility": false,
|
|
5671
|
+
"made_for_kids": false
|
|
5664
5672
|
}
|
|
5665
5673
|
}
|
|
5666
5674
|
}
|
|
@@ -12602,7 +12610,7 @@
|
|
|
12602
12610
|
"schemas": {
|
|
12603
12611
|
"Account": {
|
|
12604
12612
|
"type": "object",
|
|
12605
|
-
"description": "A social account connected to an organization.\n\nOn `discord`, on `telegram` and on `slack` an account is a **channel**, not a profile: publishing to two Discord channels of the same server — or to two Telegram channels of the same brand, or to `#anuncios` and `#general` of the same Slack workspace — costs two accounts of the plan.\n\n`error_code` other than `0` means the connection is broken — an expired token, a permission taken away — and the account has to be connected again. On `telegram` nothing expires, because there is no account token: what breaks the connection is the bot being removed from the channel or losing its permission to post there (error 968). On `slack` the bot token does not expire either: what breaks it is the app being removed from the channel (error 980) or the channel being archived or deleted (error 985).",
|
|
12613
|
+
"description": "A social account connected to an organization.\n\nOn `discord`, on `telegram` and on `slack` an account is a **channel**, not a profile: publishing to two Discord channels of the same server — or to two Telegram channels of the same brand, or to `#anuncios` and `#general` of the same Slack workspace — costs two accounts of the plan.\n\nOn `linkedin` one authorization brings two kinds of account: the **personal profile** of whoever authorizes and each **page** they manage. Both publish and both have statistics, but only pages have a comment inbox: LinkedIn does not let any app read the comments on a member's posts, so on a profile every comment endpoint returns error `2600`. `extra_data.is_personal_profile` tells them apart.\n\n`error_code` other than `0` means the connection is broken — an expired token, a permission taken away — and the account has to be connected again. On `telegram` nothing expires, because there is no account token: what breaks the connection is the bot being removed from the channel or losing its permission to post there (error 968). On `slack` the bot token does not expire either: what breaks it is the app being removed from the channel (error 980) or the channel being archived or deleted (error 985).",
|
|
12606
12614
|
"required": [
|
|
12607
12615
|
"_id",
|
|
12608
12616
|
"id_organization",
|
|
@@ -12668,6 +12676,17 @@
|
|
|
12668
12676
|
"private_message_link": {
|
|
12669
12677
|
"type": "string",
|
|
12670
12678
|
"description": "A link that opens a private chat with this account (`m.me`, `ig.me`, `wa.me`). Absent on every other network."
|
|
12679
|
+
},
|
|
12680
|
+
"extra_data": {
|
|
12681
|
+
"type": "object",
|
|
12682
|
+
"description": "What only makes sense on this account's network. Only the keys documented here are part of the contract: anything else in the object can change without notice.",
|
|
12683
|
+
"properties": {
|
|
12684
|
+
"is_personal_profile": {
|
|
12685
|
+
"type": "boolean",
|
|
12686
|
+
"description": "`linkedin` only. `true` on the personal profile of whoever authorized, `false` on a page. A profile publishes and has statistics but no comment inbox (error `2600`). Pages connected before personal profiles existed do not carry it, so treat absent as a page."
|
|
12687
|
+
}
|
|
12688
|
+
},
|
|
12689
|
+
"additionalProperties": true
|
|
12671
12690
|
}
|
|
12672
12691
|
}
|
|
12673
12692
|
},
|
|
@@ -15259,6 +15278,14 @@
|
|
|
15259
15278
|
"reply_control": {
|
|
15260
15279
|
"type": "boolean",
|
|
15261
15280
|
"description": "The publication can say **who can reply** to it (`reply_control`): anyone, the account's followers, the profiles it follows or the profiles mentioned in the text. It is set when the publication goes out and cannot be changed afterwards. On a network that answers `false` the field is deleted on save, so do not offer it. Today only `threads`."
|
|
15281
|
+
},
|
|
15282
|
+
"visibility": {
|
|
15283
|
+
"type": "boolean",
|
|
15284
|
+
"description": "The publication can say **who can see it** when it goes out (`visibility`): public, unlisted or private. On a network that answers `false` the field is deleted on save, so do not offer it. Today only `youtube`."
|
|
15285
|
+
},
|
|
15286
|
+
"made_for_kids": {
|
|
15287
|
+
"type": "boolean",
|
|
15288
|
+
"description": "The publication can say whether it is **made for kids** (`made_for_kids`, COPPA). On a network that answers `false` the field is deleted on save, so do not offer it. Today only `youtube`."
|
|
15262
15289
|
}
|
|
15263
15290
|
},
|
|
15264
15291
|
"required": [
|
|
@@ -15270,7 +15297,9 @@
|
|
|
15270
15297
|
"comments",
|
|
15271
15298
|
"destinations",
|
|
15272
15299
|
"link",
|
|
15273
|
-
"reply_control"
|
|
15300
|
+
"reply_control",
|
|
15301
|
+
"visibility",
|
|
15302
|
+
"made_for_kids"
|
|
15274
15303
|
]
|
|
15275
15304
|
},
|
|
15276
15305
|
"CommentsWebhookChange": {
|
|
@@ -15957,6 +15986,29 @@
|
|
|
15957
15986
|
}
|
|
15958
15987
|
}
|
|
15959
15988
|
}
|
|
15989
|
+
},
|
|
15990
|
+
"publication_warnings": {
|
|
15991
|
+
"type": "array",
|
|
15992
|
+
"description": "What still has to be done **by hand on the network** for a publication that DID go out. Same shape as `publication_errors`, and an empty array when there is nothing to do.\n\nIt is a separate field because these are not failures: the publication stays in state `sended` and must **not** be retried, because retrying would publish it twice. Like `publication_errors`, it belongs to the last attempt.\n\nToday there is one code. **2400** (YouTube): YouTube accepted the upload but did not make the video public, so YouTube Studio shows it as a draft or private until someone sets its audience and visibility there. The upload does not fail when this happens; PlanVortex reads the video back right after uploading it to find out. `data` carries `requested_privacy_status`, `privacy_status` (what YouTube actually left), `upload_status`, `made_for_kids` and `studio_url`, the YouTube Studio page where it is finished.",
|
|
15993
|
+
"items": {
|
|
15994
|
+
"type": "object",
|
|
15995
|
+
"required": [
|
|
15996
|
+
"code",
|
|
15997
|
+
"message"
|
|
15998
|
+
],
|
|
15999
|
+
"properties": {
|
|
16000
|
+
"code": {
|
|
16001
|
+
"type": "integer"
|
|
16002
|
+
},
|
|
16003
|
+
"message": {
|
|
16004
|
+
"type": "string"
|
|
16005
|
+
},
|
|
16006
|
+
"data": {
|
|
16007
|
+
"type": "object",
|
|
16008
|
+
"additionalProperties": true
|
|
16009
|
+
}
|
|
16010
|
+
}
|
|
16011
|
+
}
|
|
15960
16012
|
}
|
|
15961
16013
|
}
|
|
15962
16014
|
},
|
|
@@ -16221,7 +16273,7 @@
|
|
|
16221
16273
|
},
|
|
16222
16274
|
"code": {
|
|
16223
16275
|
"type": "integer",
|
|
16224
|
-
"description": "PlanVortex error code. Ranges: 500-554 auth, tokens, client apps and connect sessions · 601-612 user · 700-716 social accounts · 800-810 files · 900-996 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."
|
|
16276
|
+
"description": "PlanVortex error code. Ranges: 500-554 auth, tokens, client apps and connect sessions · 601-612 user · 700-716 social accounts · 800-810 files · 900-996 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 · 2600-2699 comments, the ones that no longer fit in 945-948."
|
|
16225
16277
|
},
|
|
16226
16278
|
"data": {
|
|
16227
16279
|
"additionalProperties": true,
|
|
@@ -17792,6 +17844,19 @@
|
|
|
17792
17844
|
],
|
|
17793
17845
|
"description": "**Who can reply** to the publication, on the networks that answer `reply_control: true` in `GET /social_capabilities` (today `threads`). Absent means the network's default, which is anyone. It is set when the publication goes out and cannot be changed afterwards.\n\n**On every other network the field is deleted on save**, like `link`."
|
|
17794
17846
|
},
|
|
17847
|
+
"visibility": {
|
|
17848
|
+
"type": "string",
|
|
17849
|
+
"enum": [
|
|
17850
|
+
"public",
|
|
17851
|
+
"unlisted",
|
|
17852
|
+
"private"
|
|
17853
|
+
],
|
|
17854
|
+
"description": "**Who can see the publication** when it goes out, on the networks that answer `visibility: true` in `GET /social_capabilities` (today `youtube`). Absent means `public`. It is set when the publication goes out.\n\n**On every other network the field is deleted on save**, like `reply_control`."
|
|
17855
|
+
},
|
|
17856
|
+
"made_for_kids": {
|
|
17857
|
+
"type": "boolean",
|
|
17858
|
+
"description": "Whether the video is **made for kids** (COPPA), on the networks that answer `made_for_kids: true` in `GET /social_capabilities` (today `youtube`). Absent means `false`.\n\n**On every other network the field is deleted on save**, like `reply_control`."
|
|
17859
|
+
},
|
|
17795
17860
|
"publication_type": {
|
|
17796
17861
|
"type": "string",
|
|
17797
17862
|
"enum": [
|
|
@@ -17855,6 +17920,29 @@
|
|
|
17855
17920
|
}
|
|
17856
17921
|
}
|
|
17857
17922
|
},
|
|
17923
|
+
"publication_warnings": {
|
|
17924
|
+
"type": "array",
|
|
17925
|
+
"description": "What still has to be done **by hand on the network** for a publication that DID go out. Same shape as `publication_errors`, and an empty array when there is nothing to do.\n\nIt is a separate field because these are not failures: the publication stays in state `sended` and must **not** be retried, because retrying would publish it twice. Like `publication_errors`, it belongs to the last attempt.\n\nToday there is one code. **2400** (YouTube): YouTube accepted the upload but did not make the video public, so YouTube Studio shows it as a draft or private until someone sets its audience and visibility there. The upload does not fail when this happens; PlanVortex reads the video back right after uploading it to find out. `data` carries `requested_privacy_status`, `privacy_status` (what YouTube actually left), `upload_status`, `made_for_kids` and `studio_url`, the YouTube Studio page where it is finished.",
|
|
17926
|
+
"items": {
|
|
17927
|
+
"type": "object",
|
|
17928
|
+
"required": [
|
|
17929
|
+
"code",
|
|
17930
|
+
"message"
|
|
17931
|
+
],
|
|
17932
|
+
"properties": {
|
|
17933
|
+
"code": {
|
|
17934
|
+
"type": "integer"
|
|
17935
|
+
},
|
|
17936
|
+
"message": {
|
|
17937
|
+
"type": "string"
|
|
17938
|
+
},
|
|
17939
|
+
"data": {
|
|
17940
|
+
"type": "object",
|
|
17941
|
+
"additionalProperties": true
|
|
17942
|
+
}
|
|
17943
|
+
}
|
|
17944
|
+
}
|
|
17945
|
+
},
|
|
17858
17946
|
"retries": {
|
|
17859
17947
|
"type": "integer",
|
|
17860
17948
|
"description": "Manual retries already spent on a failed publication, against the `max_retries` published by `GET /publication_limits`. Only `POST .../retry` increases it; updating the publication resets it to 0."
|
|
@@ -18216,6 +18304,19 @@
|
|
|
18216
18304
|
],
|
|
18217
18305
|
"description": "**Who can reply** to the publication. Only on the networks that answer `reply_control: true` in `GET /social_capabilities` (today `threads`), and **deleted** on every other one.\n\n- `everyone`: anyone. It is the network's default, and what applies when the field is omitted.\n- `followers_only`: only the account's followers.\n- `accounts_you_follow`: only the profiles the account follows.\n- `mentioned_only`: only the profiles mentioned in the text.\n\n**It is set when the publication goes out and cannot be changed afterwards**: Threads has no way to change it on a published post through its API. A value the network does not accept does not fail the request: the publication is still created, in state `withErrors` with `publication_errors[].code = 997`, whose `data` carries the `allowed` values. It is checked when the publication is **created**, so a scheduled post does not fail at 3 a.m. over a value that was already wrong.\n\nOn an update: omit it to keep what was there, send `null` or `\"\"` to go back to the network's default."
|
|
18218
18306
|
},
|
|
18307
|
+
"visibility": {
|
|
18308
|
+
"type": "string",
|
|
18309
|
+
"enum": [
|
|
18310
|
+
"public",
|
|
18311
|
+
"unlisted",
|
|
18312
|
+
"private"
|
|
18313
|
+
],
|
|
18314
|
+
"description": "**Who can see the publication** when it goes out. Only on the networks that answer `visibility: true` in `GET /social_capabilities` (today `youtube`), and **deleted** on every other one.\n\n- `public`: anyone. It is what applies when the field is omitted.\n- `unlisted`: only people with the link. It does not show on the channel or in search.\n- `private`: only the channel owner and the people they invite from YouTube Studio.\n\nYouTube's policies require this to be **the user's choice**: if your software publishes on behalf of someone, ask them. A value the network does not accept does not fail the request: the publication is still created, in state `withErrors` with `publication_errors[].code = 2500`, whose `data` carries the `allowed` values. It is checked when the publication is **created**, so a scheduled video does not fail at 3 a.m. over a value that was already wrong.\n\nIf YouTube does not leave the video with the visibility that was asked for, the publication still ends up `sended` (the video is on the channel) and carries a warning in `publication_warnings` with `code = 2400`.\n\nOn an update: omit it to keep what was there, send `null` or `\"\"` to go back to `public`."
|
|
18315
|
+
},
|
|
18316
|
+
"made_for_kids": {
|
|
18317
|
+
"type": "boolean",
|
|
18318
|
+
"description": "Whether the video is **made for kids**. Only on the networks that answer `made_for_kids: true` in `GET /social_capabilities` (today `youtube`), and **deleted** on every other one. Absent means `false`.\n\nThe law (COPPA) and YouTube's policies require a video directed at children to be uploaded with `true`, and it is the **user's** call: if your software publishes on behalf of someone, ask them. YouTube then turns off features such as comments and personalised ads on that video.\n\nIt must be a JSON boolean. Anything else (`\"yes\"`, `\"no\"`, `1`) is not guessed: the publication is still created, in state `withErrors` with `publication_errors[].code = 2501`.\n\nOn an update: omit it to keep what was there, send `null` or `\"\"` to go back to `false`."
|
|
18319
|
+
},
|
|
18219
18320
|
"files": {
|
|
18220
18321
|
"type": "array",
|
|
18221
18322
|
"description": "Identifiers of uploads previously created through the uploads endpoints, attached to this publication.\n\n**On Slack the files travel inside the message**, not as publications of their own: up to 10 attachments counting images and videos together (`publication_errors[].code = 982` over it), each one under the `max_file_size_mb.slack` ceiling (code 983), and anything the upload itself refuses comes back as code 986.\n\n**On Pinterest a pin is an image or a video, never text alone** (code 922 with neither), and it is of one kind only: two to five images make a carousel — outside that range, code 990 — images and videos are not mixed and there is no more than one video (code 916). The weight ceilings are two orders of magnitude apart, so `max_file_size_mb.pinterest` is the **image** one (~20 MB) while a video is allowed far more; over either, code 996 with `data.max_mb`.",
|
|
@@ -19130,7 +19231,7 @@
|
|
|
19130
19231
|
}
|
|
19131
19232
|
},
|
|
19132
19233
|
"CommentsCommentError": {
|
|
19133
|
-
"description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `516` | Comments require a paid plan. Every endpoint here returns this on the free plan. |\n| `700` | The account is not connected — no valid token, or nothing to ask the network about. |\n| `936` | The publication was never sent, so it has no thread to read. |\n| `940` | The client's monthly X credits are exhausted. Only X. |\n| `945` | This network has no comments (TikTok, WhatsApp). Carries `social_network` in `data`. |\n| `946` | The network has comments but does not allow **this action**: hiding on LinkedIn or Google Business, deleting someone else's on Instagram or X, deleting a review. Carries `social_network` and `action` in `data`. See `GET /social_comment_actions`. |\n| `947` | The comment no longer exists on the network. What is stored is a photograph; the network always wins. Carries `external_id` in `data`. |\n| `948` | The reply is empty or longer than the network allows. Carries the limit in `data.max`; the same number is published in `comment_characters` of `GET /social_limits`. |\n| `951` | The Google Business listing is not verified, so it cannot reply to its reviews. It is the state of the customer's profile, not of your token. |\n| `952` | PlanVortex's Google Cloud project has no approved access to the Google Business API yet. Until it does, reviews cannot be read or replied to. |\n| `965` | This Telegram channel has no linked discussion group, so it has no comments. The network gate says `true`; what is missing is a setting of **that channel**, which its owner fixes in two taps. Carries `chat_id` in `data`. |\n| `969` | The PlanVortex bot is not allowed to delete messages in this Telegram discussion group. The network allows it; this installation does not. Carries `chat_id` in `data`. |\n| `970` | Telegram is rate limiting the bot and the wait was longer than PlanVortex is willing to hold the request for. Carries `retry_after_seconds` in `data`: retry after it. |\n| `1101` | Invalid organization. |\n| `1501` | The comment's account could not be resolved. |",
|
|
19234
|
+
"description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `516` | Comments require a paid plan. Every endpoint here returns this on the free plan. |\n| `700` | The account is not connected — no valid token, or nothing to ask the network about. |\n| `936` | The publication was never sent, so it has no thread to read. |\n| `940` | The client's monthly X credits are exhausted. Only X. |\n| `945` | This network has no comments (TikTok, WhatsApp). Carries `social_network` in `data`. |\n| `946` | The network has comments but does not allow **this action**: hiding on LinkedIn or Google Business, deleting someone else's on Instagram or X, deleting a review. Carries `social_network` and `action` in `data`. See `GET /social_comment_actions`. |\n| `947` | The comment no longer exists on the network. What is stored is a photograph; the network always wins. Carries `external_id` in `data`. |\n| `948` | The reply is empty or longer than the network allows. Carries the limit in `data.max`; the same number is published in `comment_characters` of `GET /social_limits`. |\n| `951` | The Google Business listing is not verified, so it cannot reply to its reviews. It is the state of the customer's profile, not of your token. |\n| `952` | PlanVortex's Google Cloud project has no approved access to the Google Business API yet. Until it does, reviews cannot be read or replied to. |\n| `965` | This Telegram channel has no linked discussion group, so it has no comments. The network gate says `true`; what is missing is a setting of **that channel**, which its owner fixes in two taps. Carries `chat_id` in `data`. |\n| `969` | The PlanVortex bot is not allowed to delete messages in this Telegram discussion group. The network allows it; this installation does not. Carries `chat_id` in `data`. |\n| `970` | Telegram is rate limiting the bot and the wait was longer than PlanVortex is willing to hold the request for. Carries `retry_after_seconds` in `data`: retry after it. |\n| `1101` | Invalid organization. |\n| `1501` | The comment's account could not be resolved. |\n| `2600` | The network has comments but **this account** has none: a LinkedIn personal profile. LinkedIn does not let any app read a member's comments, so only pages have an inbox. Carries `social_network` and `id_account` in `data`. |",
|
|
19134
19235
|
"content": {
|
|
19135
19236
|
"application/json": {
|
|
19136
19237
|
"schema": {
|
|
@@ -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.
|
|
12
|
+
version = "0.14.0"
|
|
13
13
|
description = "Official Python client for the PlanVortex API"
|
|
14
14
|
readme = "README.md"
|
|
15
15
|
requires-python = ">=3.10"
|
|
@@ -56,6 +56,10 @@ PLANVORTEX_ERROR_RANGES: tuple[ErrorRange, ...] = (
|
|
|
56
56
|
ErrorRange(2000, 2099, "product"),
|
|
57
57
|
ErrorRange(2100, 2199, "ai_plan"),
|
|
58
58
|
ErrorRange(2200, 2299, "integration"),
|
|
59
|
+
# Los COMENTARIOS siguen aqui porque sus 945-948 se llenaron, y van con ellos a `publication`:
|
|
60
|
+
# el 2600 (esta cuenta no tiene bandeja, un perfil personal de LinkedIn) es la version por
|
|
61
|
+
# cuenta del 945, y separarlos daria dos clases para el mismo «aqui no hay comentarios».
|
|
62
|
+
ErrorRange(2600, 2699, "publication"),
|
|
59
63
|
)
|
|
60
64
|
|
|
61
65
|
# El `code` que lleva un error que NO trae codigo del servidor: un fallo de red, un timeout, un 502
|
|
@@ -139,6 +143,10 @@ class PublicationError(PlanVortexError):
|
|
|
139
143
|
the channel, and Pinterest's (987-996), which start the same way: 987, the publication does not
|
|
140
144
|
say which board it goes to. One of them is not a failure: 991 is Pinterest throttling the
|
|
141
145
|
application, it arrives as a 429 with ``Retry-After`` and the answer is to wait.
|
|
146
|
+
|
|
147
|
+
Also 2600-2699, the comment codes that did not fit after 945-948: 2600 is the per-account
|
|
148
|
+
version of 945, an account with no comment inbox on a network that has one (a LinkedIn
|
|
149
|
+
personal profile).
|
|
142
150
|
"""
|
|
143
151
|
|
|
144
152
|
|
|
@@ -17,6 +17,18 @@ if sys.version_info >= (3, 11):
|
|
|
17
17
|
else: # pragma: no cover - la rama la elige el interprete, no un test
|
|
18
18
|
from typing_extensions import NotRequired, TypedDict
|
|
19
19
|
|
|
20
|
+
|
|
21
|
+
class ExtraData(TypedDict):
|
|
22
|
+
"""
|
|
23
|
+
What only makes sense on this account's network. Only the keys documented here are part of the contract: anything else in the object can change without notice.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
is_personal_profile: NotRequired[bool]
|
|
27
|
+
"""
|
|
28
|
+
`linkedin` only. `true` on the personal profile of whoever authorized, `false` on a page. A profile publishes and has statistics but no comment inbox (error `2600`). Pages connected before personal profiles existed do not carry it, so treat absent as a page.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
|
|
20
32
|
AccountsMetricList: TypeAlias = list[
|
|
21
33
|
Literal[
|
|
22
34
|
"page_total_actions",
|
|
@@ -1003,6 +1015,14 @@ class CommentsSocialCapabilities(TypedDict):
|
|
|
1003
1015
|
"""
|
|
1004
1016
|
The publication can say **who can reply** to it (`reply_control`): anyone, the account's followers, the profiles it follows or the profiles mentioned in the text. It is set when the publication goes out and cannot be changed afterwards. On a network that answers `false` the field is deleted on save, so do not offer it. Today only `threads`.
|
|
1005
1017
|
"""
|
|
1018
|
+
visibility: bool
|
|
1019
|
+
"""
|
|
1020
|
+
The publication can say **who can see it** when it goes out (`visibility`): public, unlisted or private. On a network that answers `false` the field is deleted on save, so do not offer it. Today only `youtube`.
|
|
1021
|
+
"""
|
|
1022
|
+
made_for_kids: bool
|
|
1023
|
+
"""
|
|
1024
|
+
The publication can say whether it is **made for kids** (`made_for_kids`, COPPA). On a network that answers `false` the field is deleted on save, so do not offer it. Today only `youtube`.
|
|
1025
|
+
"""
|
|
1006
1026
|
|
|
1007
1027
|
|
|
1008
1028
|
ContactChannel: TypeAlias = Literal[
|
|
@@ -1123,6 +1143,12 @@ class PublicationError(TypedDict):
|
|
|
1123
1143
|
data: NotRequired[dict[str, Any]]
|
|
1124
1144
|
|
|
1125
1145
|
|
|
1146
|
+
class PublicationWarning(TypedDict):
|
|
1147
|
+
code: int
|
|
1148
|
+
message: str
|
|
1149
|
+
data: NotRequired[dict[str, Any]]
|
|
1150
|
+
|
|
1151
|
+
|
|
1126
1152
|
class DashboardDashboardRange(TypedDict):
|
|
1127
1153
|
"""
|
|
1128
1154
|
The range that was actually used, plus the previous period of exactly the same length. The previous one is not "last month": comparing 30 days against a calendar month would move the delta with the calendar.
|
|
@@ -1186,7 +1212,7 @@ class Error1(TypedDict):
|
|
|
1186
1212
|
"""
|
|
1187
1213
|
code: int
|
|
1188
1214
|
"""
|
|
1189
|
-
PlanVortex error code. Ranges: 500-554 auth, tokens, client apps and connect sessions · 601-612 user · 700-716 social accounts · 800-810 files · 900-996 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.
|
|
1215
|
+
PlanVortex error code. Ranges: 500-554 auth, tokens, client apps and connect sessions · 601-612 user · 700-716 social accounts · 800-810 files · 900-996 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 · 2600-2699 comments, the ones that no longer fit in 945-948.
|
|
1190
1216
|
"""
|
|
1191
1217
|
data: NotRequired[dict[str, Any]]
|
|
1192
1218
|
"""
|
|
@@ -2172,6 +2198,30 @@ class PublicationsPublicationInput(TypedDict):
|
|
|
2172
2198
|
|
|
2173
2199
|
On an update: omit it to keep what was there, send `null` or `""` to go back to the network's default.
|
|
2174
2200
|
"""
|
|
2201
|
+
visibility: NotRequired[Literal["public", "unlisted", "private"]]
|
|
2202
|
+
"""
|
|
2203
|
+
**Who can see the publication** when it goes out. Only on the networks that answer `visibility: true` in `GET /social_capabilities` (today `youtube`), and **deleted** on every other one.
|
|
2204
|
+
|
|
2205
|
+
- `public`: anyone. It is what applies when the field is omitted.
|
|
2206
|
+
- `unlisted`: only people with the link. It does not show on the channel or in search.
|
|
2207
|
+
- `private`: only the channel owner and the people they invite from YouTube Studio.
|
|
2208
|
+
|
|
2209
|
+
YouTube's policies require this to be **the user's choice**: if your software publishes on behalf of someone, ask them. A value the network does not accept does not fail the request: the publication is still created, in state `withErrors` with `publication_errors[].code = 2500`, whose `data` carries the `allowed` values. It is checked when the publication is **created**, so a scheduled video does not fail at 3 a.m. over a value that was already wrong.
|
|
2210
|
+
|
|
2211
|
+
If YouTube does not leave the video with the visibility that was asked for, the publication still ends up `sended` (the video is on the channel) and carries a warning in `publication_warnings` with `code = 2400`.
|
|
2212
|
+
|
|
2213
|
+
On an update: omit it to keep what was there, send `null` or `""` to go back to `public`.
|
|
2214
|
+
"""
|
|
2215
|
+
made_for_kids: NotRequired[bool]
|
|
2216
|
+
"""
|
|
2217
|
+
Whether the video is **made for kids**. Only on the networks that answer `made_for_kids: true` in `GET /social_capabilities` (today `youtube`), and **deleted** on every other one. Absent means `false`.
|
|
2218
|
+
|
|
2219
|
+
The law (COPPA) and YouTube's policies require a video directed at children to be uploaded with `true`, and it is the **user's** call: if your software publishes on behalf of someone, ask them. YouTube then turns off features such as comments and personalised ads on that video.
|
|
2220
|
+
|
|
2221
|
+
It must be a JSON boolean. Anything else (`"yes"`, `"no"`, `1`) is not guessed: the publication is still created, in state `withErrors` with `publication_errors[].code = 2501`.
|
|
2222
|
+
|
|
2223
|
+
On an update: omit it to keep what was there, send `null` or `""` to go back to `false`.
|
|
2224
|
+
"""
|
|
2175
2225
|
files: NotRequired[list[str]]
|
|
2176
2226
|
"""
|
|
2177
2227
|
Identifiers of uploads previously created through the uploads endpoints, attached to this publication.
|
|
@@ -2399,6 +2449,8 @@ class Account(TypedDict):
|
|
|
2399
2449
|
|
|
2400
2450
|
On `discord`, on `telegram` and on `slack` an account is a **channel**, not a profile: publishing to two Discord channels of the same server — or to two Telegram channels of the same brand, or to `#anuncios` and `#general` of the same Slack workspace — costs two accounts of the plan.
|
|
2401
2451
|
|
|
2452
|
+
On `linkedin` one authorization brings two kinds of account: the **personal profile** of whoever authorizes and each **page** they manage. Both publish and both have statistics, but only pages have a comment inbox: LinkedIn does not let any app read the comments on a member's posts, so on a profile every comment endpoint returns error `2600`. `extra_data.is_personal_profile` tells them apart.
|
|
2453
|
+
|
|
2402
2454
|
`error_code` other than `0` means the connection is broken — an expired token, a permission taken away — and the account has to be connected again. On `telegram` nothing expires, because there is no account token: what breaks the connection is the bot being removed from the channel or losing its permission to post there (error 968). On `slack` the bot token does not expire either: what breaks it is the app being removed from the channel (error 980) or the channel being archived or deleted (error 985).
|
|
2403
2455
|
"""
|
|
2404
2456
|
|
|
@@ -2445,6 +2497,10 @@ class Account(TypedDict):
|
|
|
2445
2497
|
"""
|
|
2446
2498
|
A link that opens a private chat with this account (`m.me`, `ig.me`, `wa.me`). Absent on every other network.
|
|
2447
2499
|
"""
|
|
2500
|
+
extra_data: NotRequired[ExtraData]
|
|
2501
|
+
"""
|
|
2502
|
+
What only makes sense on this account's network. Only the keys documented here are part of the contract: anything else in the object can change without notice.
|
|
2503
|
+
"""
|
|
2448
2504
|
|
|
2449
2505
|
|
|
2450
2506
|
class AccountsAccountList(TypedDict):
|
|
@@ -2993,6 +3049,14 @@ class DashboardDashboardPublicationRef(TypedDict):
|
|
|
2993
3049
|
"""
|
|
2994
3050
|
Only on the failed ones. Same shape as in a full `Publication`.
|
|
2995
3051
|
"""
|
|
3052
|
+
publication_warnings: NotRequired[list[PublicationWarning]]
|
|
3053
|
+
"""
|
|
3054
|
+
What still has to be done **by hand on the network** for a publication that DID go out. Same shape as `publication_errors`, and an empty array when there is nothing to do.
|
|
3055
|
+
|
|
3056
|
+
It is a separate field because these are not failures: the publication stays in state `sended` and must **not** be retried, because retrying would publish it twice. Like `publication_errors`, it belongs to the last attempt.
|
|
3057
|
+
|
|
3058
|
+
Today there is one code. **2400** (YouTube): YouTube accepted the upload but did not make the video public, so YouTube Studio shows it as a draft or private until someone sets its audience and visibility there. The upload does not fail when this happens; PlanVortex reads the video back right after uploading it to find out. `data` carries `requested_privacy_status`, `privacy_status` (what YouTube actually left), `upload_status`, `made_for_kids` and `studio_url`, the YouTube Studio page where it is finished.
|
|
3059
|
+
"""
|
|
2996
3060
|
|
|
2997
3061
|
|
|
2998
3062
|
class DashboardPlanUse(TypedDict):
|
|
@@ -3375,6 +3439,18 @@ class Publication(TypedDict):
|
|
|
3375
3439
|
|
|
3376
3440
|
**On every other network the field is deleted on save**, like `link`.
|
|
3377
3441
|
"""
|
|
3442
|
+
visibility: NotRequired[Literal["public", "unlisted", "private"]]
|
|
3443
|
+
"""
|
|
3444
|
+
**Who can see the publication** when it goes out, on the networks that answer `visibility: true` in `GET /social_capabilities` (today `youtube`). Absent means `public`. It is set when the publication goes out.
|
|
3445
|
+
|
|
3446
|
+
**On every other network the field is deleted on save**, like `reply_control`.
|
|
3447
|
+
"""
|
|
3448
|
+
made_for_kids: NotRequired[bool]
|
|
3449
|
+
"""
|
|
3450
|
+
Whether the video is **made for kids** (COPPA), on the networks that answer `made_for_kids: true` in `GET /social_capabilities` (today `youtube`). Absent means `false`.
|
|
3451
|
+
|
|
3452
|
+
**On every other network the field is deleted on save**, like `reply_control`.
|
|
3453
|
+
"""
|
|
3378
3454
|
publication_type: Literal["profile", "page", "group", "reels", "stories", "message"]
|
|
3379
3455
|
state: Literal["ready", "withErrors", "sended", "draft", "publishing"]
|
|
3380
3456
|
"""
|
|
@@ -3397,6 +3473,14 @@ class Publication(TypedDict):
|
|
|
3397
3473
|
|
|
3398
3474
|
On Instagram, two codes tell you whether a retry makes sense. **998**: Meta rejected the media while processing it (a codec, a duration, a URL it could not download). `data` carries `container_id`, `status_code` and, when Meta gives one, its reason in `status`. Retrying the same file fails the same way: change it first. **999**: Meta had not finished processing the media 10 minutes after it was sent. `data` carries `container_ids` and `minutes`. The file is not the problem, and a retry usually works.
|
|
3399
3475
|
"""
|
|
3476
|
+
publication_warnings: NotRequired[list[PublicationWarning]]
|
|
3477
|
+
"""
|
|
3478
|
+
What still has to be done **by hand on the network** for a publication that DID go out. Same shape as `publication_errors`, and an empty array when there is nothing to do.
|
|
3479
|
+
|
|
3480
|
+
It is a separate field because these are not failures: the publication stays in state `sended` and must **not** be retried, because retrying would publish it twice. Like `publication_errors`, it belongs to the last attempt.
|
|
3481
|
+
|
|
3482
|
+
Today there is one code. **2400** (YouTube): YouTube accepted the upload but did not make the video public, so YouTube Studio shows it as a draft or private until someone sets its audience and visibility there. The upload does not fail when this happens; PlanVortex reads the video back right after uploading it to find out. `data` carries `requested_privacy_status`, `privacy_status` (what YouTube actually left), `upload_status`, `made_for_kids` and `studio_url`, the YouTube Studio page where it is finished.
|
|
3483
|
+
"""
|
|
3400
3484
|
retries: int
|
|
3401
3485
|
"""
|
|
3402
3486
|
Manual retries already spent on a failed publication, against the `max_retries` published by `GET /publication_limits`. Only `POST .../retry` increases it; updating the publication resets it to 0.
|
|
@@ -347,6 +347,18 @@ class PublicationInput(TypedDict):
|
|
|
347
347
|
``catalog.social_capabilities()`` (today Threads). Omitted, the network's default: anyone. It is
|
|
348
348
|
set when the publication goes out and cannot be changed afterwards; on every other network the
|
|
349
349
|
field is deleted on save."""
|
|
350
|
+
visibility: NotRequired[Literal["public", "unlisted", "private"]]
|
|
351
|
+
"""**Who can see the publication** when it goes out, on the networks that answer
|
|
352
|
+
``visibility: true`` in ``catalog.social_capabilities()`` (today YouTube). Omitted, ``public``.
|
|
353
|
+
YouTube's policies require it to be the user's choice: if you publish for someone, ask them. A
|
|
354
|
+
value the network does not take does not raise: the publication is created in ``withErrors``
|
|
355
|
+
with code 2500. On every other network the field is deleted on save."""
|
|
356
|
+
made_for_kids: NotRequired[bool]
|
|
357
|
+
"""Whether the video is **made for kids** (COPPA), on the networks that answer
|
|
358
|
+
``made_for_kids: true`` in ``catalog.social_capabilities()`` (today YouTube). Omitted, ``False``.
|
|
359
|
+
It is the user's call, and it must be a real boolean: anything else is not guessed, and the
|
|
360
|
+
publication is created in ``withErrors`` with code 2501. On every other network the field is
|
|
361
|
+
deleted on save."""
|
|
350
362
|
name: NotRequired[str]
|
|
351
363
|
"""Internal name, for grouping. Never shown on the network."""
|
|
352
364
|
publication_type: NotRequired[Literal["profile", "page", "group", "reels", "stories"]]
|
|
@@ -12,7 +12,7 @@ from __future__ import annotations
|
|
|
12
12
|
|
|
13
13
|
import platform
|
|
14
14
|
|
|
15
|
-
VERSION = "0.
|
|
15
|
+
VERSION = "0.14.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.
|
|
@@ -181,7 +181,9 @@ class AsyncAccountsResource(AsyncResource):
|
|
|
181
181
|
``capability`` filters by what the account's network can do — ``publications``, ``messages``,
|
|
182
182
|
``products``, ``webhooks``, ``persistent_menu``, ``comments`` — applied on the server with
|
|
183
183
|
the same matrix ``catalog.social_capabilities()`` publishes. It is how you ask for "the
|
|
184
|
-
accounts I can publish with" without keeping a table of your own.
|
|
184
|
+
accounts I can publish with" without keeping a table of your own. ``comments`` also looks
|
|
185
|
+
at each account: it leaves out LinkedIn personal profiles, which have no inbox although
|
|
186
|
+
LinkedIn pages do.
|
|
185
187
|
"""
|
|
186
188
|
pagina: Page[Account] = await self._list(
|
|
187
189
|
f"{self._organization_path(id_organization)}/accounts",
|
|
@@ -16,6 +16,10 @@ WHAT TO KNOW BEFORE CALLING ANYTHING HERE:
|
|
|
16
16
|
:meth:`AsyncCommentsResource.actions`: Instagram, X and Bluesky do not let you delete somebody
|
|
17
17
|
else's, LinkedIn has no "hide" (neither do Discord and Telegram), and Google Business only lets
|
|
18
18
|
you delete **your own reply**.
|
|
19
|
+
- **Nor every ACCOUNT.** A LinkedIn personal profile publishes but has no inbox: LinkedIn does not
|
|
20
|
+
let any app read the comments on a profile, only on a page. Everything here raises error 2600 on
|
|
21
|
+
one. Check ``account.get("extra_data", {}).get("is_personal_profile")`` first, or list the accounts with
|
|
22
|
+
``capability="comments"``, which already leaves them out.
|
|
19
23
|
- **On Telegram the inbox starts the day the channel was connected.** The Bot API cannot read the
|
|
20
24
|
past: a bot only learns what happens while it is inside, so nothing written before the connection
|
|
21
25
|
exists here and never will. Say so in your interface — an inbox that opens empty on a busy channel
|
|
@@ -172,7 +172,9 @@ class AccountsResource(Resource):
|
|
|
172
172
|
``capability`` filters by what the account's network can do — ``publications``, ``messages``,
|
|
173
173
|
``products``, ``webhooks``, ``persistent_menu``, ``comments`` — applied on the server with
|
|
174
174
|
the same matrix ``catalog.social_capabilities()`` publishes. It is how you ask for "the
|
|
175
|
-
accounts I can publish with" without keeping a table of your own.
|
|
175
|
+
accounts I can publish with" without keeping a table of your own. ``comments`` also looks
|
|
176
|
+
at each account: it leaves out LinkedIn personal profiles, which have no inbox although
|
|
177
|
+
LinkedIn pages do.
|
|
176
178
|
"""
|
|
177
179
|
pagina: Page[Account] = self._list(
|
|
178
180
|
f"{self._organization_path(id_organization)}/accounts",
|
|
@@ -481,6 +481,12 @@ Account: TypeAlias = _models.Account
|
|
|
481
481
|
On Discord an account is a **channel**, not a profile: publishing to two channels of the same server
|
|
482
482
|
spends two accounts of the plan. ``error_code`` other than ``0`` means the connection broke — expired
|
|
483
483
|
token, permission taken away — and it has to be connected again.
|
|
484
|
+
|
|
485
|
+
On LinkedIn one authorization brings two kinds of account: the **personal profile** of whoever
|
|
486
|
+
authorizes and each **page** they manage. Both publish and have statistics, but only pages have a
|
|
487
|
+
comment inbox: LinkedIn does not let any app read the comments on a personal profile, so every
|
|
488
|
+
``comments`` call on one raises error 2600. Tell them apart with ``extra_data["is_personal_profile"]``;
|
|
489
|
+
absent means a page (those connected before profiles existed).
|
|
484
490
|
"""
|
|
485
491
|
|
|
486
492
|
AccountMetrics: TypeAlias = _models.AccountsMetricModel
|
|
@@ -355,6 +355,11 @@ def test_cada_rango_del_catalogo_cae_en_su_clase() -> None:
|
|
|
355
355
|
assert error_class_for_code(546).__name__ == "AuthError"
|
|
356
356
|
assert error_class_for_code(1408).__name__ == "PlanLimitError"
|
|
357
357
|
assert error_class_for_code(2299).__name__ == "IntegrationError"
|
|
358
|
+
# Los comentarios que no cupieron en 945-948. El 2600 es la version por CUENTA del 945 (un
|
|
359
|
+
# perfil personal de LinkedIn no tiene bandeja) y sale con la misma clase que el.
|
|
360
|
+
assert error_class_for_code(945).__name__ == "PublicationError"
|
|
361
|
+
assert error_class_for_code(2600).__name__ == "PublicationError"
|
|
362
|
+
assert error_class_for_code(2699).__name__ == "PublicationError"
|
|
358
363
|
# Los siete de las plantillas del planificador. No hay nada que registrar para que caigan aqui:
|
|
359
364
|
# entran por el rango 2100-2199, y eso es justo lo que hay que comprobar en vez de suponerlo.
|
|
360
365
|
for codigo in range(2111, 2118):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|