easyvista-python-client 0.3.0__tar.gz → 0.4.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/.pre-commit-config.yaml +19 -1
  2. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/CHANGELOG.md +506 -1
  3. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/CONTRIBUTING.md +4 -0
  4. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/PKG-INFO +45 -12
  5. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/README.md +24 -6
  6. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/docs/api_reference.rst +32 -0
  7. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/docs/conf.py +1 -5
  8. easyvista_python_client-0.4.1/docs/content.rst +447 -0
  9. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/docs/development.rst +4 -0
  10. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/docs/index.rst +1 -0
  11. easyvista_python_client-0.4.1/docs/installation.rst +64 -0
  12. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/docs/publishing.rst +1 -1
  13. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/docs/user_guide.rst +166 -25
  14. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/docs/vendor-api-reference.md +172 -18
  15. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/__init__.py +6 -0
  16. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/_async/_transport.py +53 -1
  17. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/_async/client.py +296 -61
  18. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/_sync/_transport.py +53 -1
  19. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/_sync/client.py +296 -61
  20. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/_transport.py +22 -2
  21. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/_version.py +1 -1
  22. easyvista_python_client-0.4.1/easyvista_python_client/content/__init__.py +18 -0
  23. easyvista_python_client-0.4.1/easyvista_python_client/content/conversion.py +865 -0
  24. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/directory.py +2 -1
  25. easyvista_python_client-0.4.1/easyvista_python_client/exceptions.py +117 -0
  26. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/models/common.py +14 -7
  27. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/models/request.py +33 -18
  28. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/resources/actions.py +92 -3
  29. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/resources/requests.py +19 -40
  30. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/timestamps.py +19 -13
  31. easyvista_python_client-0.4.1/easyvista_python_client/workflow.py +337 -0
  32. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/pyproject.toml +42 -8
  33. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/skills/easyvista-asset-workflow/SKILL.md +2 -2
  34. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/skills/easyvista-client-setup/SKILL.md +20 -9
  35. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/skills/easyvista-directory/SKILL.md +2 -2
  36. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/skills/easyvista-document-workflow/SKILL.md +2 -2
  37. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/skills/easyvista-instance-discovery/SKILL.md +7 -5
  38. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/skills/easyvista-reporting-and-context/SKILL.md +2 -2
  39. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/skills/easyvista-search-syntax/SKILL.md +2 -2
  40. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/skills/easyvista-ticket-actions/SKILL.md +144 -23
  41. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/skills/easyvista-ticket-workflow/SKILL.md +82 -7
  42. easyvista_python_client-0.3.0/docs/installation.rst +0 -46
  43. easyvista_python_client-0.3.0/easyvista_python_client/exceptions.py +0 -53
  44. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/.gitignore +0 -0
  45. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/.readthedocs.yaml +0 -0
  46. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/LICENSE +0 -0
  47. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/docs/_static/.gitkeep +0 -0
  48. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/docs/_templates/.gitkeep +0 -0
  49. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/docs/sponsoring.rst +0 -0
  50. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/_async/__init__.py +0 -0
  51. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/_async/_concurrency.py +0 -0
  52. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/_fields.py +0 -0
  53. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/_html.py +0 -0
  54. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/_sync/__init__.py +0 -0
  55. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/_sync/_concurrency.py +0 -0
  56. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/config.py +0 -0
  57. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/context.py +0 -0
  58. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/discovery.py +0 -0
  59. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/field_model.py +0 -0
  60. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/filters.py +0 -0
  61. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/models/__init__.py +0 -0
  62. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/models/action.py +0 -0
  63. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/models/asset.py +0 -0
  64. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/models/department.py +0 -0
  65. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/models/document.py +0 -0
  66. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/models/employee.py +0 -0
  67. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/models/generic.py +0 -0
  68. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/pagination.py +0 -0
  69. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/py.typed +0 -0
  70. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/references.py +0 -0
  71. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/reporting.py +0 -0
  72. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/resources/__init__.py +0 -0
  73. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/resources/assets.py +0 -0
  74. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/resources/departments.py +0 -0
  75. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/resources/descriptor.py +0 -0
  76. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/resources/discovery.py +0 -0
  77. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/resources/documents.py +0 -0
  78. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/easyvista_python_client/resources/employees.py +0 -0
  79. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.1}/skills/README.md +0 -0
@@ -15,7 +15,25 @@ repos:
15
15
  rev: v2.3.0
16
16
  hooks:
17
17
  - id: mypy
18
- additional_dependencies: ["pydantic>=2.8", "httpx", "tenacity"]
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
@@ -15,6 +15,509 @@ is the error. Tags carry no `v` prefix.
15
15
 
16
16
  ## [Unreleased]
17
17
 
