planvortex 0.3.0__tar.gz → 0.4.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.4.0}/CHANGELOG.md +36 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/PKG-INFO +1 -1
- {planvortex-0.3.0 → planvortex-0.4.0}/openapi/planvortex.openapi.json +118 -3
- {planvortex-0.3.0 → planvortex-0.4.0}/pyproject.toml +1 -1
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/_generated/models.py +8 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/_version.py +1 -1
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/resources/ai_plans.py +84 -8
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/resources_sync/ai_plans.py +77 -7
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/test_ai_plans.py +45 -1
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/test_route_coverage.py +6 -2
- {planvortex-0.3.0 → planvortex-0.4.0}/.gitignore +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/LICENSE +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/README.md +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/scripts/build_docs.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/scripts/check_packaging.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/scripts/generate_models.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/scripts/generate_sync.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/scripts/route_coverage.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/__init__.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/_client.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/_client_sync.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/_core/__init__.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/_core/auth.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/_core/auth_sync.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/_core/errors.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/_core/files.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/_core/http.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/_core/http_sync.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/_core/pagination.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/_core/query.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/_core/transport.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/_generated/__init__.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/_shapes.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/py.typed +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/resources/__init__.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/resources/accounts.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/resources/apps.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/resources/base.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/resources/catalog.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/resources/clients.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/resources/comments.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/resources/contacts.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/resources/dashboard.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/resources/integrations.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/resources/messages.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/resources/organizations.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/resources/products.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/resources/publications.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/resources/uploads.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/resources_sync/__init__.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/resources_sync/accounts.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/resources_sync/apps.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/resources_sync/base.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/resources_sync/catalog.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/resources_sync/clients.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/resources_sync/comments.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/resources_sync/contacts.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/resources_sync/dashboard.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/resources_sync/integrations.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/resources_sync/messages.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/resources_sync/organizations.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/resources_sync/products.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/resources_sync/publications.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/resources_sync/uploads.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/types.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/src/planvortex/webhooks.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/__init__.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/conftest.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/contrato.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/live/__init__.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/live/conftest.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/live/test_auth.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/live/test_catalog.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/live/test_connect_flow.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/live/test_errors.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/live/test_publish.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/live/test_read.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/test_accounts.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/test_apps.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/test_catalog.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/test_client.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/test_clients.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/test_comments.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/test_contacts.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/test_core.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/test_dashboard.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/test_example_comments.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/test_example_connect.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/test_example_publish.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/test_example_schedule.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/test_example_webhooks.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/test_files.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/test_generate_models.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/test_generate_sync.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/test_integrations.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/test_messages.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/test_models.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/test_openapi_freshness.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/test_organizations.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/test_packaging.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/test_pagination.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/test_products.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/test_publications.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/test_query_parity.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/test_readme.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/test_shapes_parity.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/test_types.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/test_uploads.py +0 -0
- {planvortex-0.3.0 → planvortex-0.4.0}/tests/test_webhooks.py +0 -0
|
@@ -4,6 +4,42 @@ 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.4.0] - 2026-08-31
|
|
8
|
+
|
|
9
|
+
An AI plan can now be **archived**, and deleting one finally means what people thought it meant.
|
|
10
|
+
|
|
11
|
+
The two look alike on a screen and have nothing in common underneath: archiving takes the plan out
|
|
12
|
+
of the listing and touches nothing else, while deleting takes the publications that had not gone out
|
|
13
|
+
with it. Almost every time someone reaches for "remove" they wanted the first one.
|
|
14
|
+
|
|
15
|
+
Upgrading from `0.3.0` needs no changes and `MIGRATION.md` gains no entry — everything added here is
|
|
16
|
+
additive. **Read the note on `remove()` anyway**: what it deletes changed in the server, so it has
|
|
17
|
+
been happening to your `0.3.0` calls for a while and the docs that shipped with them say otherwise.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- **`ai_plans.archive()` and `ai_plans.unarchive()`**, in both clients. Archiving is **visibility
|
|
22
|
+
only**: the plan leaves the listing and moves to the archived one, no publication is touched —
|
|
23
|
+
anything scheduled keeps publishing — no credits come back, nothing is cancelled. It therefore
|
|
24
|
+
works in **any state**, `generating` included, and it is undone. On the object it is
|
|
25
|
+
`archived_date`: a field, **not** a value of `state`, so reading the state to find out whether a
|
|
26
|
+
plan is archived will never work. Absent means active, which is how every plan created before the
|
|
27
|
+
field existed comes back.
|
|
28
|
+
- **`archived` on `ai_plans.list()` and `ai_plans.aiterate()`/`iterate()`.** `True` returns the
|
|
29
|
+
archived plans instead of the active ones, never both at once. A `False` is not sent: the server
|
|
30
|
+
switches the filter on with the literal `"true"`, so it would be noise — and it would suggest a
|
|
31
|
+
third mode with both cupboards open that does not exist.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- **`ai_plans.remove()` deletes the plan's publications that have not gone out yet**, not only the
|
|
36
|
+
drafts: if the plan had already been validated, whatever was still scheduled goes too. This is a
|
|
37
|
+
change **in the server**, so it reached your existing calls the day it shipped — before it, a
|
|
38
|
+
validated plan you deleted kept publishing by itself the following week. Two states are
|
|
39
|
+
deliberately left alone: the already published one, because deleting it here would not take it off
|
|
40
|
+
the network and would only lose its history, and the one being published at that very moment.
|
|
41
|
+
If you wanted the plan out of the way without losing anything, that is `archive()`.
|
|
42
|
+
|
|
7
43
|
## [0.3.0] - 2026-08-31
|
|
8
44
|
|
|
9
45
|
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.4.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/
|
|
@@ -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": [
|
|
@@ -9597,6 +9707,11 @@
|
|
|
9597
9707
|
],
|
|
9598
9708
|
"description": "State machine: pending -> generating -> generated -> validated | failed | cancelled. Poll the plan while state is pending or generating."
|
|
9599
9709
|
},
|
|
9710
|
+
"archived_date": {
|
|
9711
|
+
"type": "string",
|
|
9712
|
+
"format": "date-time",
|
|
9713
|
+
"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."
|
|
9714
|
+
},
|
|
9600
9715
|
"orchestrator_result": {
|
|
9601
9716
|
"type": "object",
|
|
9602
9717
|
"description": "Raw plan returned by the orchestrator, kept for audit (before validation/trimming)."
|
|
@@ -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.4.0"
|
|
13
13
|
description = "Official Python client for the PlanVortex API"
|
|
14
14
|
readme = "README.md"
|
|
15
15
|
requires-python = ">=3.10"
|
|
@@ -2872,6 +2872,14 @@ class AiPlansAiPlan(TypedDict):
|
|
|
2872
2872
|
"""
|
|
2873
2873
|
State machine: pending -> generating -> generated -> validated | failed | cancelled. Poll the plan while state is pending or generating.
|
|
2874
2874
|
"""
|
|
2875
|
+
archived_date: NotRequired[str]
|
|
2876
|
+
"""
|
|
2877
|
+
When the plan was archived. Absent means it is active, which is how every plan created before archiving existed comes back.
|
|
2878
|
+
|
|
2879
|
+
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.
|
|
2880
|
+
|
|
2881
|
+
Archiving is visibility only: it takes the plan out of the default listing and touches no publication. Anything it had scheduled keeps publishing.
|
|
2882
|
+
"""
|
|
2875
2883
|
orchestrator_result: NotRequired[dict[str, Any]]
|
|
2876
2884
|
"""
|
|
2877
2885
|
Raw plan returned by the orchestrator, kept for audit (before validation/trimming).
|
|
@@ -12,7 +12,7 @@ from __future__ import annotations
|
|
|
12
12
|
|
|
13
13
|
import platform
|
|
14
14
|
|
|
15
|
-
VERSION = "0.
|
|
15
|
+
VERSION = "0.4.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.
|
|
@@ -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")
|
|
@@ -12,7 +12,7 @@ from collections.abc import Iterator
|
|
|
12
12
|
|
|
13
13
|
from planvortex._core.pagination import Page, PageParams
|
|
14
14
|
from planvortex._shapes import AiPlanRegenerateResult
|
|
15
|
-
from planvortex.resources_sync.base import Resource, require_id
|
|
15
|
+
from planvortex.resources_sync.base import Query, Resource, require_id
|
|
16
16
|
from planvortex.types import AiPlan, AiPlanCreateRequest, AiPlanCreateResult
|
|
17
17
|
|
|
18
18
|
|
|
@@ -111,17 +111,27 @@ class AiPlansResource(Resource):
|
|
|
111
111
|
*,
|
|
112
112
|
limit: int | None = None,
|
|
113
113
|
offset: int | None = None,
|
|
114
|
+
archived: bool | None = None,
|
|
114
115
|
timeout: float | None = None,
|
|
115
116
|
) -> Page[AiPlan]:
|
|
116
|
-
"""The organization's plans, from the newest to the oldest.
|
|
117
|
+
"""The organization's ACTIVE plans, from the newest to the oldest.
|
|
117
118
|
|
|
118
|
-
**The cancelled ones do not show up
|
|
119
|
-
|
|
119
|
+
**The cancelled ones do not show up** — nor the archived ones, which are asked for with
|
|
120
|
+
``archived=True`` — and here ``publications`` are identifiers, not the whole publications:
|
|
121
|
+
for that there is :meth:`get`. With no ``limit`` they all come back.
|
|
122
|
+
|
|
123
|
+
``archived`` opens the other cupboard, never both at once: archiving moves a plan somewhere
|
|
124
|
+
else, it does not put a label on it and leave it where it was.
|
|
125
|
+
|
|
126
|
+
.. code-block:: python
|
|
127
|
+
|
|
128
|
+
activos = pv.ai_plans.list(client_id, org_id)
|
|
129
|
+
guardados = pv.ai_plans.list(client_id, org_id, archived=True)
|
|
120
130
|
"""
|
|
121
131
|
pagina: Page[AiPlan] = self._list(
|
|
122
132
|
self._path(id_client, id_organization),
|
|
123
133
|
"ai_plans",
|
|
124
|
-
|
|
134
|
+
self._list_query(limit, offset, archived),
|
|
125
135
|
timeout=timeout,
|
|
126
136
|
)
|
|
127
137
|
return pagina
|
|
@@ -133,6 +143,7 @@ class AiPlansResource(Resource):
|
|
|
133
143
|
*,
|
|
134
144
|
limit: int | None = None,
|
|
135
145
|
offset: int | None = None,
|
|
146
|
+
archived: bool | None = None,
|
|
136
147
|
timeout: float | None = None,
|
|
137
148
|
) -> Iterator[AiPlan]:
|
|
138
149
|
"""The organization's plans, chaining pages."""
|
|
@@ -143,6 +154,7 @@ class AiPlansResource(Resource):
|
|
|
143
154
|
id_organization,
|
|
144
155
|
limit=params.limit,
|
|
145
156
|
offset=params.offset,
|
|
157
|
+
archived=archived,
|
|
146
158
|
timeout=timeout,
|
|
147
159
|
)
|
|
148
160
|
|
|
@@ -219,6 +231,47 @@ class AiPlansResource(Resource):
|
|
|
219
231
|
resultado: AiPlanRegenerateResult = self._post(ruta, {"target": target}, timeout=timeout)
|
|
220
232
|
return resultado
|
|
221
233
|
|
|
234
|
+
def archive(
|
|
235
|
+
self,
|
|
236
|
+
id_client: str,
|
|
237
|
+
id_organization: str,
|
|
238
|
+
id_ai_plan: str,
|
|
239
|
+
*,
|
|
240
|
+
timeout: float | None = None,
|
|
241
|
+
) -> AiPlan:
|
|
242
|
+
"""Archive the plan: it leaves the listing and moves to the archived one
|
|
243
|
+
(``list(..., archived=True)``).
|
|
244
|
+
|
|
245
|
+
It is **visibility only**. No publication is touched — anything scheduled keeps publishing —
|
|
246
|
+
no credits are refunded and nothing is cancelled, so it works in ANY state, ``generating``
|
|
247
|
+
included: it does not interrupt the job. Undone with :meth:`unarchive`.
|
|
248
|
+
|
|
249
|
+
It is what people are after almost every time they think of "removing" a plan: :meth:`remove`
|
|
250
|
+
takes the publications that have not gone out with it, and this does not.
|
|
251
|
+
"""
|
|
252
|
+
plan: AiPlan = self._post_one(
|
|
253
|
+
f"{self._one_path(id_client, id_organization, id_ai_plan)}/archive",
|
|
254
|
+
"ai_plan",
|
|
255
|
+
timeout=timeout,
|
|
256
|
+
)
|
|
257
|
+
return plan
|
|
258
|
+
|
|
259
|
+
def unarchive(
|
|
260
|
+
self,
|
|
261
|
+
id_client: str,
|
|
262
|
+
id_organization: str,
|
|
263
|
+
id_ai_plan: str,
|
|
264
|
+
*,
|
|
265
|
+
timeout: float | None = None,
|
|
266
|
+
) -> AiPlan:
|
|
267
|
+
"""Put the plan back in the active listing. On a plan that was not archived it does nothing."""
|
|
268
|
+
plan: AiPlan = self._post_one(
|
|
269
|
+
f"{self._one_path(id_client, id_organization, id_ai_plan)}/unarchive",
|
|
270
|
+
"ai_plan",
|
|
271
|
+
timeout=timeout,
|
|
272
|
+
)
|
|
273
|
+
return plan
|
|
274
|
+
|
|
222
275
|
def remove(
|
|
223
276
|
self,
|
|
224
277
|
id_client: str,
|
|
@@ -227,11 +280,28 @@ class AiPlansResource(Resource):
|
|
|
227
280
|
*,
|
|
228
281
|
timeout: float | None = None,
|
|
229
282
|
) -> None:
|
|
230
|
-
"""
|
|
231
|
-
|
|
283
|
+
"""Delete the plan **and its publications that have not gone out yet**: the generated drafts
|
|
284
|
+
and, if it had already been validated, whatever was still scheduled. The already published
|
|
285
|
+
ones stay — deleting them here would not take them off the network, it would only lose their
|
|
286
|
+
history — and neither is the one being published at that very moment touched.
|
|
287
|
+
|
|
288
|
+
The plan itself is not wiped: it moves to ``cancelled``, disappears from :meth:`list` and
|
|
289
|
+
:meth:`get` still returns it. Spent credits are not refunded and a ``generating`` plan cannot
|
|
290
|
+
be deleted (2102): wait for the job to finish.
|
|
291
|
+
|
|
292
|
+
To stop seeing it without losing anything, :meth:`archive`.
|
|
232
293
|
"""
|
|
233
294
|
self._delete(self._one_path(id_client, id_organization, id_ai_plan), timeout=timeout)
|
|
234
295
|
|
|
296
|
+
def _list_query(self, limit: int | None, offset: int | None, archived: bool | None) -> Query:
|
|
297
|
+
return {
|
|
298
|
+
"limit": limit,
|
|
299
|
+
"offset": offset,
|
|
300
|
+
# El servidor enciende el filtro con el literal "true": mandar `archived=false` pediria
|
|
301
|
+
# lo mismo que no mandar nada, asi que un `False` se omite en vez de viajar como ruido.
|
|
302
|
+
"archived": True if archived else None,
|
|
303
|
+
}
|
|
304
|
+
|
|
235
305
|
def _path(self, id_client: str, id_organization: str) -> str:
|
|
236
306
|
cliente = require_id(id_client, "id_client")
|
|
237
307
|
organizacion = require_id(id_organization, "id_organization")
|
|
@@ -22,7 +22,7 @@ from __future__ import annotations
|
|
|
22
22
|
from pytest_httpx2 import HTTPXMock
|
|
23
23
|
|
|
24
24
|
from tests.conftest import BASE_URL, ClienteDePrueba
|
|
25
|
-
from tests.contrato import cuerpo, peticiones, ruta, unica
|
|
25
|
+
from tests.contrato import cuerpo, peticiones, query, ruta, unica
|
|
26
26
|
|
|
27
27
|
PLANES = f"{BASE_URL}/clients/cli1/organizations/org1/ai_plans"
|
|
28
28
|
PLAN = {
|
|
@@ -222,3 +222,47 @@ def test_borrar_es_cancelar(cliente: ClienteDePrueba, httpx_mock: HTTPXMock) ->
|
|
|
222
222
|
|
|
223
223
|
assert cliente.esperar(cliente.pv.ai_plans.remove("cli1", "org1", "plan1")) is None
|
|
224
224
|
assert unica(httpx_mock).method == "DELETE"
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def test_archivar_y_desarchivar_van_a_dos_rutas(cliente: ClienteDePrueba, httpx_mock: HTTPXMock) -> None:
|
|
228
|
+
"""Son DOS rutas y no un cuerpo con un booleano: lo que hay que fijar es que cada metodo vaya a
|
|
229
|
+
la suya, y que el estado del plan no cambie al archivarlo.
|
|
230
|
+
"""
|
|
231
|
+
archivado = {**PLAN, "state": "validated", "archived_date": "2026-08-31T09:00:00.000Z"}
|
|
232
|
+
httpx_mock.add_response(url=f"{PLANES}/plan1/archive", method="POST", json={"ai_plan": archivado})
|
|
233
|
+
httpx_mock.add_response(
|
|
234
|
+
url=f"{PLANES}/plan1/unarchive", method="POST", json={"ai_plan": {**PLAN, "state": "validated"}}
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
guardado = cliente.esperar(cliente.pv.ai_plans.archive("cli1", "org1", "plan1"))
|
|
238
|
+
devuelto = cliente.esperar(cliente.pv.ai_plans.unarchive("cli1", "org1", "plan1"))
|
|
239
|
+
|
|
240
|
+
assert guardado["archived_date"] == "2026-08-31T09:00:00.000Z"
|
|
241
|
+
# Ausente, no `None`: es como llega cualquier plan activo, incluidos los anteriores al campo
|
|
242
|
+
assert "archived_date" not in devuelto
|
|
243
|
+
# Archivar no toca el ciclo de vida: un plan validado archivado sigue siendo `validated`
|
|
244
|
+
assert guardado["state"] == "validated"
|
|
245
|
+
archivar, desarchivar = peticiones(httpx_mock)
|
|
246
|
+
assert ruta(archivar) == "/clients/cli1/organizations/org1/ai_plans/plan1/archive"
|
|
247
|
+
assert ruta(desarchivar) == "/clients/cli1/organizations/org1/ai_plans/plan1/unarchive"
|
|
248
|
+
# POST vacios, como validar y reintentar
|
|
249
|
+
assert cuerpo(archivar) is None
|
|
250
|
+
assert cuerpo(desarchivar) is None
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def test_el_listado_pide_el_otro_armario_solo_cuando_se_le_pide(
|
|
254
|
+
cliente: ClienteDePrueba, httpx_mock: HTTPXMock
|
|
255
|
+
) -> None:
|
|
256
|
+
httpx_mock.add_response(url=f"{PLANES}?archived=true", json={"ai_plans": [], "total": 0})
|
|
257
|
+
httpx_mock.add_response(url=PLANES, json={"ai_plans": [PLAN], "total": 1}, is_reusable=True)
|
|
258
|
+
|
|
259
|
+
cliente.esperar(cliente.pv.ai_plans.list("cli1", "org1", archived=True))
|
|
260
|
+
cliente.esperar(cliente.pv.ai_plans.list("cli1", "org1", archived=False))
|
|
261
|
+
cliente.esperar(cliente.pv.ai_plans.list("cli1", "org1"))
|
|
262
|
+
|
|
263
|
+
archivados, negado, sin_decir = peticiones(httpx_mock)
|
|
264
|
+
assert query(archivados) == {"archived": ["true"]}
|
|
265
|
+
# `False` no viaja: el servidor enciende el filtro con el literal "true", asi que mandarlo seria
|
|
266
|
+
# ruido, y ademas invitaria a creer que hay un tercer modo con los dos armarios a la vez
|
|
267
|
+
assert "archived" not in query(negado)
|
|
268
|
+
assert "archived" not in query(sin_decir)
|
|
@@ -45,10 +45,14 @@ RECURSOS = (
|
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
def test_no_queda_ninguna_ruta_del_alcance_sin_metodo() -> None:
|
|
48
|
-
"""
|
|
48
|
+
"""115 de 115. Si esto falla, el mensaje dice exactamente cual falta.
|
|
49
|
+
|
|
50
|
+
Las dos ultimas son `archive` y `unarchive` de un plan de IA: son DOS rutas y no un cuerpo con
|
|
51
|
+
un booleano, asi que el alcance crece de dos en dos cuando aparece una pareja de estas.
|
|
52
|
+
"""
|
|
49
53
|
sin_cubrir, _, total = route_coverage.informe()
|
|
50
54
|
|
|
51
|
-
assert total ==
|
|
55
|
+
assert total == 115, "el alcance ha cambiado de tamano: mira si el spec ha crecido"
|
|
52
56
|
assert not sin_cubrir, "rutas del spec que ningun recurso construye:\n" + "\n".join(
|
|
53
57
|
str(operacion) for operacion in sin_cubrir
|
|
54
58
|
)
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|