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.
Files changed (80) hide show
  1. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/.pre-commit-config.yaml +19 -1
  2. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/CHANGELOG.md +576 -4
  3. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/CONTRIBUTING.md +4 -0
  4. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/PKG-INFO +49 -13
  5. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/README.md +28 -7
  6. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/docs/api_reference.rst +32 -0
  7. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/docs/conf.py +1 -5
  8. easyvista_python_client-0.4.0/docs/content.rst +425 -0
  9. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/docs/development.rst +4 -0
  10. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/docs/index.rst +1 -0
  11. easyvista_python_client-0.4.0/docs/installation.rst +64 -0
  12. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/docs/publishing.rst +18 -6
  13. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/docs/user_guide.rst +189 -32
  14. easyvista_python_client-0.4.0/docs/vendor-api-reference.md +509 -0
  15. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/__init__.py +6 -0
  16. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/_async/_transport.py +53 -1
  17. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/_async/client.py +317 -61
  18. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/_sync/_transport.py +53 -1
  19. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/_sync/client.py +317 -61
  20. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/_transport.py +22 -2
  21. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/_version.py +1 -1
  22. easyvista_python_client-0.4.0/easyvista_python_client/content/__init__.py +18 -0
  23. easyvista_python_client-0.4.0/easyvista_python_client/content/conversion.py +823 -0
  24. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/directory.py +2 -1
  25. easyvista_python_client-0.4.0/easyvista_python_client/exceptions.py +117 -0
  26. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/models/action.py +112 -0
  27. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/models/common.py +98 -7
  28. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/models/request.py +33 -18
  29. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/references.py +9 -1
  30. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/resources/actions.py +92 -3
  31. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/resources/requests.py +19 -40
  32. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/timestamps.py +19 -13
  33. easyvista_python_client-0.4.0/easyvista_python_client/workflow.py +337 -0
  34. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/pyproject.toml +42 -8
  35. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/skills/easyvista-asset-workflow/SKILL.md +2 -2
  36. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/skills/easyvista-client-setup/SKILL.md +20 -9
  37. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/skills/easyvista-directory/SKILL.md +2 -2
  38. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/skills/easyvista-document-workflow/SKILL.md +2 -2
  39. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/skills/easyvista-instance-discovery/SKILL.md +7 -5
  40. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/skills/easyvista-reporting-and-context/SKILL.md +2 -2
  41. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/skills/easyvista-search-syntax/SKILL.md +2 -2
  42. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/skills/easyvista-ticket-actions/SKILL.md +197 -23
  43. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/skills/easyvista-ticket-workflow/SKILL.md +82 -7
  44. easyvista_python_client-0.2.0/docs/installation.rst +0 -46
  45. easyvista_python_client-0.2.0/docs/vendor-api-reference.md +0 -224
  46. easyvista_python_client-0.2.0/easyvista_python_client/exceptions.py +0 -53
  47. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/.gitignore +0 -0
  48. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/.readthedocs.yaml +0 -0
  49. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/LICENSE +0 -0
  50. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/docs/_static/.gitkeep +0 -0
  51. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/docs/_templates/.gitkeep +0 -0
  52. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/docs/sponsoring.rst +0 -0
  53. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/_async/__init__.py +0 -0
  54. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/_async/_concurrency.py +0 -0
  55. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/_fields.py +0 -0
  56. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/_html.py +0 -0
  57. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/_sync/__init__.py +0 -0
  58. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/_sync/_concurrency.py +0 -0
  59. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/config.py +0 -0
  60. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/context.py +0 -0
  61. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/discovery.py +0 -0
  62. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/field_model.py +0 -0
  63. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/filters.py +0 -0
  64. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/models/__init__.py +0 -0
  65. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/models/asset.py +0 -0
  66. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/models/department.py +0 -0
  67. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/models/document.py +0 -0
  68. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/models/employee.py +0 -0
  69. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/models/generic.py +0 -0
  70. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/pagination.py +0 -0
  71. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/py.typed +0 -0
  72. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/reporting.py +0 -0
  73. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/resources/__init__.py +0 -0
  74. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/resources/assets.py +0 -0
  75. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/resources/departments.py +0 -0
  76. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/resources/descriptor.py +0 -0
  77. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/resources/discovery.py +0 -0
  78. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/resources/documents.py +0 -0
  79. {easyvista_python_client-0.2.0 → easyvista_python_client-0.4.0}/easyvista_python_client/resources/employees.py +0 -0
  80. {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
- 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
@@ -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 versions;
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 `&lt;` 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
+ `&lt;` 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 `&lt;script&gt;`, 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 `&lt;`. `#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/v0.2.0...HEAD
1026
- [0.2.0]: https://github.com/baraline/easyvista_python_client/compare/0.1.0...v0.2.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