18
+ ## [0.4.1] - 2026-10-02
19
+
20
+ A patch release of the content reader. A `<u>`, `<mark>` or `<ins>` round a
21
+ block no longer reads as broken Markdown, four guards of 0.4.0's fixes gain
22
+ the tests they lacked, and `docs/content.rst` corrects what it said about
23
+ `start` values, `ValueError`, the CVE-2025-6069 tail and a second round
24
+ trip. Writing is unchanged, and so is everything outside
25
+ `easyvista_python_client.content`. `glpi_python_client`'s port of this
26
+ converter makes the same correction.
27
+
28
+ ### Fixed
29
+
30
+ - **A block inside `<u>`, `<mark>` or `<ins>` read as broken Markdown**, since
31
+ 0.4.0 kept these tags raw (fix 11). Markdown has no inline tag round blocks:
32
+ a table inside one read as pipe text, a list, a heading, a quote or a rule
33
+ as its Markdown source, paragraphs were not a fixed point, and
34
+ `<u><pre>code</pre></u>` left a fence open that showed the rest of the memo
35
+ as code. Round a block the tag is now dropped and the blocks kept, as
36
+ `glpi_python_client`'s reader at `917f030` read them. Inside a table cell
37
+ or a heading, whose blocks are one line, the tag stays, and around inline
38
+ content nothing changes. The Markdown read from such a memo changes once.
39
+
40
+ ### Documentation
41
+
42
+ - `docs/content.rst`, corrected after a review of the converter:
43
+ - a `start` that is not a decimal number counts from 1 as a browser
44
+ counts one holding no digit; a browser reads `" 3"`, `"+3"` or
45
+ `"3abc"` as 3, which the converter counts from 1;
46
+ - any `ValueError` raised while converting degrades the memo to text, not
47
+ only an unreadable `colspan` or `start` (the module and
48
+ `from_transport` docstrings say so too);
49
+ - the CVE-2025-6069 note says that everything after a memo's last `>`
50
+ reads as text, an unterminated comment and a closing tag cut short
51
+ inside a link included, where a patched CPython drops some of it;
52
+ - a second write-and-read cycle changes nothing more except for two
53
+ shapes, adjacent lists with different bullets and a fence whose info
54
+ string holds a character reference, where 0.4.0's page and its
55
+ changelog said it changes nothing;
56
+ - `<u>`, `<mark>` and `<ins>` round a block, and a known hole for
57
+ `<b>`, `<em>` and `<s>` round blocks.
58
+ - The `easyvista-ticket-actions` skill says a `<u>` round a block is
59
+ dropped.
60
+
61
+ ### Notes
62
+
63
+ - Four guards of the 0.4.0 fixes that no test caught are pinned, each by a
64
+ test that fails without it: a second `<` before a space keeps one escape
65
+ (fix 5), a header cell escapes a `|` in `<kbd>` or `<samp>` and splits a
66
+ line break in inline code (fix 6), a table inside an `<a>` without `href`
67
+ stays a table (fix 7), and an ordered item numbered 10 or more indents its
68
+ content by its bullet's width (fix 12). A count pins that the walk finding
69
+ blocks inside `<u>`, `<mark>` and `<ins>` checks each tag about once.
70
+
71
+ ## [0.4.0] - 2026-10-02
72
+
73
+ Adds Markdown <-> memo HTML conversion as an optional extra, drops Python
74
+ 3.10, and **stops a ticket's workflow changing by accident -- which is a
75
+ breaking release.** `set_status` is gone (it was the vendor close request under
76
+ a name that hid it), `close_ticket` requires an explicit opt-in, and every
77
+ write the package can tell may change a ticket's workflow is refused before it
78
+ is sent unless the call says so. What it cannot tell is not covered (see
79
+ *Notes*).
80
+
81
+ A `0.4.0` section was first prepared on 2026-09-30 around a different
82
+ converter. It was never tagged or uploaded, and this section replaces it;
83
+ `### Changed since the 2026-09-30 preparation` says what moved, for anyone who
84
+ built against that branch.
85
+
86
+ **Upgrading.** **Python 3.10 is no longer supported** (see `### Removed`). On
87
+ 3.10, pip keeps resolving `0.3.0`, the last release that installs there. The
88
+ converter is purely additive: the core package imports none of the extra's
89
+ dependencies. Every other breaking change is in the workflow guard, and each is
90
+ marked `**BREAKING**` in `### Changed` or `### Removed`. Read `### Upgrading`
91
+ below.
92
+
93
+ ### Added
94
+
95
+ - **The workflow guard.** `WorkflowEffect` (`INTERRUPTS`, `ADVANCES`,
96
+ `UNKNOWN`) and `EasyvistaWorkflowEffectRefused`, both exported at the package
97
+ root, and `easyvista_python_client.workflow` (`workflow_triggers`,
98
+ `classify_workflow_effects`), which names what a write may do to a ticket's
99
+ workflow. `EasyvistaWorkflowEffectRefused` is a `ValueError`, **not** an
100
+ `EasyvistaError`: the refused write is never sent, so there is no status code
101
+ and nothing transient, and it carries `effects` and `triggers` (what named
102
+ them). A ticket's status follows its workflow -- "Advancing through the steps
103
+ of a workflow changes the status of a ticket." (tier 1) -- so a write that
104
+ touches workflow state is not a bookkeeping write.
105
+ - An `allow_workflow_effect=` keyword, taking one `WorkflowEffect` or an
106
+ iterable of them, on `send`, `update_ticket`, `create_action`, `create_task`,
107
+ `update_action` and `end_action` (default: allows nothing), and **required**
108
+ on `close_ticket`. `RequestSpec.allow_workflow_effect` and
109
+ `RequestSpec.allowing()` carry the same opt-in on a request spec.
110
+ - `resources.actions.build_get_action(..., fields=...)` projects the item read,
111
+ as the list builders already did.
112
+ - `reassign_action(action_id, *, group_id=None, done_by_id=None)` on both
113
+ clients, with `resources.actions.build_reassign_action`: reassign an action
114
+ (for example, escalate the open workflow step to another group) without
115
+ ending it. It is not refused by the workflow guard. The vendor documents no
116
+ reassignment route (tier 1), so the effect was measured: 2026-10-02, one
117
+ instance, two tickets, so it may not generalise -- the group was stored, the
118
+ step stayed open, the ticket's status did not move and no new action rows
119
+ appeared; the ticket's owning group was read on one of the two tickets and
120
+ did not follow the action's. The person write (`done_by_id`) is unmeasured.
121
+ - `easyvista_python_client.content.EasyvistaContentConverter`, behind the new
122
+ optional extra `easyvista-python-client[content]`. It has two static
123
+ methods. `from_transport(value, *, plain_text_is_markdown=False)` reads a
124
+ memo, rich-text HTML or plain text, as Markdown. `to_transport(value)`
125
+ renders Markdown as the HTML a memo is written with.
126
+
127
+ **The Markdown is CommonMark with GFM tables.** Writing renders it with
128
+ `cmark-gfm`, with only its `table` extension: a newline is a line break, and
129
+ raw HTML passes through. Reading converts HTML with `markdownify`, then
130
+ re-renders the result with `mdformat`, which keeps only the escapes
131
+ CommonMark needs.
132
+
133
+ **Text in a memo is literal, and the Markdown spells it so.** A typed
134
+ `__init__` reads as `\_\_init\_\_`, `\\serveur` as `\\\serveur`, `# titre`
135
+ at a line start as `\# titre`, and `<Entrée>` as `\<Entrée>`. Ordinary prose
136
+ such as `fichier_de_test_v2.xlsx`, `C:\Temp`, `R&D` or `Ticket #4521` comes
137
+ back as typed. The aim is that rendering the Markdown displays what the
138
+ memo displayed, and that reading that back gives the same Markdown; the
139
+ measurements below say how far that held, and `docs/content.rst` lists the
140
+ shapes where it does not.
141
+
142
+ What else a reader sees:
143
+ - A memo with no HTML element is read as literal lines, a hard break per
144
+ line. Pass `plain_text_is_markdown=True` for a value that is your own
145
+ Markdown: it then passes through, stripped, unless it starts with `<` and
146
+ holds a real HTML element anywhere. So Markdown opening with an autolink
147
+ and carrying inline HTML further on is read as HTML, and loses the
148
+ autolink.
149
+ - A memo holding one real HTML element is read as HTML throughout, so
150
+ Markdown syntax beside it is kept as literal characters. The obsolete
151
+ elements (`<font>`, `<center>`, ...) count as HTML.
152
+ - Underline, highlight and strike, which CommonMark cannot spell, are kept
153
+ as raw `<u>`, `<mark>`, `<ins>` and `<s>`.
154
+ - A table nested in a table is flattened to its words, every one kept and in
155
+ order, and a table without a header row gains an empty one.
156
+ - Nested lists nest and keep their numbers. `<script>`, `<style>`,
157
+ `<template>` and `<title>` bodies are dropped.
158
+
159
+ Measured 2026-10-02 on 367 memos read from one preproduction instance on
160
+ 2026-10-01 (tier 4, one instance, may not generalise): no memo changed a
161
+ word, and 367 of 367 readings were fixed points. 65 of them displayed
162
+ differently from the memo, each in a documented way that keeps its words:
163
+ in 62 a table gained an empty header row (61 of them a notification
164
+ template's flattened nested table), and 3 had no HTML element and were
165
+ read as literal lines. Synthetic shapes do lose words: a table row longer
166
+ than the first, a `|` in a cell's code or link, an image title holding a
167
+ `"`, and others that `docs/content.rst` lists.
168
+
169
+ It is a port of `glpi_python_client`'s `content/conversion.py` at `917f030`,
170
+ plus 15 fixes listed under `### Notes`. **The two should move together.**
171
+
172
+ Why it is here: until now this package could only strip HTML to plain text,
173
+ so a downstream GLPI-to-EasyVista sync wrote its own converter. Measured
174
+ 2026-09-30 (tier 4, one instance), a GLPI description holding a pasted URL
175
+ and a titled link reached EasyVista with neither link clickable. EasyVista
176
+ stored exactly the HTML it was sent. That converter had escaped `&lt;` a
177
+ second time, fused the link title into the `href`, cut a URL at its first
178
+ `)`, and would read two lone asterisks as emphasis. This converter handles
179
+ each of those correctly (checked 2026-10-02 on synthetic equivalents): a
180
+ `&lt;` is escaped once, a link title stays a `title`, a URL keeps its
181
+ parentheses, and a lone `*` stays literal.
182
+
183
+ Two properties are worth knowing before relying on it:
184
+ - **Deep nesting degrades to text.** The conversion is attempted, and a
185
+ `RecursionError` is answered with the memo's words. From a shallow stack,
186
+ the deepest `<div>` document that still converts with its structure is 245
187
+ levels on CPython 3.11 and 327 on 3.12–3.14 (measured 2026-10-02). A caller
188
+ already within about 25 frames of the recursion limit still gets
189
+ `EasyvistaContentError`, and within the last few frames, a bare
190
+ `RecursionError`.
191
+ - **It is not a sanitiser.** Raw HTML, `javascript:` link targets and
192
+ `<javascript:...>` autolinks in the Markdown all go out live, and reading
193
+ keeps a memo's `javascript:` links. A caller relaying Markdown it did not
194
+ write must neutralise raw HTML and executable schemes. Text a memo
195
+ *displays* as markup, such as `&lt;script&gt;`, reads back escaped, as the
196
+ text it is.
197
+
198
+ Importing `easyvista_python_client.content` without the extra, or with any
199
+ one of its six packages missing, raises an `ImportError` naming
200
+ `pip install "easyvista-python-client[content]"`.
201
+ - `EasyvistaContentError`, a subclass of `EasyvistaError` exported at the
202
+ package root. It is raised for any converter fault the text fallback does
203
+ not absorb, with the underlying exception as `__cause__`. It lives in the
204
+ core package, so it can be caught whether or not the extra is installed.
205
+ - The `dev` and `docs` extras now install the `content` extra's packages, with
206
+ the same bounds, since CI runs the converter's tests and the API reference
207
+ imports it. `testing/test_public_api.py` fails if the copies drift, or if
208
+ its list of the extra's import names stops matching `pyproject.toml`.
209
+
210
+ ### Changed
211
+
212
+ - **BREAKING** `close_ticket` requires `allow_workflow_effect=`, keyword-only
213
+ with no default: leaving it out is a `TypeError`, and a value that does not
214
+ include `WorkflowEffect.INTERRUPTS` is refused with
215
+ `EasyvistaWorkflowEffectRefused` before any request is made. The vendor
216
+ documents the close request as interrupting the workflow (tier 1, see
217
+ *Removed*), so the call site now says that it means to.
218
+ - **BREAKING** `end_action` is guarded. Unless `allow_workflow_effect` includes
219
+ `WorkflowEffect.ADVANCES` it first reads the action (one item read projecting
220
+ `ACTION_ID` and `WORKFLOW_ID`) and refuses, with no end request sent, when
221
+ the action is a workflow step (`WORKFLOW_ID` set), when the record comes back
222
+ without a `WORKFLOW_ID` column (which cannot be told from a step), or when
223
+ the read returns a different `ACTION_ID` than the one asked for. `end_all=True`
224
+ always needs `ADVANCES`. Ending an action you created yourself needs no
225
+ opt-in (see *Notes*). If the read fails, its error propagates and nothing is ended.
226
+ Ending a workflow step moves the workflow on -- vendor-documented only by the
227
+ UI's Finish wizard, and measured on one instance on 2026-09-01 (2 of 2), so
228
+ it may not generalise.
229
+ - **BREAKING** `end_action`'s explicit `action_id` must be a positive integer.
230
+ `0`, negatives, blanks, RFC numbers, floats and booleans now raise
231
+ `ValueError` before any request, and a numeric string is sent as an integer
232
+ (`"123"` goes out as `123`). `action_id=None` is still refused unless
233
+ `end_all=True`.
234
+ - **BREAKING** `update_ticket`, `update_action`, `create_action`, `create_task`
235
+ and `send` refuse a body or route that may change the workflow unless the call
236
+ passes `allow_workflow_effect=`. Named: the workflow-control bodies `closed`,
237
+ `end_action`, `suspended` and `restarted` as a top-level key in any casing on
238
+ any path; on a `requests/{rfc}` route, the status, catalog and parent-request
239
+ columns and a `DELETE`; on an existing `actions/{id}` route, the end date,
240
+ type, parent, ticket and workflow, stage and step columns (creating an action
241
+ or a task names a narrower set); a write to `actions/<x>` where `<x>` is not an
242
+ integer id, which is the vendor's end-action route `PUT
243
+ actions/{rfc_number}`, whatever the body says; every ticket sub-route that is
244
+ a command rather than a record (`close`, `suspend`, `restart`,
245
+ `workflowstart`, ...) and `requests/without-workflow`. The column rules apply
246
+ to the `requests/` and `actions/` routes only: a route of any other family is
247
+ not classified by column. What the typed models declare needs no opt-in,
248
+ and neither do text, owner, group, done-by, impact or urgency. The exact
249
+ lists are in `docs/vendor-api-reference.md`, "Ticket workflow". **This is a
250
+ deny-list, and a deny-list of columns cannot be complete**: what is not named
251
+ is unclassified, not proven neutral.
252
+ - **BREAKING** `update_action` refuses an action id that is not a positive
253
+ integer, `None` included: `PUT actions/{rfc_number}` is the end-action route
254
+ on the same path, so an RFC number would not edit an action.
255
+ - **BREAKING** The spec builders changed with the guard. A spec from
256
+ `resources.requests.build_close_ticket` or `resources.actions.build_end_action`
257
+ names a workflow effect, so this package's transport refuses it until it is
258
+ passed through `.allowing(...)`; code that builds one and sends it itself must
259
+ say so. And `resources.actions.build_end_action` and `build_update_action`
260
+ now raise `ValueError` at build time for an action id that is not a positive
261
+ integer, where they formerly passed the id through as given.
262
+ - **BREAKING** `send()` -- the path every typed method and the client's own
263
+ `send` go through -- refuses outright, with `ValueError` and whatever the
264
+ method or opt-in, a path containing a dot segment (`.` or `..`), a
265
+ percent-encoded slash or backslash, or a raw backslash: the HTTP client
266
+ collapses a dot segment, and a server may read the others as a separator, so
267
+ the request could reach a route other than the one that was checked. No API
268
+ route needs one. Whether this server reads them as separators is not
269
+ measured; the check fails closed. Document downloads (`get_bytes`,
270
+ `stream_bytes`) are reads and are never gated.
271
+ - **BREAKING** A request is treated as a read only when its method **and** the
272
+ value of every method-override header (`X-HTTP-Method-Override`,
273
+ `X-HTTP-Method`, `X-Method-Override`) on the request are reads, so such a
274
+ header cannot hide a write behind a `GET`: a request whose override header
275
+ names a write is classified as that write, and refused if it names a workflow
276
+ effect and the call did not allow it, whatever method it is sent as. The
277
+ headers read are the ones that go on the wire, `config.extra_headers` with
278
+ the request's own laid over them. Whether this API honours these headers is
279
+ not recorded.
280
+ - **BREAKING** A write that names a workflow effect and is allowed is sent
281
+ **once**, never retried: `close_ticket` and `end_action` formerly retried a
282
+ 429, a 5xx or a connection error when `max_retries` was above its default of
283
+ `0`, and now raise after the first attempt. Each close inserts another
284
+ anticipated closing action and each end ends whatever is open, so a resend
285
+ after a lost response is not a repeat of the same request. This includes
286
+ `end_action` on your own action. Every other request keeps its retries.
287
+
288
+ ### Removed
289
+
290
+ - **BREAKING** — Python 3.10. `requires-python` is now `>=3.11`, the 3.10
291
+ classifier is gone, and the CI and release matrices run 3.11–3.14. CPython
292
+ 3.10 reaches end of life in October 2026 (PEP 619), and `glpi_python_client`,
293
+ whose converter `content` ports, dropped it in `524304a`. Two requirements
294
+ that existed only for 3.10 go with it: `typing-extensions` (a runtime
295
+ dependency on 3.10 only) and `tomli` (in `dev` and `docs`). Nothing moves on
296
+ 3.11 and later. The timestamp parser keeps the normalisation it carried for
297
+ 3.10, so it accepts and refuses the same values as before.
298
+ - **BREAKING** `EasyvistaClient.set_status` / `AsyncEasyvistaClient.set_status`
299
+ and `resources.requests.build_set_status`. They sent the vendor CLOSE request,
300
+ which the vendor close page documents (tier 1, re-read 2026-10-02) as
301
+ interrupting the workflow, setting the final status, deleting the unfinished
302
+ actions when `delete_actions` is set (otherwise, by the package's reading of
303
+ the page, ending them) and inserting an anticipated closing action, none of it
304
+ conditional on the status sent. The page documents final statuses only, so
305
+ for a non-final one that is an extrapolation it neither exempts nor covers.
306
+ A synchroniser that used `set_status` to mirror an intermediate status closed
307
+ tickets early. The root cause was established on 2026-10-01/02 from the
308
+ synchroniser's code (it sent the close request right after every create and on
309
+ every status push) and from the vendor page; the drain of the open workflow
310
+ action across such a write was measured on one ticket (2026-09-01, one
311
+ instance, tier 4, so it may not generalise). The vendor documents no status
312
+ setter and this package has none: a flat status update is excluded from the
313
+ vendor's ticket update body (tier 1) and was seen to return 200 while dropping
314
+ the status (0.2.0 entry below), and a ticket's status follows its workflow.
315
+ `close_ticket` and `resources.requests.build_close_ticket` remain.
316
+
317
+ ### Changed since the 2026-09-30 preparation
318
+
319
+ None of this reached PyPI, so it is not a break for anyone upgrading from
320
+ `0.3.0`. It matters to a caller who built against the branch.
321
+
322
+ - **The converter changed design.** The first preparation ported
323
+ `glpi_python_client` at `4fc3bed`, built on python-markdown with its
324
+ `nl2br`, `sane_lists`, `fenced_code` and `tables` extensions.
325
+ `glpi_python_client` replaced that design in `917f030` with markdownify +
326
+ mdformat inbound and cmark-gfm outbound, and this package follows it.
327
+ - **The `content` extra's dependencies changed.** `markdown` is gone, and
328
+ `cmarkgfm`, `markdown-it-py`, `mdformat` and `mdformat-tables` are new. The
329
+ bounds are now `beautifulsoup4>=4.15`, `cmarkgfm>=2025.10.22`,
330
+ `markdown-it-py>=3.0,<4`, `markdownify>=1.2.3,<1.3`, `mdformat>=0.7.22,<0.8`
331
+ and `mdformat-tables>=1.0,<1.1`. The upper bounds are deliberate, because
332
+ the reader overrides private surfaces of `markdownify` and `mdformat`.
333
+ `docs/content.rst` gives the reason for each bound.
334
+ - **The Markdown dialect is CommonMark with GFM tables**, not
335
+ python-markdown's. What a caller sees:
336
+ - A fence's language tag now survives a round trip.
337
+ - Strong inside emphasis keeps its bold.
338
+ - Two `<br>` in a row stay two line breaks.
339
+ - A `<pre>` opening a list item stays code.
340
+ - Struck text is kept as a raw `<s>` instead of losing the line.
341
+ - `~~text~~` is literal tildes on write, as before.
342
+ - `<javascript:alert(1)>` now becomes a live link, where python-markdown left
343
+ it as raw markup.
344
+ - **Escapes differ.** A displayed `<` reads as `\<` rather than `&lt;`. `#4521`
345
+ at a line start is no longer escaped, since CommonMark needs a space after
346
+ `#`. A hard break reads as a trailing backslash rather than two spaces. Link
347
+ targets are kept in `<...>` where they need it, and a `[` or `]` in one comes
348
+ back percent-encoded.
349
+ - **Plain text is read as literal lines.** A memo with no HTML element used to
350
+ pass through verbatim, so its text was read as Markdown and a typed
351
+ `__init__` rendered bold. It is now escaped, and each line kept with a hard
352
+ break. How EasyVista's UI displays such a memo is unverified (`O-MEMOFORMAT`).
353
+ `plain_text_is_markdown=True` restores pass-through for your own Markdown.
354
+ - **The depth cliff moved.** With the old design it was measured at 493 levels
355
+ on CPython 3.12–3.14. With this one it is 327 there, and 245 on 3.11.
356
+ - **The `beautifulsoup4` workaround is gone.** The old design rewrote
357
+ self-closing void tags to dodge a defect that dropped the text after a
358
+ `<br />` in a body that also used `<br>`. That defect no longer reproduced on
359
+ 4.15.0 (measured 2026-09-30), so the floor is now 4.15 and the workaround is
360
+ removed.
361
+
362
+ ### Upgrading
363
+
364
+ - `client.set_status(rfc, status_guid=g)` -- delete it; nothing replaces it,
365
+ because the vendor documents no status setter (a flat status update is
366
+ excluded from its ticket update body, and was seen to drop the status -- see
367
+ 0.2.0) and a ticket's status follows its workflow. To move a ticket through
368
+ its workflow, end the step's open action with
369
+ `end_action(rfc, action_id=..., allow_workflow_effect=WorkflowEffect.ADVANCES)`.
370
+ The status that follows is the workflow's, not yours to choose. This is not
371
+ documented on the REST page, which is silent about the workflow; it was
372
+ measured on one instance on 2026-09-01 (2 of 2 tickets) and may not
373
+ generalise, so re-read the ticket afterwards. To close, call
374
+ `close_ticket(rfc, allow_workflow_effect=WorkflowEffect.INTERRUPTS, status_guid=g)`.
375
+ - `end_action` callers: pass the integer id of an action you read, never an RFC
376
+ number, `0` or a blank. Ending your own action still needs no opt-in (see
377
+ *Notes*), and now costs one extra item read. Ending a workflow step, or any action whose record
378
+ shows no `WORKFLOW_ID`, needs `WorkflowEffect.ADVANCES`, and so does
379
+ `end_all=True`.
380
+ - Code that puts a status, catalog or other named column into `extra_payload`,
381
+ or calls `send` with a workflow route or body, now raises until it passes
382
+ `allow_workflow_effect=`. `WorkflowEffect.UNKNOWN` means undocumented, not
383
+ harmless: read "Ticket workflow" in `docs/vendor-api-reference.md` first.
384
+ - Code that builds a close or end-action spec with `build_close_ticket` or
385
+ `build_end_action` and sends it through the transport itself: pass the spec
386
+ through `.allowing(...)` first, and pass `build_end_action` and
387
+ `build_update_action` an integer action id, never an RFC number.
388
+ - Catch `EasyvistaWorkflowEffectRefused` (or `ValueError`) where you record
389
+ per-record failures. It is a `ValueError`, **not** an `EasyvistaError`, and
390
+ carries no status code; it is never transient. Code that catches
391
+ `EasyvistaError` around a close for cleanup will NOT catch it. `ValueError`
392
+ also catches the other local refusals above.
393
+ - If you set `max_retries` above its default of `0`, a lost response to an
394
+ allowed workflow write now surfaces as an error instead of a silent resend.
395
+ Re-read the ticket before repeating it.
396
+ - The minor bump is deliberate: a dependant pinned `>=0.3.0,<0.4` does **not**
397
+ pick this up, and must widen its constraint on purpose.
398
+
399
+ ### Documentation
400
+
401
+ - `docs/vendor-api-reference.md` gains "Ticket workflow": what each documented
402
+ write does to a workflow, with the vendor page quoted (tier 1), the guard's
403
+ deny-list exactly as the code holds it, and the measurements labelled tier 4
404
+ with their instance and date. O-CLOSE-DEFAULT is closed at tier 1: the vendor
405
+ close page documents an omitted `status_GUID` as defaulting to the Closed
406
+ meta-status. It was not measured here.
407
+ - The README, the user guide, the API reference and the `easyvista-client-setup`,
408
+ `easyvista-ticket-actions` and `easyvista-ticket-workflow` skills describe the
409
+ guard; the `easyvista-instance-discovery` skill now says `close_ticket` stops
410
+ the workflow and is not a way to pick an intermediate status. The
411
+ ticket-workflow skill's first gotcha is now that `close_ticket` is not a
412
+ status setter.
413
+ - The `PostRequest.workflow_start` docstring records that the flag is a no-op
414
+ (tier 4: two tickets identical but for it came back byte-identical, 2026-09-01,
415
+ one instance), so `workflow_start=False` does not create a ticket without its
416
+ workflow. The vendor create page documents no such parameter and states that
417
+ the workflow is started; the workflow-less create is the virtual-agent route
418
+ `requests/without-workflow`, which the guard refuses unless allowed.
419
+ - **Retracted:** that `set_status` reaches every status, as the 0.2.0 entry
420
+ below put it ("a fresh ticket landed on exactly the status requested every
421
+ time, non-terminal ones included") and the `RequestUpdate` docstring repeated
422
+ it ("That route reaches **every** status, not just terminal ones"). Six status
423
+ GUIDs were tried and each landed, but that was measured by re-reading the
424
+ status only: the measurement never looked at the workflow or the ticket's
425
+ open actions, and the vendor close page documents the close request as
426
+ interrupting the workflow (tier 1). A status that landed is not evidence that
427
+ nothing else moved, and that finding was read as a safe status setter, which
428
+ it was not. The 0.2.0 section is left as written.
429
+ - `docs/content.rst` is a user-guide page for the converter. It covers what a
430
+ memo holds, what each direction reads and writes, what text is escaped and
431
+ why, and what survives a round trip and what is lost. It also covers what
432
+ the converter does not sanitise, the dependency bounds, and the
433
+ CVE-2025-6069 guard. The API reference gains a "Rich-text content" section.
434
+ - `docs/vendor-api-reference.md` records the memo format as tier 4 (above),
435
+ and the vendor's MEMO and TEXT AREA form objects as tier 1
436
+ (`docs.easyvista.com/docs/form`, read 2026-10-02). That page does not say
437
+ which object a ticket's or an action's memo is. A second tier-1 page
438
+ (`docs.easyvista.com/docs/service-manager-comment-log-creation`, read
439
+ 2026-10-02) sets a custom comment-log field on the request form to "Text
440
+ area", which leans towards HTML display but is not about the built-in
441
+ memos. It also opens `O-MEMOFORMAT` for what is not yet known:
442
+ - how the UI displays a memo with no HTML element;
443
+ - what the web UI's own editor writes;
444
+ - how the UI treats the newlines between blocks;
445
+ - what it shows for raw markup;
446
+ - whether an empty table header row is visible;
447
+ - whether a percent-encoded link target still resolves.
448
+ - The `easyvista-ticket-workflow` and `easyvista-ticket-actions` skills each
449
+ gain a gotcha: a memo stores what it is sent, nothing renders Markdown for
450
+ you, and the converter's dialect is CommonMark with GFM tables.
451
+ - `docs/installation.rst`, `docs/development.rst`, `docs/publishing.rst`,
452
+ `README.md`, `CONTRIBUTING.md` and every skill's `compatibility` line now
453
+ state the Python 3.11 floor.
454
+
455
+ ### Notes
456
+
457
+ - What the workflow guard does not establish. It is a deny-list: the vendor's
458
+ update pages accept every column of the ticket and action tables except an
459
+ excluded list (tier 1), and a per-instance business rule can fire on
460
+ any write, so a write the guard does not name is unclassified, not proven
461
+ neutral. `end_action` tells a workflow step from your own action by
462
+ `WORKFLOW_ID` (1500 of 1500 rows, 2026-09-02, one instance); whether an
463
+ action created under a step by `create_action` carries one is unmeasured, and
464
+ if it does, ending it is refused too -- the safe direction. The live checks
465
+ in `integration_tests/test_live_workflow_guard.py` are gated: its first two
466
+ tests read only, and the rest write and run only when
467
+ `EASYVISTA_TEST_RUN_WORKFLOW_CENSUS=1` is set.
468
+ - **The 15 fixes on top of `glpi_python_client` `917f030`**, measured
469
+ together on this package's synthetic corpus and on the 367-memo sample, and
470
+ still to be proposed to `glpi_python_client`:
471
+ 1. Escapes and character references in an image's alt text are kept.
472
+ markdown-it-py 3 leaves them as `text_special` tokens, which mdformat
473
+ rendered as nothing.
474
+ 2. A paragraph line opening with `~~~` is escaped, because CommonMark reads
475
+ it as a fence.
476
+ 3. A `!` that ends a text before a link is escaped, so it does not turn the
477
+ link into an image.
478
+ 4. An alt text starting with `^` is escaped, because cmark-gfm never opens
479
+ an image on `![^`.
480
+ 5. A `<` that mdformat leaves bare after an escaped one is escaped, so
481
+ `<<a@b.c>` no longer becomes an autolink.
482
+ 6. A `<br>` inside inline code (`<code>`, `<kbd>`, `<samp>`) splits the
483
+ span, and a `|` in such a span in a table cell is escaped.
484
+ 7. A table inside a heading or a link is flattened, as one inside a cell
485
+ already was.
486
+ 8. A block inside a flattened cell stays on its link's line.
487
+ 9. A flattened cell's edge counts as a space when deciding whether `**`
488
+ closes.
489
+ 10. `<center>` is a block, as `<div>` is, except inside `<pre>`.
490
+ 11. `<u>`, `<mark>` and `<ins>` are kept as raw tags.
491
+ 12. Ordered items are numbered in linear time, where counting every earlier
492
+ item was quadratic, and `start` is read with `isdecimal`.
493
+ 13. The pattern that splits emphasis and links into edges and content is
494
+ linear on runs of spaces or `<br>`. It was quadratic.
495
+ 14. A `colspan` or `start` that is not a decimal number, such as `"²"` or
496
+ 5,000 digits, degrades the memo to text instead of failing it.
497
+ 15. Every `<` after a memo's last `>` is read as text. CPython's
498
+ `html.parser` before 3.11.14, 3.12.12 and 3.13.6 is quadratic on a run
499
+ of unfinished tags there (CVE-2025-6069, python/cpython#135462).
500
+ - **What does not round-trip is documented, not hidden.** `docs/content.rst`
501
+ lists both sides.
502
+ - Markdown written and read back comes back in mdformat's canonical
503
+ spelling. Text in angle brackets that is not a URL is lost. An e-mail
504
+ autolink becomes a `mailto:` link, and `[`/`]` in a link target come back
505
+ percent-encoded.
506
+ - A memo read and written back kept every word on the 367-memo sample. A
507
+ nested table's grid becomes text in its cell, and a header-less table
508
+ gains an empty header row. The known holes, all synthetic and absent
509
+ from the sample, are listed too, the ones that lose words among them.
510
+
511
+ After one write-and-read cycle, a further one changes nothing more.
512
+ - `cmarkgfm` 2025.10.22 bundles cmark-gfm `0.29.0.gfm.13`. PyPI lists no wheel
513
+ of that release for macOS x86_64 or Windows ARM64 (read 2026-10-02). Unless a
514
+ later release adds one, it builds from source there, which needs a C
515
+ compiler.
516
+ - mdformat 1.x, markdown-it-py 4.x and mdformat-gfm 1.0 were tried and are not
517
+ adopted. markdown-it-py 4 caps the cells it fills in, which ends a ragged
518
+ table early and turns the rest into literal pipes. Re-measure before raising
519
+ those bounds.
520
+
18
521
  ## [0.3.0] - 2026-09-02
19
522
 
20
523
  Makes the task-vs-action distinction inspectable. A GLPI comment corresponds to
@@ -1142,7 +1645,9 @@ Initial public release.
1142
1645
  status/error code, with non-retryable validation errors (HTTP 590, code 2013).
1143
1646
  - `py.typed` marker — the package ships inline type information.
1144
1647
 
1145
- [Unreleased]: https://github.com/baraline/easyvista_python_client/compare/0.3.0...HEAD
1648
+ [Unreleased]: https://github.com/baraline/easyvista_python_client/compare/0.4.1...HEAD
1649
+ [0.4.1]: https://github.com/baraline/easyvista_python_client/compare/0.4.0...0.4.1
1650
+ [0.4.0]: https://github.com/baraline/easyvista_python_client/compare/0.3.0...0.4.0
1146
1651
  [0.3.0]: https://github.com/baraline/easyvista_python_client/compare/0.2.0...0.3.0
1147
1652
  [0.2.0]: https://github.com/baraline/easyvista_python_client/compare/0.1.0...0.2.0
1148
1653
  [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