planvortex 0.3.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.
- {planvortex-0.3.0 → planvortex-0.5.0}/CHANGELOG.md +56 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/PKG-INFO +1 -1
- {planvortex-0.3.0 → planvortex-0.5.0}/openapi/planvortex.openapi.json +133 -14
- {planvortex-0.3.0 → planvortex-0.5.0}/pyproject.toml +1 -1
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/_generated/models.py +15 -2
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/_shapes.py +4 -3
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/_version.py +1 -1
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/resources/accounts.py +1 -1
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/resources/ai_plans.py +84 -8
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/resources_sync/accounts.py +1 -1
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/resources_sync/ai_plans.py +77 -7
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/types.py +1 -1
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/test_ai_plans.py +45 -1
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/test_route_coverage.py +6 -2
- {planvortex-0.3.0 → planvortex-0.5.0}/.gitignore +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/LICENSE +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/README.md +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/scripts/build_docs.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/scripts/check_packaging.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/scripts/generate_models.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/scripts/generate_sync.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/scripts/route_coverage.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/__init__.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/_client.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/_client_sync.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/_core/__init__.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/_core/auth.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/_core/auth_sync.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/_core/errors.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/_core/files.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/_core/http.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/_core/http_sync.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/_core/pagination.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/_core/query.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/_core/transport.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/_generated/__init__.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/py.typed +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/resources/__init__.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/resources/apps.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/resources/base.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/resources/catalog.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/resources/clients.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/resources/comments.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/resources/contacts.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/resources/dashboard.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/resources/integrations.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/resources/messages.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/resources/organizations.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/resources/products.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/resources/publications.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/resources/uploads.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/resources_sync/__init__.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/resources_sync/apps.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/resources_sync/base.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/resources_sync/catalog.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/resources_sync/clients.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/resources_sync/comments.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/resources_sync/contacts.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/resources_sync/dashboard.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/resources_sync/integrations.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/resources_sync/messages.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/resources_sync/organizations.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/resources_sync/products.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/resources_sync/publications.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/resources_sync/uploads.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/src/planvortex/webhooks.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/__init__.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/conftest.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/contrato.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/live/__init__.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/live/conftest.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/live/test_auth.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/live/test_catalog.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/live/test_connect_flow.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/live/test_errors.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/live/test_publish.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/live/test_read.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/test_accounts.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/test_apps.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/test_catalog.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/test_client.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/test_clients.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/test_comments.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/test_contacts.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/test_core.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/test_dashboard.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/test_example_comments.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/test_example_connect.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/test_example_publish.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/test_example_schedule.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/test_example_webhooks.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/test_files.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/test_generate_models.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/test_generate_sync.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/test_integrations.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/test_messages.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/test_models.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/test_openapi_freshness.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/test_organizations.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/test_packaging.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/test_pagination.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/test_products.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/test_publications.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/test_query_parity.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/test_readme.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/test_shapes_parity.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/test_types.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/test_uploads.py +0 -0
- {planvortex-0.3.0 → planvortex-0.5.0}/tests/test_webhooks.py +0 -0
|
@@ -4,6 +4,62 @@ 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
|
+
|
|
27
|
+
## [0.4.0] - 2026-08-31
|
|
28
|
+
|
|
29
|
+
An AI plan can now be **archived**, and deleting one finally means what people thought it meant.
|
|
30
|
+
|
|
31
|
+
The two look alike on a screen and have nothing in common underneath: archiving takes the plan out
|
|
32
|
+
of the listing and touches nothing else, while deleting takes the publications that had not gone out
|
|
33
|
+
with it. Almost every time someone reaches for "remove" they wanted the first one.
|
|
34
|
+
|
|
35
|
+
Upgrading from `0.3.0` needs no changes and `MIGRATION.md` gains no entry — everything added here is
|
|
36
|
+
additive. **Read the note on `remove()` anyway**: what it deletes changed in the server, so it has
|
|
37
|
+
been happening to your `0.3.0` calls for a while and the docs that shipped with them say otherwise.
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- **`ai_plans.archive()` and `ai_plans.unarchive()`**, in both clients. Archiving is **visibility
|
|
42
|
+
only**: the plan leaves the listing and moves to the archived one, no publication is touched —
|
|
43
|
+
anything scheduled keeps publishing — no credits come back, nothing is cancelled. It therefore
|
|
44
|
+
works in **any state**, `generating` included, and it is undone. On the object it is
|
|
45
|
+
`archived_date`: a field, **not** a value of `state`, so reading the state to find out whether a
|
|
46
|
+
plan is archived will never work. Absent means active, which is how every plan created before the
|
|
47
|
+
field existed comes back.
|
|
48
|
+
- **`archived` on `ai_plans.list()` and `ai_plans.aiterate()`/`iterate()`.** `True` returns the
|
|
49
|
+
archived plans instead of the active ones, never both at once. A `False` is not sent: the server
|
|
50
|
+
switches the filter on with the literal `"true"`, so it would be noise — and it would suggest a
|
|
51
|
+
third mode with both cupboards open that does not exist.
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
|
|
55
|
+
- **`ai_plans.remove()` deletes the plan's publications that have not gone out yet**, not only the
|
|
56
|
+
drafts: if the plan had already been validated, whatever was still scheduled goes too. This is a
|
|
57
|
+
change **in the server**, so it reached your existing calls the day it shipped — before it, a
|
|
58
|
+
validated plan you deleted kept publishing by itself the following week. Two states are
|
|
59
|
+
deliberately left alone: the already published one, because deleting it here would not take it off
|
|
60
|
+
the network and would only lose its history, and the one being published at that very moment.
|
|
61
|
+
If you wanted the plan out of the way without losing anything, that is `archive()`.
|
|
62
|
+
|
|
7
63
|
## [0.3.0] - 2026-08-31
|
|
8
64
|
|
|
9
65
|
The AI planner stops being one thing. A plan can now be generated from **your own photos**, from an
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: planvortex
|
|
3
|
-
Version: 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`.**
|
|
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",
|
|
@@ -1000,7 +1000,7 @@
|
|
|
1000
1000
|
"ai_plans"
|
|
1001
1001
|
],
|
|
1002
1002
|
"summary": "List AI publication plans",
|
|
1003
|
-
"description": "Return the organization's AI plans ordered by creation date (desc), paginated. Requires the ai_plans:read permission.\n\n**Cancelled plans are not listed.** Deleting a plan sets it to `cancelled` rather than removing it, and this listing filters those out — so a plan you deleted simply stops appearing, while `GET` by id still returns it.\n\nWithout `limit` the whole list comes back.",
|
|
1003
|
+
"description": "Return the organization's AI plans ordered by creation date (desc), paginated. Requires the ai_plans:read permission.\n\n**Active plans by default, archived ones with `archived=true`** — never both at once. Archiving is visibility only (see `POST .../archive`): the plan leaves this listing and keeps every publication it had.\n\n**Cancelled plans are not listed either.** Deleting a plan sets it to `cancelled` rather than removing it, and this listing filters those out — so a plan you deleted simply stops appearing, while `GET` by id still returns it.\n\nWithout `limit` the whole list comes back.",
|
|
1004
1004
|
"operationId": "getAiPlans",
|
|
1005
1005
|
"x-planvortex-identity": [
|
|
1006
1006
|
"current_user",
|
|
@@ -1031,6 +1031,16 @@
|
|
|
1031
1031
|
"schema": {
|
|
1032
1032
|
"type": "integer"
|
|
1033
1033
|
}
|
|
1034
|
+
},
|
|
1035
|
+
{
|
|
1036
|
+
"name": "archived",
|
|
1037
|
+
"in": "query",
|
|
1038
|
+
"description": "`true` returns the archived plans instead of the active ones. Anything other than the literal string `true` is read as false.",
|
|
1039
|
+
"required": false,
|
|
1040
|
+
"schema": {
|
|
1041
|
+
"type": "boolean",
|
|
1042
|
+
"default": false
|
|
1043
|
+
}
|
|
1034
1044
|
}
|
|
1035
1045
|
],
|
|
1036
1046
|
"responses": {
|
|
@@ -1113,8 +1123,8 @@
|
|
|
1113
1123
|
"tags": [
|
|
1114
1124
|
"ai_plans"
|
|
1115
1125
|
],
|
|
1116
|
-
"summary": "
|
|
1117
|
-
"description": "
|
|
1126
|
+
"summary": "Delete an AI publication plan and its pending publications",
|
|
1127
|
+
"description": "Delete the plan **and every publication of it that has not gone out yet**: the generated drafts and, if the plan was already validated, whatever was still scheduled. Two states are deliberately left alone: `sended`, because it is already live on the network and deleting it here would only lose its history while the post stays up, and `publishing`, which the publishing job holds at that very moment.\n\nAI credits already spent on generation are NOT refunded (same criterion as X: if the provider charged, we charge). A plan being generated cannot be deleted (state `generating`, error 2102): wait for the job to finish.\n\nTo keep the plan and its publications and only take it out of the listing, archive it instead (`POST .../archive`). Requires the ai_plans:delete permission.",
|
|
1118
1128
|
"operationId": "deleteAiPlan",
|
|
1119
1129
|
"x-planvortex-identity": [
|
|
1120
1130
|
"current_user",
|
|
@@ -1258,6 +1268,106 @@
|
|
|
1258
1268
|
}
|
|
1259
1269
|
}
|
|
1260
1270
|
},
|
|
1271
|
+
"/clients/{id_client}/organizations/{id_organization}/ai_plans/{id_ai_plan}/archive": {
|
|
1272
|
+
"post": {
|
|
1273
|
+
"tags": [
|
|
1274
|
+
"ai_plans"
|
|
1275
|
+
],
|
|
1276
|
+
"summary": "Archive an AI publication plan",
|
|
1277
|
+
"description": "Take the plan out of the default listing and move it to the archived one (`GET .../ai_plans?archived=true`). It is **visibility only**: no publication is touched — anything scheduled keeps publishing — no credits are refunded, and nothing is cancelled. It is reversible with `POST .../unarchive` and valid in **any state**, `generating` included, because it does not interrupt the generation job.\n\nArchiving an already archived plan just refreshes `archived_date`. Requires the ai_plans:update permission.",
|
|
1278
|
+
"operationId": "archiveAiPlan",
|
|
1279
|
+
"x-planvortex-identity": [
|
|
1280
|
+
"current_user",
|
|
1281
|
+
"current_app"
|
|
1282
|
+
],
|
|
1283
|
+
"parameters": [
|
|
1284
|
+
{
|
|
1285
|
+
"$ref": "#/components/parameters/AiPlansidClient"
|
|
1286
|
+
},
|
|
1287
|
+
{
|
|
1288
|
+
"$ref": "#/components/parameters/AiPlansidOrganization"
|
|
1289
|
+
},
|
|
1290
|
+
{
|
|
1291
|
+
"$ref": "#/components/parameters/AiPlansidAiPlan"
|
|
1292
|
+
}
|
|
1293
|
+
],
|
|
1294
|
+
"responses": {
|
|
1295
|
+
"200": {
|
|
1296
|
+
"description": "Successful operation. Returns the plan with `archived_date` set.",
|
|
1297
|
+
"content": {
|
|
1298
|
+
"application/json": {
|
|
1299
|
+
"schema": {
|
|
1300
|
+
"$ref": "#/components/schemas/AiPlansAiPlanOne"
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
},
|
|
1305
|
+
"400": {
|
|
1306
|
+
"description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `2100` | AI plan doesn't exist. The identifier is invalid or doesn't exist. |\n| `521` | Invalid client. The identifier is invalid or doesn't exist |",
|
|
1307
|
+
"content": {
|
|
1308
|
+
"application/json": {
|
|
1309
|
+
"schema": {
|
|
1310
|
+
"$ref": "#/components/schemas/Error"
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
},
|
|
1315
|
+
"500": {
|
|
1316
|
+
"description": "Unhandled error by the server"
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
},
|
|
1321
|
+
"/clients/{id_client}/organizations/{id_organization}/ai_plans/{id_ai_plan}/unarchive": {
|
|
1322
|
+
"post": {
|
|
1323
|
+
"tags": [
|
|
1324
|
+
"ai_plans"
|
|
1325
|
+
],
|
|
1326
|
+
"summary": "Unarchive an AI publication plan",
|
|
1327
|
+
"description": "Return the plan to the default listing: `archived_date` is cleared. Unarchiving a plan that was not archived does nothing and answers 200. Requires the ai_plans:update permission.",
|
|
1328
|
+
"operationId": "unarchiveAiPlan",
|
|
1329
|
+
"x-planvortex-identity": [
|
|
1330
|
+
"current_user",
|
|
1331
|
+
"current_app"
|
|
1332
|
+
],
|
|
1333
|
+
"parameters": [
|
|
1334
|
+
{
|
|
1335
|
+
"$ref": "#/components/parameters/AiPlansidClient"
|
|
1336
|
+
},
|
|
1337
|
+
{
|
|
1338
|
+
"$ref": "#/components/parameters/AiPlansidOrganization"
|
|
1339
|
+
},
|
|
1340
|
+
{
|
|
1341
|
+
"$ref": "#/components/parameters/AiPlansidAiPlan"
|
|
1342
|
+
}
|
|
1343
|
+
],
|
|
1344
|
+
"responses": {
|
|
1345
|
+
"200": {
|
|
1346
|
+
"description": "Successful operation. Returns the plan without `archived_date`.",
|
|
1347
|
+
"content": {
|
|
1348
|
+
"application/json": {
|
|
1349
|
+
"schema": {
|
|
1350
|
+
"$ref": "#/components/schemas/AiPlansAiPlanOne"
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
},
|
|
1355
|
+
"400": {
|
|
1356
|
+
"description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `2100` | AI plan doesn't exist. The identifier is invalid or doesn't exist. |\n| `521` | Invalid client. The identifier is invalid or doesn't exist |",
|
|
1357
|
+
"content": {
|
|
1358
|
+
"application/json": {
|
|
1359
|
+
"schema": {
|
|
1360
|
+
"$ref": "#/components/schemas/Error"
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
},
|
|
1365
|
+
"500": {
|
|
1366
|
+
"description": "Unhandled error by the server"
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
},
|
|
1261
1371
|
"/clients/{id_client}/organizations/{id_organization}/ai_plans/{id_ai_plan}/publications/{id_publication}/regenerate": {
|
|
1262
1372
|
"post": {
|
|
1263
1373
|
"tags": [
|
|
@@ -1762,7 +1872,8 @@
|
|
|
1762
1872
|
"google_business",
|
|
1763
1873
|
"bluesky",
|
|
1764
1874
|
"discord",
|
|
1765
|
-
"telegram"
|
|
1875
|
+
"telegram",
|
|
1876
|
+
"threads"
|
|
1766
1877
|
]
|
|
1767
1878
|
}
|
|
1768
1879
|
}
|
|
@@ -3487,7 +3598,7 @@
|
|
|
3487
3598
|
"comments"
|
|
3488
3599
|
],
|
|
3489
3600
|
"summary": "The inbox: first-level comments across the whole organization",
|
|
3490
|
-
"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
|
|
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.",
|
|
3491
3602
|
"operationId": "getComments",
|
|
3492
3603
|
"x-planvortex-identity": [
|
|
3493
3604
|
"current_user",
|
|
@@ -3976,7 +4087,7 @@
|
|
|
3976
4087
|
"catalog"
|
|
3977
4088
|
],
|
|
3978
4089
|
"summary": "What every network supports",
|
|
3979
|
-
"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
|
|
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.",
|
|
3980
4091
|
"operationId": "getSocialCapabilities",
|
|
3981
4092
|
"x-planvortex-identity": [
|
|
3982
4093
|
"current_user",
|
|
@@ -4034,7 +4145,7 @@
|
|
|
4034
4145
|
"catalog"
|
|
4035
4146
|
],
|
|
4036
4147
|
"summary": "What each network lets you do to a comment",
|
|
4037
|
-
"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.",
|
|
4038
4149
|
"operationId": "getSocialCommentActions",
|
|
4039
4150
|
"x-planvortex-identity": [
|
|
4040
4151
|
"current_user",
|
|
@@ -9400,7 +9511,7 @@
|
|
|
9400
9511
|
"required": [
|
|
9401
9512
|
"type"
|
|
9402
9513
|
],
|
|
9403
|
-
"description": "**How** an account of this network is authorized, which is not always \"send the user to this URL\".\n\
|
|
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.",
|
|
9404
9515
|
"properties": {
|
|
9405
9516
|
"type": {
|
|
9406
9517
|
"type": "string",
|
|
@@ -9597,6 +9708,11 @@
|
|
|
9597
9708
|
],
|
|
9598
9709
|
"description": "State machine: pending -> generating -> generated -> validated | failed | cancelled. Poll the plan while state is pending or generating."
|
|
9599
9710
|
},
|
|
9711
|
+
"archived_date": {
|
|
9712
|
+
"type": "string",
|
|
9713
|
+
"format": "date-time",
|
|
9714
|
+
"description": "When the plan was archived. Absent means it is active, which is how every plan created before archiving existed comes back.\n\nIt is a field of its own and NOT a value of `state` on purpose: `state` is the generation lifecycle and archiving is orthogonal to it — a `validated` plan that already published gets archived just like a `failed` one. Inside the enum you would have to decide which state it returns to when unarchived, and that question has no answer.\n\nArchiving is visibility only: it takes the plan out of the default listing and touches no publication. Anything it had scheduled keeps publishing."
|
|
9715
|
+
},
|
|
9600
9716
|
"orchestrator_result": {
|
|
9601
9717
|
"type": "object",
|
|
9602
9718
|
"description": "Raw plan returned by the orchestrator, kept for audit (before validation/trimming)."
|
|
@@ -10399,7 +10515,7 @@
|
|
|
10399
10515
|
"$ref": "#/components/schemas/CatalogSocialLimitsMap"
|
|
10400
10516
|
}
|
|
10401
10517
|
],
|
|
10402
|
-
"description": "Maximum length of a publication's text. Bluesky
|
|
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."
|
|
10403
10519
|
},
|
|
10404
10520
|
"max_post_bytes": {
|
|
10405
10521
|
"allOf": [
|
|
@@ -11142,7 +11258,8 @@
|
|
|
11142
11258
|
"google_business",
|
|
11143
11259
|
"bluesky",
|
|
11144
11260
|
"discord",
|
|
11145
|
-
"telegram"
|
|
11261
|
+
"telegram",
|
|
11262
|
+
"threads"
|
|
11146
11263
|
]
|
|
11147
11264
|
},
|
|
11148
11265
|
"CommentsCommentThread": {
|
|
@@ -13610,7 +13727,7 @@
|
|
|
13610
13727
|
},
|
|
13611
13728
|
"PublicationStats": {
|
|
13612
13729
|
"type": "object",
|
|
13613
|
-
"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.",
|
|
13614
13731
|
"properties": {
|
|
13615
13732
|
"likes": {
|
|
13616
13733
|
"type": "integer"
|
|
@@ -13749,7 +13866,8 @@
|
|
|
13749
13866
|
"youtube",
|
|
13750
13867
|
"bluesky",
|
|
13751
13868
|
"discord",
|
|
13752
|
-
"telegram"
|
|
13869
|
+
"telegram",
|
|
13870
|
+
"threads"
|
|
13753
13871
|
],
|
|
13754
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."
|
|
13755
13873
|
},
|
|
@@ -14092,7 +14210,8 @@
|
|
|
14092
14210
|
"google_business",
|
|
14093
14211
|
"bluesky",
|
|
14094
14212
|
"discord",
|
|
14095
|
-
"telegram"
|
|
14213
|
+
"telegram",
|
|
14214
|
+
"threads"
|
|
14096
14215
|
],
|
|
14097
14216
|
"type": "string",
|
|
14098
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.
|
|
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
|
-
|
|
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
|
|
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
|
"""
|
|
@@ -2872,6 +2877,14 @@ class AiPlansAiPlan(TypedDict):
|
|
|
2872
2877
|
"""
|
|
2873
2878
|
State machine: pending -> generating -> generated -> validated | failed | cancelled. Poll the plan while state is pending or generating.
|
|
2874
2879
|
"""
|
|
2880
|
+
archived_date: NotRequired[str]
|
|
2881
|
+
"""
|
|
2882
|
+
When the plan was archived. Absent means it is active, which is how every plan created before archiving existed comes back.
|
|
2883
|
+
|
|
2884
|
+
It is a field of its own and NOT a value of `state` on purpose: `state` is the generation lifecycle and archiving is orthogonal to it — a `validated` plan that already published gets archived just like a `failed` one. Inside the enum you would have to decide which state it returns to when unarchived, and that question has no answer.
|
|
2885
|
+
|
|
2886
|
+
Archiving is visibility only: it takes the plan out of the default listing and touches no publication. Anything it had scheduled keeps publishing.
|
|
2887
|
+
"""
|
|
2875
2888
|
orchestrator_result: NotRequired[dict[str, Any]]
|
|
2876
2889
|
"""
|
|
2877
2890
|
Raw plan returned by the orchestrator, kept for audit (before validation/trimming).
|
|
@@ -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``.
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.**
|
|
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
|
|
|
@@ -22,7 +22,13 @@ AND WHAT SURPRISES PEOPLE:
|
|
|
22
22
|
so Monday/Wednesday/Friday with 3 accounts is 9 publications, not 21.
|
|
23
23
|
- **These routes hang off the CLIENT**, not only off the organization: they carry both identifiers.
|
|
24
24
|
- **Deleting is cancelling.** The plan moves to ``cancelled`` and stops appearing in the listing, but
|
|
25
|
-
:meth:`AsyncAiPlansResource.get` still returns it.
|
|
25
|
+
:meth:`AsyncAiPlansResource.get` still returns it. What does go for good are its publications that
|
|
26
|
+
had not gone out yet: the drafts and, if the plan was already validated, whatever was still
|
|
27
|
+
scheduled.
|
|
28
|
+
- **Archiving is NOT deleting, and it is the action people are usually after.**
|
|
29
|
+
:meth:`AsyncAiPlansResource.archive` takes the plan out of the listing and touches no publication
|
|
30
|
+
— anything scheduled keeps publishing — works in any state and can be undone. The archived ones
|
|
31
|
+
are read with ``list(..., archived=True)``, never next to the active ones.
|
|
26
32
|
|
|
27
33
|
THE TEMPLATES: WHAT THE PLAN IS GENERATED FROM
|
|
28
34
|
|
|
@@ -67,7 +73,7 @@ from collections.abc import AsyncIterator
|
|
|
67
73
|
|
|
68
74
|
from planvortex._core.pagination import Page, PageParams
|
|
69
75
|
from planvortex._shapes import AiPlanRegenerateResult
|
|
70
|
-
from planvortex.resources.base import AsyncResource, require_id
|
|
76
|
+
from planvortex.resources.base import AsyncResource, Query, require_id
|
|
71
77
|
from planvortex.types import AiPlan, AiPlanCreateRequest, AiPlanCreateResult
|
|
72
78
|
|
|
73
79
|
|
|
@@ -166,17 +172,27 @@ class AsyncAiPlansResource(AsyncResource):
|
|
|
166
172
|
*,
|
|
167
173
|
limit: int | None = None,
|
|
168
174
|
offset: int | None = None,
|
|
175
|
+
archived: bool | None = None,
|
|
169
176
|
timeout: float | None = None,
|
|
170
177
|
) -> Page[AiPlan]:
|
|
171
|
-
"""The organization's plans, from the newest to the oldest.
|
|
178
|
+
"""The organization's ACTIVE plans, from the newest to the oldest.
|
|
172
179
|
|
|
173
|
-
**The cancelled ones do not show up
|
|
174
|
-
|
|
180
|
+
**The cancelled ones do not show up** — nor the archived ones, which are asked for with
|
|
181
|
+
``archived=True`` — and here ``publications`` are identifiers, not the whole publications:
|
|
182
|
+
for that there is :meth:`get`. With no ``limit`` they all come back.
|
|
183
|
+
|
|
184
|
+
``archived`` opens the other cupboard, never both at once: archiving moves a plan somewhere
|
|
185
|
+
else, it does not put a label on it and leave it where it was.
|
|
186
|
+
|
|
187
|
+
.. code-block:: python
|
|
188
|
+
|
|
189
|
+
activos = await pv.ai_plans.list(client_id, org_id)
|
|
190
|
+
guardados = await pv.ai_plans.list(client_id, org_id, archived=True)
|
|
175
191
|
"""
|
|
176
192
|
pagina: Page[AiPlan] = await self._list(
|
|
177
193
|
self._path(id_client, id_organization),
|
|
178
194
|
"ai_plans",
|
|
179
|
-
|
|
195
|
+
self._list_query(limit, offset, archived),
|
|
180
196
|
timeout=timeout,
|
|
181
197
|
)
|
|
182
198
|
return pagina
|
|
@@ -188,6 +204,7 @@ class AsyncAiPlansResource(AsyncResource):
|
|
|
188
204
|
*,
|
|
189
205
|
limit: int | None = None,
|
|
190
206
|
offset: int | None = None,
|
|
207
|
+
archived: bool | None = None,
|
|
191
208
|
timeout: float | None = None,
|
|
192
209
|
) -> AsyncIterator[AiPlan]:
|
|
193
210
|
"""The organization's plans, chaining pages."""
|
|
@@ -198,6 +215,7 @@ class AsyncAiPlansResource(AsyncResource):
|
|
|
198
215
|
id_organization,
|
|
199
216
|
limit=params.limit,
|
|
200
217
|
offset=params.offset,
|
|
218
|
+
archived=archived,
|
|
201
219
|
timeout=timeout,
|
|
202
220
|
)
|
|
203
221
|
|
|
@@ -274,6 +292,47 @@ class AsyncAiPlansResource(AsyncResource):
|
|
|
274
292
|
resultado: AiPlanRegenerateResult = await self._post(ruta, {"target": target}, timeout=timeout)
|
|
275
293
|
return resultado
|
|
276
294
|
|
|
295
|
+
async def archive(
|
|
296
|
+
self,
|
|
297
|
+
id_client: str,
|
|
298
|
+
id_organization: str,
|
|
299
|
+
id_ai_plan: str,
|
|
300
|
+
*,
|
|
301
|
+
timeout: float | None = None,
|
|
302
|
+
) -> AiPlan:
|
|
303
|
+
"""Archive the plan: it leaves the listing and moves to the archived one
|
|
304
|
+
(``list(..., archived=True)``).
|
|
305
|
+
|
|
306
|
+
It is **visibility only**. No publication is touched — anything scheduled keeps publishing —
|
|
307
|
+
no credits are refunded and nothing is cancelled, so it works in ANY state, ``generating``
|
|
308
|
+
included: it does not interrupt the job. Undone with :meth:`unarchive`.
|
|
309
|
+
|
|
310
|
+
It is what people are after almost every time they think of "removing" a plan: :meth:`remove`
|
|
311
|
+
takes the publications that have not gone out with it, and this does not.
|
|
312
|
+
"""
|
|
313
|
+
plan: AiPlan = await self._post_one(
|
|
314
|
+
f"{self._one_path(id_client, id_organization, id_ai_plan)}/archive",
|
|
315
|
+
"ai_plan",
|
|
316
|
+
timeout=timeout,
|
|
317
|
+
)
|
|
318
|
+
return plan
|
|
319
|
+
|
|
320
|
+
async def unarchive(
|
|
321
|
+
self,
|
|
322
|
+
id_client: str,
|
|
323
|
+
id_organization: str,
|
|
324
|
+
id_ai_plan: str,
|
|
325
|
+
*,
|
|
326
|
+
timeout: float | None = None,
|
|
327
|
+
) -> AiPlan:
|
|
328
|
+
"""Put the plan back in the active listing. On a plan that was not archived it does nothing."""
|
|
329
|
+
plan: AiPlan = await self._post_one(
|
|
330
|
+
f"{self._one_path(id_client, id_organization, id_ai_plan)}/unarchive",
|
|
331
|
+
"ai_plan",
|
|
332
|
+
timeout=timeout,
|
|
333
|
+
)
|
|
334
|
+
return plan
|
|
335
|
+
|
|
277
336
|
async def remove(
|
|
278
337
|
self,
|
|
279
338
|
id_client: str,
|
|
@@ -282,11 +341,28 @@ class AsyncAiPlansResource(AsyncResource):
|
|
|
282
341
|
*,
|
|
283
342
|
timeout: float | None = None,
|
|
284
343
|
) -> None:
|
|
285
|
-
"""
|
|
286
|
-
|
|
344
|
+
"""Delete the plan **and its publications that have not gone out yet**: the generated drafts
|
|
345
|
+
and, if it had already been validated, whatever was still scheduled. The already published
|
|
346
|
+
ones stay — deleting them here would not take them off the network, it would only lose their
|
|
347
|
+
history — and neither is the one being published at that very moment touched.
|
|
348
|
+
|
|
349
|
+
The plan itself is not wiped: it moves to ``cancelled``, disappears from :meth:`list` and
|
|
350
|
+
:meth:`get` still returns it. Spent credits are not refunded and a ``generating`` plan cannot
|
|
351
|
+
be deleted (2102): wait for the job to finish.
|
|
352
|
+
|
|
353
|
+
To stop seeing it without losing anything, :meth:`archive`.
|
|
287
354
|
"""
|
|
288
355
|
await self._delete(self._one_path(id_client, id_organization, id_ai_plan), timeout=timeout)
|
|
289
356
|
|
|
357
|
+
def _list_query(self, limit: int | None, offset: int | None, archived: bool | None) -> Query:
|
|
358
|
+
return {
|
|
359
|
+
"limit": limit,
|
|
360
|
+
"offset": offset,
|
|
361
|
+
# El servidor enciende el filtro con el literal "true": mandar `archived=false` pediria
|
|
362
|
+
# lo mismo que no mandar nada, asi que un `False` se omite en vez de viajar como ruido.
|
|
363
|
+
"archived": True if archived else None,
|
|
364
|
+
}
|
|
365
|
+
|
|
290
366
|
def _path(self, id_client: str, id_organization: str) -> str:
|
|
291
367
|
cliente = require_id(id_client, "id_client")
|
|
292
368
|
organizacion = require_id(id_organization, "id_organization")
|