easyvista-python-client 0.2.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.
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/.pre-commit-config.yaml +19 -1
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/CHANGELOG.md +576 -4
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/CONTRIBUTING.md +4 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/PKG-INFO +49 -13
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/README.md +28 -7
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/docs/api_reference.rst +32 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/docs/conf.py +1 -5
- easyvista_python_client-0.4.0/docs/content.rst +425 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/docs/development.rst +4 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/docs/index.rst +1 -0
- easyvista_python_client-0.4.0/docs/installation.rst +64 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/docs/publishing.rst +18 -6
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/docs/user_guide.rst +189 -32
- easyvista_python_client-0.4.0/docs/vendor-api-reference.md +509 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/__init__.py +6 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/_async/_transport.py +53 -1
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/_async/client.py +317 -61
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/_sync/_transport.py +53 -1
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/_sync/client.py +317 -61
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/_transport.py +22 -2
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/_version.py +1 -1
- easyvista_python_client-0.4.0/easyvista_python_client/content/__init__.py +18 -0
- easyvista_python_client-0.4.0/easyvista_python_client/content/conversion.py +823 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/directory.py +2 -1
- easyvista_python_client-0.4.0/easyvista_python_client/exceptions.py +117 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/models/action.py +112 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/models/common.py +98 -7
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/models/request.py +33 -18
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/references.py +9 -1
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/resources/actions.py +92 -3
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/resources/requests.py +19 -40
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/timestamps.py +19 -13
- easyvista_python_client-0.4.0/easyvista_python_client/workflow.py +337 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/pyproject.toml +42 -8
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/skills/easyvista-asset-workflow/SKILL.md +2 -2
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/skills/easyvista-client-setup/SKILL.md +20 -9
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/skills/easyvista-directory/SKILL.md +2 -2
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/skills/easyvista-document-workflow/SKILL.md +2 -2
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/skills/easyvista-instance-discovery/SKILL.md +7 -5
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/skills/easyvista-reporting-and-context/SKILL.md +2 -2
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/skills/easyvista-search-syntax/SKILL.md +2 -2
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/skills/easyvista-ticket-actions/SKILL.md +197 -23
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/skills/easyvista-ticket-workflow/SKILL.md +82 -7
- easyvista_python_client-0.2.0/docs/installation.rst +0 -46
- easyvista_python_client-0.2.0/docs/vendor-api-reference.md +0 -224
- easyvista_python_client-0.2.0/easyvista_python_client/exceptions.py +0 -53
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/.gitignore +0 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/.readthedocs.yaml +0 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/LICENSE +0 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/docs/_static/.gitkeep +0 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/docs/_templates/.gitkeep +0 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/docs/sponsoring.rst +0 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/_async/__init__.py +0 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/_async/_concurrency.py +0 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/_fields.py +0 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/_html.py +0 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/_sync/__init__.py +0 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/_sync/_concurrency.py +0 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/config.py +0 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/context.py +0 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/discovery.py +0 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/field_model.py +0 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/filters.py +0 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/models/__init__.py +0 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/models/asset.py +0 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/models/department.py +0 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/models/document.py +0 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/models/employee.py +0 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/models/generic.py +0 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/pagination.py +0 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/py.typed +0 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/reporting.py +0 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/resources/__init__.py +0 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/resources/assets.py +0 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/resources/departments.py +0 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/resources/descriptor.py +0 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/resources/discovery.py +0 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/resources/documents.py +0 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/resources/employees.py +0 -0
- {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/skills/README.md +0 -0
|
@@ -15,7 +15,25 @@ repos:
|
|
|
15
15
|
rev: v2.3.0
|
|
16
16
|
hooks:
|
|
17
17
|
- id: mypy
|
|
18
|
-
|
|
18
|
+
# The runtime dependencies, then the TYPED packages of the `content`
|
|
19
|
+
# extra. The hook's venv holds only this list, so without those four
|
|
20
|
+
# every converter base class (MarkdownConverter, MDRenderer,
|
|
21
|
+
# RenderTreeNode, MarkdownIt) resolves to Any and strict mode's
|
|
22
|
+
# disallow_subclassing_any fails content/conversion.py even though
|
|
23
|
+
# `mypy easyvista_python_client` in CI is clean. The extra's other two,
|
|
24
|
+
# cmarkgfm and mdformat-tables, ship no type information, so they are
|
|
25
|
+
# Any either way and are left out; cmarkgfm would also make every
|
|
26
|
+
# contributor without a wheel for their platform compile it before
|
|
27
|
+
# any commit. Each line here must equal the extra's requirement in
|
|
28
|
+
# pyproject; testing/test_public_api.py fails if one drifts.
|
|
29
|
+
additional_dependencies:
|
|
30
|
+
- "pydantic>=2.8"
|
|
31
|
+
- "httpx"
|
|
32
|
+
- "tenacity"
|
|
33
|
+
- "beautifulsoup4>=4.15"
|
|
34
|
+
- "markdown-it-py>=3.0,<4"
|
|
35
|
+
- "markdownify>=1.2.3,<1.3"
|
|
36
|
+
- "mdformat>=0.7.22,<0.8"
|
|
19
37
|
# Must mirror [tool.mypy] exclude in pyproject.toml. pre-commit passes
|
|
20
38
|
# filenames explicitly, and mypy ignores its own `exclude` for files named
|
|
21
39
|
# on the command line -- so this regex is the ONLY thing keeping the hook
|
|
@@ -4,11 +4,574 @@ All notable changes to this project are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
-
While the package is pre-1.0, breaking changes may land between minor
|
|
8
|
-
a deprecation policy will follow the 1.0 release.
|
|
7
|
+
While the package is pre-1.0, breaking changes may land between **minor**
|
|
8
|
+
versions; a deprecation policy will follow the 1.0 release. A patch release
|
|
9
|
+
never carries one. Each breaking change is marked `**BREAKING**` in its section
|
|
10
|
+
with the reasoning, so read the section for the version you are moving to.
|
|
11
|
+
|
|
12
|
+
**The git tag is what defines a version.** Where a section's narrative or date
|
|
13
|
+
disagrees with the tree its tag points at, the tag is authoritative and the prose
|
|
14
|
+
is the error. Tags carry no `v` prefix.
|
|
9
15
|
|
|
10
16
|
## [Unreleased]
|
|
11
17
|
|
|
18
|
+
## [0.4.0] - 2026-10-02
|
|
19
|
+
|
|
20
|
+
Adds Markdown <-> memo HTML conversion as an optional extra, drops Python
|
|
21
|
+
3.10, and **stops a ticket's workflow changing by accident -- which is a
|
|
22
|
+
breaking release.** `set_status` is gone (it was the vendor close request under
|
|
23
|
+
a name that hid it), `close_ticket` requires an explicit opt-in, and every
|
|
24
|
+
write the package can tell may change a ticket's workflow is refused before it
|
|
25
|
+
is sent unless the call says so. What it cannot tell is not covered (see
|
|
26
|
+
*Notes*).
|
|
27
|
+
|
|
28
|
+
A `0.4.0` section was first prepared on 2026-09-30 around a different
|
|
29
|
+
converter. It was never tagged or uploaded, and this section replaces it;
|
|
30
|
+
`### Changed since the 2026-09-30 preparation` says what moved, for anyone who
|
|
31
|
+
built against that branch.
|
|
32
|
+
|
|
33
|
+
**Upgrading.** **Python 3.10 is no longer supported** (see `### Removed`). On
|
|
34
|
+
3.10, pip keeps resolving `0.3.0`, the last release that installs there. The
|
|
35
|
+
converter is purely additive: the core package imports none of the extra's
|
|
36
|
+
dependencies. Every other breaking change is in the workflow guard, and each is
|
|
37
|
+
marked `**BREAKING**` in `### Changed` or `### Removed`. Read `### Upgrading`
|
|
38
|
+
below.
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- **The workflow guard.** `WorkflowEffect` (`INTERRUPTS`, `ADVANCES`,
|
|
43
|
+
`UNKNOWN`) and `EasyvistaWorkflowEffectRefused`, both exported at the package
|
|
44
|
+
root, and `easyvista_python_client.workflow` (`workflow_triggers`,
|
|
45
|
+
`classify_workflow_effects`), which names what a write may do to a ticket's
|
|
46
|
+
workflow. `EasyvistaWorkflowEffectRefused` is a `ValueError`, **not** an
|
|
47
|
+
`EasyvistaError`: the refused write is never sent, so there is no status code
|
|
48
|
+
and nothing transient, and it carries `effects` and `triggers` (what named
|
|
49
|
+
them). A ticket's status follows its workflow -- "Advancing through the steps
|
|
50
|
+
of a workflow changes the status of a ticket." (tier 1) -- so a write that
|
|
51
|
+
touches workflow state is not a bookkeeping write.
|
|
52
|
+
- An `allow_workflow_effect=` keyword, taking one `WorkflowEffect` or an
|
|
53
|
+
iterable of them, on `send`, `update_ticket`, `create_action`, `create_task`,
|
|
54
|
+
`update_action` and `end_action` (default: allows nothing), and **required**
|
|
55
|
+
on `close_ticket`. `RequestSpec.allow_workflow_effect` and
|
|
56
|
+
`RequestSpec.allowing()` carry the same opt-in on a request spec.
|
|
57
|
+
- `resources.actions.build_get_action(..., fields=...)` projects the item read,
|
|
58
|
+
as the list builders already did.
|
|
59
|
+
- `reassign_action(action_id, *, group_id=None, done_by_id=None)` on both
|
|
60
|
+
clients, with `resources.actions.build_reassign_action`: reassign an action
|
|
61
|
+
(for example, escalate the open workflow step to another group) without
|
|
62
|
+
ending it. It is not refused by the workflow guard. The vendor documents no
|
|
63
|
+
reassignment route (tier 1), so the effect was measured: 2026-10-02, one
|
|
64
|
+
instance, two tickets, so it may not generalise -- the group was stored, the
|
|
65
|
+
step stayed open, the ticket's status did not move and no new action rows
|
|
66
|
+
appeared; the ticket's owning group was read on one of the two tickets and
|
|
67
|
+
did not follow the action's. The person write (`done_by_id`) is unmeasured.
|
|
68
|
+
- `easyvista_python_client.content.EasyvistaContentConverter`, behind the new
|
|
69
|
+
optional extra `easyvista-python-client[content]`. It has two static
|
|
70
|
+
methods. `from_transport(value, *, plain_text_is_markdown=False)` reads a
|
|
71
|
+
memo, rich-text HTML or plain text, as Markdown. `to_transport(value)`
|
|
72
|
+
renders Markdown as the HTML a memo is written with.
|
|
73
|
+
|
|
74
|
+
**The Markdown is CommonMark with GFM tables.** Writing renders it with
|
|
75
|
+
`cmark-gfm`, with only its `table` extension: a newline is a line break, and
|
|
76
|
+
raw HTML passes through. Reading converts HTML with `markdownify`, then
|
|
77
|
+
re-renders the result with `mdformat`, which keeps only the escapes
|
|
78
|
+
CommonMark needs.
|
|
79
|
+
|
|
80
|
+
**Text in a memo is literal, and the Markdown spells it so.** A typed
|
|
81
|
+
`__init__` reads as `\_\_init\_\_`, `\\serveur` as `\\\serveur`, `# titre`
|
|
82
|
+
at a line start as `\# titre`, and `<Entrée>` as `\<Entrée>`. Ordinary prose
|
|
83
|
+
such as `fichier_de_test_v2.xlsx`, `C:\Temp`, `R&D` or `Ticket #4521` comes
|
|
84
|
+
back as typed. The aim is that rendering the Markdown displays what the
|
|
85
|
+
memo displayed, and that reading that back gives the same Markdown; the
|
|
86
|
+
measurements below say how far that held, and `docs/content.rst` lists the
|
|
87
|
+
shapes where it does not.
|
|
88
|
+
|
|
89
|
+
What else a reader sees:
|
|
90
|
+
- A memo with no HTML element is read as literal lines, a hard break per
|
|
91
|
+
line. Pass `plain_text_is_markdown=True` for a value that is your own
|
|
92
|
+
Markdown: it then passes through, stripped, unless it starts with `<` and
|
|
93
|
+
holds a real HTML element anywhere. So Markdown opening with an autolink
|
|
94
|
+
and carrying inline HTML further on is read as HTML, and loses the
|
|
95
|
+
autolink.
|
|
96
|
+
- A memo holding one real HTML element is read as HTML throughout, so
|
|
97
|
+
Markdown syntax beside it is kept as literal characters. The obsolete
|
|
98
|
+
elements (`<font>`, `<center>`, ...) count as HTML.
|
|
99
|
+
- Underline, highlight and strike, which CommonMark cannot spell, are kept
|
|
100
|
+
as raw `<u>`, `<mark>`, `<ins>` and `<s>`.
|
|
101
|
+
- A table nested in a table is flattened to its words, every one kept and in
|
|
102
|
+
order, and a table without a header row gains an empty one.
|
|
103
|
+
- Nested lists nest and keep their numbers. `<script>`, `<style>`,
|
|
104
|
+
`<template>` and `<title>` bodies are dropped.
|
|
105
|
+
|
|
106
|
+
Measured 2026-10-02 on 367 memos read from one preproduction instance on
|
|
107
|
+
2026-10-01 (tier 4, one instance, may not generalise): no memo changed a
|
|
108
|
+
word, and 367 of 367 readings were fixed points. 65 of them displayed
|
|
109
|
+
differently from the memo, each in a documented way that keeps its words:
|
|
110
|
+
in 62 a table gained an empty header row (61 of them a notification
|
|
111
|
+
template's flattened nested table), and 3 had no HTML element and were
|
|
112
|
+
read as literal lines. Synthetic shapes do lose words: a table row longer
|
|
113
|
+
than the first, a `|` in a cell's code or link, an image title holding a
|
|
114
|
+
`"`, and others that `docs/content.rst` lists.
|
|
115
|
+
|
|
116
|
+
It is a port of `glpi_python_client`'s `content/conversion.py` at `917f030`,
|
|
117
|
+
plus 15 fixes listed under `### Notes`. **The two should move together.**
|
|
118
|
+
|
|
119
|
+
Why it is here: until now this package could only strip HTML to plain text,
|
|
120
|
+
so a downstream GLPI-to-EasyVista sync wrote its own converter. Measured
|
|
121
|
+
2026-09-30 (tier 4, one instance), a GLPI description holding a pasted URL
|
|
122
|
+
and a titled link reached EasyVista with neither link clickable. EasyVista
|
|
123
|
+
stored exactly the HTML it was sent. That converter had escaped `<` a
|
|
124
|
+
second time, fused the link title into the `href`, cut a URL at its first
|
|
125
|
+
`)`, and would read two lone asterisks as emphasis. This converter handles
|
|
126
|
+
each of those correctly (checked 2026-10-02 on synthetic equivalents): a
|
|
127
|
+
`<` is escaped once, a link title stays a `title`, a URL keeps its
|
|
128
|
+
parentheses, and a lone `*` stays literal.
|
|
129
|
+
|
|
130
|
+
Two properties are worth knowing before relying on it:
|
|
131
|
+
- **Deep nesting degrades to text.** The conversion is attempted, and a
|
|
132
|
+
`RecursionError` is answered with the memo's words. From a shallow stack,
|
|
133
|
+
the deepest `<div>` document that still converts with its structure is 245
|
|
134
|
+
levels on CPython 3.11 and 327 on 3.12–3.14 (measured 2026-10-02). A caller
|
|
135
|
+
already within about 25 frames of the recursion limit still gets
|
|
136
|
+
`EasyvistaContentError`, and within the last few frames, a bare
|
|
137
|
+
`RecursionError`.
|
|
138
|
+
- **It is not a sanitiser.** Raw HTML, `javascript:` link targets and
|
|
139
|
+
`<javascript:...>` autolinks in the Markdown all go out live, and reading
|
|
140
|
+
keeps a memo's `javascript:` links. A caller relaying Markdown it did not
|
|
141
|
+
write must neutralise raw HTML and executable schemes. Text a memo
|
|
142
|
+
*displays* as markup, such as `<script>`, reads back escaped, as the
|
|
143
|
+
text it is.
|
|
144
|
+
|
|
145
|
+
Importing `easyvista_python_client.content` without the extra, or with any
|
|
146
|
+
one of its six packages missing, raises an `ImportError` naming
|
|
147
|
+
`pip install "easyvista-python-client[content]"`.
|
|
148
|
+
- `EasyvistaContentError`, a subclass of `EasyvistaError` exported at the
|
|
149
|
+
package root. It is raised for any converter fault the text fallback does
|
|
150
|
+
not absorb, with the underlying exception as `__cause__`. It lives in the
|
|
151
|
+
core package, so it can be caught whether or not the extra is installed.
|
|
152
|
+
- The `dev` and `docs` extras now install the `content` extra's packages, with
|
|
153
|
+
the same bounds, since CI runs the converter's tests and the API reference
|
|
154
|
+
imports it. `testing/test_public_api.py` fails if the copies drift, or if
|
|
155
|
+
its list of the extra's import names stops matching `pyproject.toml`.
|
|
156
|
+
|
|
157
|
+
### Changed
|
|
158
|
+
|
|
159
|
+
- **BREAKING** `close_ticket` requires `allow_workflow_effect=`, keyword-only
|
|
160
|
+
with no default: leaving it out is a `TypeError`, and a value that does not
|
|
161
|
+
include `WorkflowEffect.INTERRUPTS` is refused with
|
|
162
|
+
`EasyvistaWorkflowEffectRefused` before any request is made. The vendor
|
|
163
|
+
documents the close request as interrupting the workflow (tier 1, see
|
|
164
|
+
*Removed*), so the call site now says that it means to.
|
|
165
|
+
- **BREAKING** `end_action` is guarded. Unless `allow_workflow_effect` includes
|
|
166
|
+
`WorkflowEffect.ADVANCES` it first reads the action (one item read projecting
|
|
167
|
+
`ACTION_ID` and `WORKFLOW_ID`) and refuses, with no end request sent, when
|
|
168
|
+
the action is a workflow step (`WORKFLOW_ID` set), when the record comes back
|
|
169
|
+
without a `WORKFLOW_ID` column (which cannot be told from a step), or when
|
|
170
|
+
the read returns a different `ACTION_ID` than the one asked for. `end_all=True`
|
|
171
|
+
always needs `ADVANCES`. Ending an action you created yourself needs no
|
|
172
|
+
opt-in (see *Notes*). If the read fails, its error propagates and nothing is ended.
|
|
173
|
+
Ending a workflow step moves the workflow on -- vendor-documented only by the
|
|
174
|
+
UI's Finish wizard, and measured on one instance on 2026-09-01 (2 of 2), so
|
|
175
|
+
it may not generalise.
|
|
176
|
+
- **BREAKING** `end_action`'s explicit `action_id` must be a positive integer.
|
|
177
|
+
`0`, negatives, blanks, RFC numbers, floats and booleans now raise
|
|
178
|
+
`ValueError` before any request, and a numeric string is sent as an integer
|
|
179
|
+
(`"123"` goes out as `123`). `action_id=None` is still refused unless
|
|
180
|
+
`end_all=True`.
|
|
181
|
+
- **BREAKING** `update_ticket`, `update_action`, `create_action`, `create_task`
|
|
182
|
+
and `send` refuse a body or route that may change the workflow unless the call
|
|
183
|
+
passes `allow_workflow_effect=`. Named: the workflow-control bodies `closed`,
|
|
184
|
+
`end_action`, `suspended` and `restarted` as a top-level key in any casing on
|
|
185
|
+
any path; on a `requests/{rfc}` route, the status, catalog and parent-request
|
|
186
|
+
columns and a `DELETE`; on an existing `actions/{id}` route, the end date,
|
|
187
|
+
type, parent, ticket and workflow, stage and step columns (creating an action
|
|
188
|
+
or a task names a narrower set); a write to `actions/<x>` where `<x>` is not an
|
|
189
|
+
integer id, which is the vendor's end-action route `PUT
|
|
190
|
+
actions/{rfc_number}`, whatever the body says; every ticket sub-route that is
|
|
191
|
+
a command rather than a record (`close`, `suspend`, `restart`,
|
|
192
|
+
`workflowstart`, ...) and `requests/without-workflow`. The column rules apply
|
|
193
|
+
to the `requests/` and `actions/` routes only: a route of any other family is
|
|
194
|
+
not classified by column. What the typed models declare needs no opt-in,
|
|
195
|
+
and neither do text, owner, group, done-by, impact or urgency. The exact
|
|
196
|
+
lists are in `docs/vendor-api-reference.md`, "Ticket workflow". **This is a
|
|
197
|
+
deny-list, and a deny-list of columns cannot be complete**: what is not named
|
|
198
|
+
is unclassified, not proven neutral.
|
|
199
|
+
- **BREAKING** `update_action` refuses an action id that is not a positive
|
|
200
|
+
integer, `None` included: `PUT actions/{rfc_number}` is the end-action route
|
|
201
|
+
on the same path, so an RFC number would not edit an action.
|
|
202
|
+
- **BREAKING** The spec builders changed with the guard. A spec from
|
|
203
|
+
`resources.requests.build_close_ticket` or `resources.actions.build_end_action`
|
|
204
|
+
names a workflow effect, so this package's transport refuses it until it is
|
|
205
|
+
passed through `.allowing(...)`; code that builds one and sends it itself must
|
|
206
|
+
say so. And `resources.actions.build_end_action` and `build_update_action`
|
|
207
|
+
now raise `ValueError` at build time for an action id that is not a positive
|
|
208
|
+
integer, where they formerly passed the id through as given.
|
|
209
|
+
- **BREAKING** `send()` -- the path every typed method and the client's own
|
|
210
|
+
`send` go through -- refuses outright, with `ValueError` and whatever the
|
|
211
|
+
method or opt-in, a path containing a dot segment (`.` or `..`), a
|
|
212
|
+
percent-encoded slash or backslash, or a raw backslash: the HTTP client
|
|
213
|
+
collapses a dot segment, and a server may read the others as a separator, so
|
|
214
|
+
the request could reach a route other than the one that was checked. No API
|
|
215
|
+
route needs one. Whether this server reads them as separators is not
|
|
216
|
+
measured; the check fails closed. Document downloads (`get_bytes`,
|
|
217
|
+
`stream_bytes`) are reads and are never gated.
|
|
218
|
+
- **BREAKING** A request is treated as a read only when its method **and** the
|
|
219
|
+
value of every method-override header (`X-HTTP-Method-Override`,
|
|
220
|
+
`X-HTTP-Method`, `X-Method-Override`) on the request are reads, so such a
|
|
221
|
+
header cannot hide a write behind a `GET`: a request whose override header
|
|
222
|
+
names a write is classified as that write, and refused if it names a workflow
|
|
223
|
+
effect and the call did not allow it, whatever method it is sent as. The
|
|
224
|
+
headers read are the ones that go on the wire, `config.extra_headers` with
|
|
225
|
+
the request's own laid over them. Whether this API honours these headers is
|
|
226
|
+
not recorded.
|
|
227
|
+
- **BREAKING** A write that names a workflow effect and is allowed is sent
|
|
228
|
+
**once**, never retried: `close_ticket` and `end_action` formerly retried a
|
|
229
|
+
429, a 5xx or a connection error when `max_retries` was above its default of
|
|
230
|
+
`0`, and now raise after the first attempt. Each close inserts another
|
|
231
|
+
anticipated closing action and each end ends whatever is open, so a resend
|
|
232
|
+
after a lost response is not a repeat of the same request. This includes
|
|
233
|
+
`end_action` on your own action. Every other request keeps its retries.
|
|
234
|
+
|
|
235
|
+
### Removed
|
|
236
|
+
|
|
237
|
+
- **BREAKING** — Python 3.10. `requires-python` is now `>=3.11`, the 3.10
|
|
238
|
+
classifier is gone, and the CI and release matrices run 3.11–3.14. CPython
|
|
239
|
+
3.10 reaches end of life in October 2026 (PEP 619), and `glpi_python_client`,
|
|
240
|
+
whose converter `content` ports, dropped it in `524304a`. Two requirements
|
|
241
|
+
that existed only for 3.10 go with it: `typing-extensions` (a runtime
|
|
242
|
+
dependency on 3.10 only) and `tomli` (in `dev` and `docs`). Nothing moves on
|
|
243
|
+
3.11 and later. The timestamp parser keeps the normalisation it carried for
|
|
244
|
+
3.10, so it accepts and refuses the same values as before.
|
|
245
|
+
- **BREAKING** `EasyvistaClient.set_status` / `AsyncEasyvistaClient.set_status`
|
|
246
|
+
and `resources.requests.build_set_status`. They sent the vendor CLOSE request,
|
|
247
|
+
which the vendor close page documents (tier 1, re-read 2026-10-02) as
|
|
248
|
+
interrupting the workflow, setting the final status, deleting the unfinished
|
|
249
|
+
actions when `delete_actions` is set (otherwise, by the package's reading of
|
|
250
|
+
the page, ending them) and inserting an anticipated closing action, none of it
|
|
251
|
+
conditional on the status sent. The page documents final statuses only, so
|
|
252
|
+
for a non-final one that is an extrapolation it neither exempts nor covers.
|
|
253
|
+
A synchroniser that used `set_status` to mirror an intermediate status closed
|
|
254
|
+
tickets early. The root cause was established on 2026-10-01/02 from the
|
|
255
|
+
synchroniser's code (it sent the close request right after every create and on
|
|
256
|
+
every status push) and from the vendor page; the drain of the open workflow
|
|
257
|
+
action across such a write was measured on one ticket (2026-09-01, one
|
|
258
|
+
instance, tier 4, so it may not generalise). The vendor documents no status
|
|
259
|
+
setter and this package has none: a flat status update is excluded from the
|
|
260
|
+
vendor's ticket update body (tier 1) and was seen to return 200 while dropping
|
|
261
|
+
the status (0.2.0 entry below), and a ticket's status follows its workflow.
|
|
262
|
+
`close_ticket` and `resources.requests.build_close_ticket` remain.
|
|
263
|
+
|
|
264
|
+
### Changed since the 2026-09-30 preparation
|
|
265
|
+
|
|
266
|
+
None of this reached PyPI, so it is not a break for anyone upgrading from
|
|
267
|
+
`0.3.0`. It matters to a caller who built against the branch.
|
|
268
|
+
|
|
269
|
+
- **The converter changed design.** The first preparation ported
|
|
270
|
+
`glpi_python_client` at `4fc3bed`, built on python-markdown with its
|
|
271
|
+
`nl2br`, `sane_lists`, `fenced_code` and `tables` extensions.
|
|
272
|
+
`glpi_python_client` replaced that design in `917f030` with markdownify +
|
|
273
|
+
mdformat inbound and cmark-gfm outbound, and this package follows it.
|
|
274
|
+
- **The `content` extra's dependencies changed.** `markdown` is gone, and
|
|
275
|
+
`cmarkgfm`, `markdown-it-py`, `mdformat` and `mdformat-tables` are new. The
|
|
276
|
+
bounds are now `beautifulsoup4>=4.15`, `cmarkgfm>=2025.10.22`,
|
|
277
|
+
`markdown-it-py>=3.0,<4`, `markdownify>=1.2.3,<1.3`, `mdformat>=0.7.22,<0.8`
|
|
278
|
+
and `mdformat-tables>=1.0,<1.1`. The upper bounds are deliberate, because
|
|
279
|
+
the reader overrides private surfaces of `markdownify` and `mdformat`.
|
|
280
|
+
`docs/content.rst` gives the reason for each bound.
|
|
281
|
+
- **The Markdown dialect is CommonMark with GFM tables**, not
|
|
282
|
+
python-markdown's. What a caller sees:
|
|
283
|
+
- A fence's language tag now survives a round trip.
|
|
284
|
+
- Strong inside emphasis keeps its bold.
|
|
285
|
+
- Two `<br>` in a row stay two line breaks.
|
|
286
|
+
- A `<pre>` opening a list item stays code.
|
|
287
|
+
- Struck text is kept as a raw `<s>` instead of losing the line.
|
|
288
|
+
- `~~text~~` is literal tildes on write, as before.
|
|
289
|
+
- `<javascript:alert(1)>` now becomes a live link, where python-markdown left
|
|
290
|
+
it as raw markup.
|
|
291
|
+
- **Escapes differ.** A displayed `<` reads as `\<` rather than `<`. `#4521`
|
|
292
|
+
at a line start is no longer escaped, since CommonMark needs a space after
|
|
293
|
+
`#`. A hard break reads as a trailing backslash rather than two spaces. Link
|
|
294
|
+
targets are kept in `<...>` where they need it, and a `[` or `]` in one comes
|
|
295
|
+
back percent-encoded.
|
|
296
|
+
- **Plain text is read as literal lines.** A memo with no HTML element used to
|
|
297
|
+
pass through verbatim, so its text was read as Markdown and a typed
|
|
298
|
+
`__init__` rendered bold. It is now escaped, and each line kept with a hard
|
|
299
|
+
break. How EasyVista's UI displays such a memo is unverified (`O-MEMOFORMAT`).
|
|
300
|
+
`plain_text_is_markdown=True` restores pass-through for your own Markdown.
|
|
301
|
+
- **The depth cliff moved.** With the old design it was measured at 493 levels
|
|
302
|
+
on CPython 3.12–3.14. With this one it is 327 there, and 245 on 3.11.
|
|
303
|
+
- **The `beautifulsoup4` workaround is gone.** The old design rewrote
|
|
304
|
+
self-closing void tags to dodge a defect that dropped the text after a
|
|
305
|
+
`<br />` in a body that also used `<br>`. That defect no longer reproduced on
|
|
306
|
+
4.15.0 (measured 2026-09-30), so the floor is now 4.15 and the workaround is
|
|
307
|
+
removed.
|
|
308
|
+
|
|
309
|
+
### Upgrading
|
|
310
|
+
|
|
311
|
+
- `client.set_status(rfc, status_guid=g)` -- delete it; nothing replaces it,
|
|
312
|
+
because the vendor documents no status setter (a flat status update is
|
|
313
|
+
excluded from its ticket update body, and was seen to drop the status -- see
|
|
314
|
+
0.2.0) and a ticket's status follows its workflow. To move a ticket through
|
|
315
|
+
its workflow, end the step's open action with
|
|
316
|
+
`end_action(rfc, action_id=..., allow_workflow_effect=WorkflowEffect.ADVANCES)`.
|
|
317
|
+
The status that follows is the workflow's, not yours to choose. This is not
|
|
318
|
+
documented on the REST page, which is silent about the workflow; it was
|
|
319
|
+
measured on one instance on 2026-09-01 (2 of 2 tickets) and may not
|
|
320
|
+
generalise, so re-read the ticket afterwards. To close, call
|
|
321
|
+
`close_ticket(rfc, allow_workflow_effect=WorkflowEffect.INTERRUPTS, status_guid=g)`.
|
|
322
|
+
- `end_action` callers: pass the integer id of an action you read, never an RFC
|
|
323
|
+
number, `0` or a blank. Ending your own action still needs no opt-in (see
|
|
324
|
+
*Notes*), and now costs one extra item read. Ending a workflow step, or any action whose record
|
|
325
|
+
shows no `WORKFLOW_ID`, needs `WorkflowEffect.ADVANCES`, and so does
|
|
326
|
+
`end_all=True`.
|
|
327
|
+
- Code that puts a status, catalog or other named column into `extra_payload`,
|
|
328
|
+
or calls `send` with a workflow route or body, now raises until it passes
|
|
329
|
+
`allow_workflow_effect=`. `WorkflowEffect.UNKNOWN` means undocumented, not
|
|
330
|
+
harmless: read "Ticket workflow" in `docs/vendor-api-reference.md` first.
|
|
331
|
+
- Code that builds a close or end-action spec with `build_close_ticket` or
|
|
332
|
+
`build_end_action` and sends it through the transport itself: pass the spec
|
|
333
|
+
through `.allowing(...)` first, and pass `build_end_action` and
|
|
334
|
+
`build_update_action` an integer action id, never an RFC number.
|
|
335
|
+
- Catch `EasyvistaWorkflowEffectRefused` (or `ValueError`) where you record
|
|
336
|
+
per-record failures. It is a `ValueError`, **not** an `EasyvistaError`, and
|
|
337
|
+
carries no status code; it is never transient. Code that catches
|
|
338
|
+
`EasyvistaError` around a close for cleanup will NOT catch it. `ValueError`
|
|
339
|
+
also catches the other local refusals above.
|
|
340
|
+
- If you set `max_retries` above its default of `0`, a lost response to an
|
|
341
|
+
allowed workflow write now surfaces as an error instead of a silent resend.
|
|
342
|
+
Re-read the ticket before repeating it.
|
|
343
|
+
- The minor bump is deliberate: a dependant pinned `>=0.3.0,<0.4` does **not**
|
|
344
|
+
pick this up, and must widen its constraint on purpose.
|
|
345
|
+
|
|
346
|
+
### Documentation
|
|
347
|
+
|
|
348
|
+
- `docs/vendor-api-reference.md` gains "Ticket workflow": what each documented
|
|
349
|
+
write does to a workflow, with the vendor page quoted (tier 1), the guard's
|
|
350
|
+
deny-list exactly as the code holds it, and the measurements labelled tier 4
|
|
351
|
+
with their instance and date. O-CLOSE-DEFAULT is closed at tier 1: the vendor
|
|
352
|
+
close page documents an omitted `status_GUID` as defaulting to the Closed
|
|
353
|
+
meta-status. It was not measured here.
|
|
354
|
+
- The README, the user guide, the API reference and the `easyvista-client-setup`,
|
|
355
|
+
`easyvista-ticket-actions` and `easyvista-ticket-workflow` skills describe the
|
|
356
|
+
guard; the `easyvista-instance-discovery` skill now says `close_ticket` stops
|
|
357
|
+
the workflow and is not a way to pick an intermediate status. The
|
|
358
|
+
ticket-workflow skill's first gotcha is now that `close_ticket` is not a
|
|
359
|
+
status setter.
|
|
360
|
+
- The `PostRequest.workflow_start` docstring records that the flag is a no-op
|
|
361
|
+
(tier 4: two tickets identical but for it came back byte-identical, 2026-09-01,
|
|
362
|
+
one instance), so `workflow_start=False` does not create a ticket without its
|
|
363
|
+
workflow. The vendor create page documents no such parameter and states that
|
|
364
|
+
the workflow is started; the workflow-less create is the virtual-agent route
|
|
365
|
+
`requests/without-workflow`, which the guard refuses unless allowed.
|
|
366
|
+
- **Retracted:** that `set_status` reaches every status, as the 0.2.0 entry
|
|
367
|
+
below put it ("a fresh ticket landed on exactly the status requested every
|
|
368
|
+
time, non-terminal ones included") and the `RequestUpdate` docstring repeated
|
|
369
|
+
it ("That route reaches **every** status, not just terminal ones"). Six status
|
|
370
|
+
GUIDs were tried and each landed, but that was measured by re-reading the
|
|
371
|
+
status only: the measurement never looked at the workflow or the ticket's
|
|
372
|
+
open actions, and the vendor close page documents the close request as
|
|
373
|
+
interrupting the workflow (tier 1). A status that landed is not evidence that
|
|
374
|
+
nothing else moved, and that finding was read as a safe status setter, which
|
|
375
|
+
it was not. The 0.2.0 section is left as written.
|
|
376
|
+
- `docs/content.rst` is a user-guide page for the converter. It covers what a
|
|
377
|
+
memo holds, what each direction reads and writes, what text is escaped and
|
|
378
|
+
why, and what survives a round trip and what is lost. It also covers what
|
|
379
|
+
the converter does not sanitise, the dependency bounds, and the
|
|
380
|
+
CVE-2025-6069 guard. The API reference gains a "Rich-text content" section.
|
|
381
|
+
- `docs/vendor-api-reference.md` records the memo format as tier 4 (above),
|
|
382
|
+
and the vendor's MEMO and TEXT AREA form objects as tier 1
|
|
383
|
+
(`docs.easyvista.com/docs/form`, read 2026-10-02). That page does not say
|
|
384
|
+
which object a ticket's or an action's memo is. A second tier-1 page
|
|
385
|
+
(`docs.easyvista.com/docs/service-manager-comment-log-creation`, read
|
|
386
|
+
2026-10-02) sets a custom comment-log field on the request form to "Text
|
|
387
|
+
area", which leans towards HTML display but is not about the built-in
|
|
388
|
+
memos. It also opens `O-MEMOFORMAT` for what is not yet known:
|
|
389
|
+
- how the UI displays a memo with no HTML element;
|
|
390
|
+
- what the web UI's own editor writes;
|
|
391
|
+
- how the UI treats the newlines between blocks;
|
|
392
|
+
- what it shows for raw markup;
|
|
393
|
+
- whether an empty table header row is visible;
|
|
394
|
+
- whether a percent-encoded link target still resolves.
|
|
395
|
+
- The `easyvista-ticket-workflow` and `easyvista-ticket-actions` skills each
|
|
396
|
+
gain a gotcha: a memo stores what it is sent, nothing renders Markdown for
|
|
397
|
+
you, and the converter's dialect is CommonMark with GFM tables.
|
|
398
|
+
- `docs/installation.rst`, `docs/development.rst`, `docs/publishing.rst`,
|
|
399
|
+
`README.md`, `CONTRIBUTING.md` and every skill's `compatibility` line now
|
|
400
|
+
state the Python 3.11 floor.
|
|
401
|
+
|
|
402
|
+
### Notes
|
|
403
|
+
|
|
404
|
+
- What the workflow guard does not establish. It is a deny-list: the vendor's
|
|
405
|
+
update pages accept every column of the ticket and action tables except an
|
|
406
|
+
excluded list (tier 1), and a per-instance business rule can fire on
|
|
407
|
+
any write, so a write the guard does not name is unclassified, not proven
|
|
408
|
+
neutral. `end_action` tells a workflow step from your own action by
|
|
409
|
+
`WORKFLOW_ID` (1500 of 1500 rows, 2026-09-02, one instance); whether an
|
|
410
|
+
action created under a step by `create_action` carries one is unmeasured, and
|
|
411
|
+
if it does, ending it is refused too -- the safe direction. The live checks
|
|
412
|
+
in `integration_tests/test_live_workflow_guard.py` are gated: its first two
|
|
413
|
+
tests read only, and the rest write and run only when
|
|
414
|
+
`EASYVISTA_TEST_RUN_WORKFLOW_CENSUS=1` is set.
|
|
415
|
+
- **The 15 fixes on top of `glpi_python_client` `917f030`**, measured
|
|
416
|
+
together on this package's synthetic corpus and on the 367-memo sample, and
|
|
417
|
+
still to be proposed to `glpi_python_client`:
|
|
418
|
+
1. Escapes and character references in an image's alt text are kept.
|
|
419
|
+
markdown-it-py 3 leaves them as `text_special` tokens, which mdformat
|
|
420
|
+
rendered as nothing.
|
|
421
|
+
2. A paragraph line opening with `~~~` is escaped, because CommonMark reads
|
|
422
|
+
it as a fence.
|
|
423
|
+
3. A `!` that ends a text before a link is escaped, so it does not turn the
|
|
424
|
+
link into an image.
|
|
425
|
+
4. An alt text starting with `^` is escaped, because cmark-gfm never opens
|
|
426
|
+
an image on `![^`.
|
|
427
|
+
5. A `<` that mdformat leaves bare after an escaped one is escaped, so
|
|
428
|
+
`<<a@b.c>` no longer becomes an autolink.
|
|
429
|
+
6. A `<br>` inside inline code (`<code>`, `<kbd>`, `<samp>`) splits the
|
|
430
|
+
span, and a `|` in such a span in a table cell is escaped.
|
|
431
|
+
7. A table inside a heading or a link is flattened, as one inside a cell
|
|
432
|
+
already was.
|
|
433
|
+
8. A block inside a flattened cell stays on its link's line.
|
|
434
|
+
9. A flattened cell's edge counts as a space when deciding whether `**`
|
|
435
|
+
closes.
|
|
436
|
+
10. `<center>` is a block, as `<div>` is, except inside `<pre>`.
|
|
437
|
+
11. `<u>`, `<mark>` and `<ins>` are kept as raw tags.
|
|
438
|
+
12. Ordered items are numbered in linear time, where counting every earlier
|
|
439
|
+
item was quadratic, and `start` is read with `isdecimal`.
|
|
440
|
+
13. The pattern that splits emphasis and links into edges and content is
|
|
441
|
+
linear on runs of spaces or `<br>`. It was quadratic.
|
|
442
|
+
14. A `colspan` or `start` that is not a decimal number, such as `"²"` or
|
|
443
|
+
5,000 digits, degrades the memo to text instead of failing it.
|
|
444
|
+
15. Every `<` after a memo's last `>` is read as text. CPython's
|
|
445
|
+
`html.parser` before 3.11.14, 3.12.12 and 3.13.6 is quadratic on a run
|
|
446
|
+
of unfinished tags there (CVE-2025-6069, python/cpython#135462).
|
|
447
|
+
- **What does not round-trip is documented, not hidden.** `docs/content.rst`
|
|
448
|
+
lists both sides.
|
|
449
|
+
- Markdown written and read back comes back in mdformat's canonical
|
|
450
|
+
spelling. Text in angle brackets that is not a URL is lost. An e-mail
|
|
451
|
+
autolink becomes a `mailto:` link, and `[`/`]` in a link target come back
|
|
452
|
+
percent-encoded.
|
|
453
|
+
- A memo read and written back kept every word on the 367-memo sample. A
|
|
454
|
+
nested table's grid becomes text in its cell, and a header-less table
|
|
455
|
+
gains an empty header row. The known holes, all synthetic and absent
|
|
456
|
+
from the sample, are listed too, the ones that lose words among them.
|
|
457
|
+
|
|
458
|
+
After one write-and-read cycle, a further one changes nothing more.
|
|
459
|
+
- `cmarkgfm` 2025.10.22 bundles cmark-gfm `0.29.0.gfm.13`. PyPI lists no wheel
|
|
460
|
+
of that release for macOS x86_64 or Windows ARM64 (read 2026-10-02). Unless a
|
|
461
|
+
later release adds one, it builds from source there, which needs a C
|
|
462
|
+
compiler.
|
|
463
|
+
- mdformat 1.x, markdown-it-py 4.x and mdformat-gfm 1.0 were tried and are not
|
|
464
|
+
adopted. markdown-it-py 4 caps the cells it fills in, which ends a ragged
|
|
465
|
+
table early and turns the rest into literal pipes. Re-measure before raising
|
|
466
|
+
those bounds.
|
|
467
|
+
|
|
468
|
+
## [0.3.0] - 2026-09-02
|
|
469
|
+
|
|
470
|
+
Makes the task-vs-action distinction inspectable. A GLPI comment corresponds to
|
|
471
|
+
an EasyVista **task**, not an action; actions carry effort and cost, so a
|
|
472
|
+
timeline mirrored without that distinction imports time-tracking and workflow
|
|
473
|
+
rows as conversation.
|
|
474
|
+
|
|
475
|
+
**Upgrading.** One breaking change, in `Action` (see `### Changed`). The minor
|
|
476
|
+
bump is deliberate: a dependant pinned `>=0.2.0,<0.3` does **not** pick this up,
|
|
477
|
+
and must widen its constraint on purpose — which is the point, because the
|
|
478
|
+
retyping below can change an answer silently rather than raise.
|
|
479
|
+
|
|
480
|
+
Before widening, check whether you read `action.ELAPSED_TIME`, `TIME_COST`,
|
|
481
|
+
`CONTRACTUAL_COST`, `START_DATE_UT` or `END_DATE_UT` off `model_extra`, or read
|
|
482
|
+
those keys out of a `model_dump(by_alias=True)` and treated them as strings. Both
|
|
483
|
+
change here. Nothing on the write side moves: `PostAction`, `PostTask` and
|
|
484
|
+
`ActionUpdate` are untouched.
|
|
485
|
+
|
|
486
|
+
### Added
|
|
487
|
+
|
|
488
|
+
- Five columns declared on `Action` with real types: `elapsed_time` (minutes,
|
|
489
|
+
`int | None`), `time_cost` and `contractual_cost` (exact `Decimal | None`,
|
|
490
|
+
parsed from the API's French decimal comma), `start_date_ut` and
|
|
491
|
+
`end_date_ut` (aware `datetime | None`). They previously arrived only as
|
|
492
|
+
untyped `extra="allow"` strings.
|
|
493
|
+
|
|
494
|
+
**The `""` sentinel and `"0"` stay different answers.** `""` means the column
|
|
495
|
+
does not apply to this record and maps to `None`; `"0"` / `"0,00"` means it
|
|
496
|
+
applies and is zero, and maps to `0` / `Decimal("0.00")`. Collapsing the two
|
|
497
|
+
destroys the only signal that says whether a record tracks effort. Measured
|
|
498
|
+
over 1500 live rows: `ELAPSED_TIME` was `""` on 384 and `"0"` on 895.
|
|
499
|
+
- `Action.is_workflow_generated` — whether `WORKFLOW_ID` is set, i.e. whether
|
|
500
|
+
the workflow engine owns the row. Clean on 1500/1500 measured rows, and the
|
|
501
|
+
one structural fact in this area that holds.
|
|
502
|
+
|
|
503
|
+
It is **not** an `is_task()`, and the package deliberately does not ship one:
|
|
504
|
+
nothing on an action record says which route created it, and the
|
|
505
|
+
effort-shape heuristic that looks like it should is wrong in both directions
|
|
506
|
+
— 173 of 1500 rows had a `WORKFLOW_ID` with an empty `ELAPSED_TIME`, and 39
|
|
507
|
+
of 49 public comments carried a non-empty one, one of them with a real
|
|
508
|
+
`TIME_COST` of `99,00`. Which action types count as conversation is
|
|
509
|
+
per-deployment policy and stays with the caller.
|
|
510
|
+
- `scripts/tests/test_no_private_instance_identifiers.py` — a guard refusing any
|
|
511
|
+
private EasyVista instance identifier (hostname, domain, real catalog GUID or
|
|
512
|
+
code) in a tracked file. `.gitignore` has always stated that policy in prose;
|
|
513
|
+
it had no enforcement, and preparing this release put a real preprod hostname
|
|
514
|
+
into `docs/vendor-api-reference.md` — a tracked file that also ships in the
|
|
515
|
+
sdist — with all five gates passing, because none of them reads prose for
|
|
516
|
+
this. Both a public push and a PyPI upload are irreversible, so the check is
|
|
517
|
+
now mechanical. It carries its own negative control, and deliberately does
|
|
518
|
+
**not** refuse the illustrative account number or the synthetic
|
|
519
|
+
`EAZ_INC_000` stand-in the tracked tests already use.
|
|
520
|
+
- `models.common.OptionalDecimal`, the annotated type behind the two cost
|
|
521
|
+
columns. Accepts either decimal separator, so a dot-configured deployment
|
|
522
|
+
needs no setting, and **refuses a grouping separator** rather than guessing
|
|
523
|
+
— `'1.234,56'` and `'1,234.56'` are the same amount under opposite
|
|
524
|
+
conventions. Not exported from the package root.
|
|
525
|
+
|
|
526
|
+
### Changed
|
|
527
|
+
|
|
528
|
+
- **BREAKING** — `ELAPSED_TIME`, `TIME_COST`, `CONTRACTUAL_COST`,
|
|
529
|
+
`START_DATE_UT` and `END_DATE_UT` are now declared fields on `Action` instead
|
|
530
|
+
of `extra="allow"` extras. Three consequences:
|
|
531
|
+
- `action.ELAPSED_TIME` (extra attribute access) raises `AttributeError`; use
|
|
532
|
+
`action.elapsed_time`. The same for the other four.
|
|
533
|
+
- They are gone from `action.model_extra`, and
|
|
534
|
+
`model_dump(by_alias=True)["ELAPSED_TIME"]` is now an `int | None` rather
|
|
535
|
+
than a `str`.
|
|
536
|
+
- `classify_fields()` bucketing is **unchanged** — none of the five starts
|
|
537
|
+
with `E_`, so all five have always landed in `.official`. What changed is
|
|
538
|
+
*presence*: `.official` (and the dump) now always carries all five keys,
|
|
539
|
+
`None` included, where previously a key the API did not return was simply
|
|
540
|
+
absent. Code that iterates `.official` sees five more keys on a default
|
|
541
|
+
`list_actions` row.
|
|
542
|
+
- **`None` is now ambiguous.** These columns are not on the default
|
|
543
|
+
`list_actions` projection, so on a default row every one reads `None`,
|
|
544
|
+
meaning *not returned* — not the `""` that means *does not apply*. The two
|
|
545
|
+
are indistinguishable once validated. Project them explicitly or read
|
|
546
|
+
item-level before reading meaning into a `None`.
|
|
547
|
+
- A `Decimal` in a dump is not JSON-serialisable, so
|
|
548
|
+
`json.dumps(action.model_dump(by_alias=True))` raises where a cost is
|
|
549
|
+
populated. Use `model_dump(mode="json")`, which renders the amount as a
|
|
550
|
+
string with a `.` decimal point rather than the `,` the API sent.
|
|
551
|
+
- A malformed value now raises where it previously passed through as a
|
|
552
|
+
string. This is the same trade `OptionalDateTime` already makes — a wrong
|
|
553
|
+
number is worse than a loud failure — and it carries the same blast radius:
|
|
554
|
+
the descriptor validates a page in a list comprehension, so one bad value
|
|
555
|
+
fails a whole `list_actions` call. See `O-COSTGROUP` in
|
|
556
|
+
`docs/vendor-api-reference.md`.
|
|
557
|
+
|
|
558
|
+
### Documentation
|
|
559
|
+
|
|
560
|
+
- `docs/vendor-api-reference.md` records, as tier 2 read on two 2025.3
|
|
561
|
+
deployments on 2026-09-02, that `/requests/{rfc_number}/tasks` is **POST
|
|
562
|
+
only** and that **no task read route exists** under any spelling — the only
|
|
563
|
+
timeline reads are `GET /actions`, `/actions/{id}` and
|
|
564
|
+
`/actions/{id}/{comment}`. A task is written as a task and read back as an
|
|
565
|
+
action, which is why there is no `list_tasks`/`iter_tasks` and why
|
|
566
|
+
`create_task` returns an `Action`. `create_task`'s docstring now says so
|
|
567
|
+
where a reader meets it.
|
|
568
|
+
- Same file, tier 4: the effort-column measurements above, and two side
|
|
569
|
+
findings. `ACTION_LABEL_*` is the label of the workflow **step**, not the
|
|
570
|
+
name of the action type — type 20 appeared under six different labels — so
|
|
571
|
+
it is a stable type name only for the non-workflow types. And types 14, 27
|
|
572
|
+
and 28 have an empty label in every language column at both list and item
|
|
573
|
+
level, so those ids cannot be named through the API at all (`O-ACTIONTYPE28`).
|
|
574
|
+
|
|
12
575
|
## [0.2.0] - 2026-09-02
|
|
13
576
|
|
|
14
577
|
The first release since `0.1.0`. A `0.2.0` section was prepared and dated
|
|
@@ -1002,6 +1565,13 @@ the release is additive.
|
|
|
1002
1565
|
|
|
1003
1566
|
Initial public release.
|
|
1004
1567
|
|
|
1568
|
+
> **The tag is authoritative, and it disagrees with this date.** The `0.1.0` tag
|
|
1569
|
+
> points at `3216a33` (2026-08-04), roughly 150 commits after `6df6a75`, the
|
|
1570
|
+
> commit this section was written to describe. Per the policy above, `0.1.0`
|
|
1571
|
+
> **is** the tree at `3216a33`; the date on this heading and the scope below
|
|
1572
|
+
> under-describe it. The tag is published, so it is not being moved — this note
|
|
1573
|
+
> exists so the discrepancy is recorded rather than rediscovered.
|
|
1574
|
+
|
|
1005
1575
|
### Added
|
|
1006
1576
|
|
|
1007
1577
|
- Synchronous `EasyvistaClient` and asynchronous `AsyncEasyvistaClient` over the
|
|
@@ -1022,6 +1592,8 @@ Initial public release.
|
|
|
1022
1592
|
status/error code, with non-retryable validation errors (HTTP 590, code 2013).
|
|
1023
1593
|
- `py.typed` marker — the package ships inline type information.
|
|
1024
1594
|
|
|
1025
|
-
[Unreleased]: https://github.com/baraline/easyvista_python_client/compare/
|
|
1026
|
-
[0.
|
|
1595
|
+
[Unreleased]: https://github.com/baraline/easyvista_python_client/compare/0.4.0...HEAD
|
|
1596
|
+
[0.4.0]: https://github.com/baraline/easyvista_python_client/compare/0.3.0...0.4.0
|
|
1597
|
+
[0.3.0]: https://github.com/baraline/easyvista_python_client/compare/0.2.0...0.3.0
|
|
1598
|
+
[0.2.0]: https://github.com/baraline/easyvista_python_client/compare/0.1.0...0.2.0
|
|
1027
1599
|
[0.1.0]: https://github.com/baraline/easyvista_python_client/releases/tag/0.1.0
|
|
@@ -8,6 +8,10 @@ this page is the short version.
|
|
|
8
8
|
|
|
9
9
|
## Development Setup
|
|
10
10
|
|
|
11
|
+
The project needs Python 3.11 or newer. `requires-python` is `>=3.11` since
|
|
12
|
+
0.4.0, so a `.venv` created with 3.10 refuses the editable install below:
|
|
13
|
+
recreate it with a newer interpreter.
|
|
14
|
+
|
|
11
15
|
```bash
|
|
12
16
|
python -m venv .venv
|
|
13
17
|
.venv\Scripts\activate
|