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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/.pre-commit-config.yaml +19 -1
  2. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/CHANGELOG.md +452 -1
  3. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/CONTRIBUTING.md +4 -0
  4. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/PKG-INFO +45 -12
  5. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/README.md +24 -6
  6. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/docs/api_reference.rst +32 -0
  7. {easyvista_python_client-0.3.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.3.0 → easyvista_python_client-0.4.0}/docs/development.rst +4 -0
  10. {easyvista_python_client-0.3.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.3.0 → easyvista_python_client-0.4.0}/docs/publishing.rst +1 -1
  13. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/docs/user_guide.rst +166 -25
  14. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/docs/vendor-api-reference.md +172 -18
  15. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/easyvista_python_client/__init__.py +6 -0
  16. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/easyvista_python_client/_async/_transport.py +53 -1
  17. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/easyvista_python_client/_async/client.py +296 -61
  18. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/easyvista_python_client/_sync/_transport.py +53 -1
  19. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/easyvista_python_client/_sync/client.py +296 -61
  20. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/easyvista_python_client/_transport.py +22 -2
  21. {easyvista_python_client-0.3.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.3.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.3.0 → easyvista_python_client-0.4.0}/easyvista_python_client/models/common.py +14 -7
  27. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/easyvista_python_client/models/request.py +33 -18
  28. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/easyvista_python_client/resources/actions.py +92 -3
  29. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/easyvista_python_client/resources/requests.py +19 -40
  30. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/easyvista_python_client/timestamps.py +19 -13
  31. easyvista_python_client-0.4.0/easyvista_python_client/workflow.py +337 -0
  32. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/pyproject.toml +42 -8
  33. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/skills/easyvista-asset-workflow/SKILL.md +2 -2
  34. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/skills/easyvista-client-setup/SKILL.md +20 -9
  35. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/skills/easyvista-directory/SKILL.md +2 -2
  36. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/skills/easyvista-document-workflow/SKILL.md +2 -2
  37. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/skills/easyvista-instance-discovery/SKILL.md +7 -5
  38. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/skills/easyvista-reporting-and-context/SKILL.md +2 -2
  39. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/skills/easyvista-search-syntax/SKILL.md +2 -2
  40. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/skills/easyvista-ticket-actions/SKILL.md +143 -23
  41. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/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.0}/.gitignore +0 -0
  45. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/.readthedocs.yaml +0 -0
  46. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/LICENSE +0 -0
  47. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/docs/_static/.gitkeep +0 -0
  48. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/docs/_templates/.gitkeep +0 -0
  49. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/docs/sponsoring.rst +0 -0
  50. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/easyvista_python_client/_async/__init__.py +0 -0
  51. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/easyvista_python_client/_async/_concurrency.py +0 -0
  52. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/easyvista_python_client/_fields.py +0 -0
  53. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/easyvista_python_client/_html.py +0 -0
  54. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/easyvista_python_client/_sync/__init__.py +0 -0
  55. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/easyvista_python_client/_sync/_concurrency.py +0 -0
  56. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/easyvista_python_client/config.py +0 -0
  57. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/easyvista_python_client/context.py +0 -0
  58. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/easyvista_python_client/discovery.py +0 -0
  59. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/easyvista_python_client/field_model.py +0 -0
  60. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/easyvista_python_client/filters.py +0 -0
  61. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/easyvista_python_client/models/__init__.py +0 -0
  62. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/easyvista_python_client/models/action.py +0 -0
  63. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/easyvista_python_client/models/asset.py +0 -0
  64. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/easyvista_python_client/models/department.py +0 -0
  65. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/easyvista_python_client/models/document.py +0 -0
  66. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/easyvista_python_client/models/employee.py +0 -0
  67. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/easyvista_python_client/models/generic.py +0 -0
  68. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/easyvista_python_client/pagination.py +0 -0
  69. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/easyvista_python_client/py.typed +0 -0
  70. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/easyvista_python_client/references.py +0 -0
  71. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/easyvista_python_client/reporting.py +0 -0
  72. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/easyvista_python_client/resources/__init__.py +0 -0
  73. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/easyvista_python_client/resources/assets.py +0 -0
  74. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/easyvista_python_client/resources/departments.py +0 -0
  75. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/easyvista_python_client/resources/descriptor.py +0 -0
  76. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/easyvista_python_client/resources/discovery.py +0 -0
  77. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/easyvista_python_client/resources/documents.py +0 -0
  78. {easyvista_python_client-0.3.0 → easyvista_python_client-0.4.0}/easyvista_python_client/resources/employees.py +0 -0
  79. {easyvista_python_client-0.3.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
@@ -15,6 +15,456 @@ is the error. Tags carry no `v` prefix.
15
15
 
16
16
  ## [Unreleased]
17
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
+
18
468
  ## [0.3.0] - 2026-09-02
19
469
 
20
470
  Makes the task-vs-action distinction inspectable. A GLPI comment corresponds to
@@ -1142,7 +1592,8 @@ Initial public release.
1142
1592
  status/error code, with non-retryable validation errors (HTTP 590, code 2013).
1143
1593
  - `py.typed` marker — the package ships inline type information.
1144
1594
 
1145
- [Unreleased]: https://github.com/baraline/easyvista_python_client/compare/0.3.0...HEAD
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
1146
1597
  [0.3.0]: https://github.com/baraline/easyvista_python_client/compare/0.2.0...0.3.0
1147
1598
  [0.2.0]: https://github.com/baraline/easyvista_python_client/compare/0.1.0...0.2.0
1148
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: easyvista-python-client
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Typed Python client for the EasyVista Service Manager REST API
5
5
  Project-URL: Homepage, https://github.com/baraline/easyvista_python_client
6
6
  Project-URL: Documentation, https://easyvista-python-client.readthedocs.io/en/latest/
@@ -14,7 +14,6 @@ Classifier: Development Status :: 3 - Alpha
14
14
  Classifier: Intended Audience :: Developers
15
15
  Classifier: Operating System :: OS Independent
16
16
  Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3.10
18
17
  Classifier: Programming Language :: Python :: 3.11
19
18
  Classifier: Programming Language :: Python :: 3.12
20
19
  Classifier: Programming Language :: Python :: 3.13
@@ -22,13 +21,25 @@ Classifier: Programming Language :: Python :: 3.14
22
21
  Classifier: Topic :: Internet :: WWW/HTTP
23
22
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
23
  Classifier: Typing :: Typed
25
- Requires-Python: >=3.10
24
+ Requires-Python: >=3.11
26
25
  Requires-Dist: httpx>=0.27
27
26
  Requires-Dist: pydantic>=2.8
28
27
  Requires-Dist: tenacity>=8.2
29
- Requires-Dist: typing-extensions>=4.7; python_version < '3.11'
28
+ Provides-Extra: content
29
+ Requires-Dist: beautifulsoup4>=4.15; extra == 'content'
30
+ Requires-Dist: cmarkgfm>=2025.10.22; extra == 'content'
31
+ Requires-Dist: markdown-it-py<4,>=3.0; extra == 'content'
32
+ Requires-Dist: markdownify<1.3,>=1.2.3; extra == 'content'
33
+ Requires-Dist: mdformat-tables<1.1,>=1.0; extra == 'content'
34
+ Requires-Dist: mdformat<0.8,>=0.7.22; extra == 'content'
30
35
  Provides-Extra: dev
36
+ Requires-Dist: beautifulsoup4>=4.15; extra == 'dev'
31
37
  Requires-Dist: build>=1.2; extra == 'dev'
38
+ Requires-Dist: cmarkgfm>=2025.10.22; extra == 'dev'
39
+ Requires-Dist: markdown-it-py<4,>=3.0; extra == 'dev'
40
+ Requires-Dist: markdownify<1.3,>=1.2.3; extra == 'dev'
41
+ Requires-Dist: mdformat-tables<1.1,>=1.0; extra == 'dev'
42
+ Requires-Dist: mdformat<0.8,>=0.7.22; extra == 'dev'
32
43
  Requires-Dist: mypy>=1.11; extra == 'dev'
33
44
  Requires-Dist: numpydoc>=1.8; extra == 'dev'
34
45
  Requires-Dist: pre-commit>=4.0; extra == 'dev'
@@ -40,15 +51,19 @@ Requires-Dist: ruff>=0.6; extra == 'dev'
40
51
  Requires-Dist: sphinx-rtd-theme>=2.0; extra == 'dev'
41
52
  Requires-Dist: sphinx<8.2,>=7.2; extra == 'dev'
42
53
  Requires-Dist: tokenize-rt==6.2.0; extra == 'dev'
43
- Requires-Dist: tomli>=2.0; (python_version < '3.11') and extra == 'dev'
44
54
  Requires-Dist: twine>=7.0; extra == 'dev'
45
55
  Requires-Dist: unasync==0.6.0; extra == 'dev'
46
56
  Requires-Dist: vulture>=2.11; extra == 'dev'
47
57
  Provides-Extra: docs
58
+ Requires-Dist: beautifulsoup4>=4.15; extra == 'docs'
59
+ Requires-Dist: cmarkgfm>=2025.10.22; extra == 'docs'
60
+ Requires-Dist: markdown-it-py<4,>=3.0; extra == 'docs'
61
+ Requires-Dist: markdownify<1.3,>=1.2.3; extra == 'docs'
62
+ Requires-Dist: mdformat-tables<1.1,>=1.0; extra == 'docs'
63
+ Requires-Dist: mdformat<0.8,>=0.7.22; extra == 'docs'
48
64
  Requires-Dist: numpydoc>=1.8; extra == 'docs'
49
65
  Requires-Dist: sphinx-rtd-theme>=2.0; extra == 'docs'
50
66
  Requires-Dist: sphinx<8.2,>=7.2; extra == 'docs'
51
- Requires-Dist: tomli>=2.0; (python_version < '3.11') and extra == 'docs'
52
67
  Description-Content-Type: text/markdown
53
68
 
54
69
  # easyvista-python-client
@@ -56,7 +71,7 @@ Description-Content-Type: text/markdown
56
71
  [![CI](https://github.com/baraline/easyvista_python_client/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/baraline/easyvista_python_client/actions/workflows/ci.yml)
57
72
  [![Coverage](https://codecov.io/gh/baraline/easyvista_python_client/branch/main/graph/badge.svg)](https://codecov.io/gh/baraline/easyvista_python_client)
58
73
  [![License](https://img.shields.io/github/license/baraline/easyvista_python_client)](https://github.com/baraline/easyvista_python_client/blob/main/LICENSE)
59
- [![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://github.com/baraline/easyvista_python_client)
74
+ [![Python](https://img.shields.io/badge/python-3.11%2B-blue)](https://github.com/baraline/easyvista_python_client)
60
75
  [![Docs](https://readthedocs.org/projects/easyvista-python-client/badge/?version=latest)](https://easyvista-python-client.readthedocs.io/en/latest/)
61
76
 
62
77
 
@@ -82,6 +97,17 @@ Build it locally with `pip install -e ".[docs]"` then
82
97
  pip install easyvista-python-client
83
98
  ```
84
99
 
100
+ Python 3.11 or newer. 0.4.0 dropped 3.10; on 3.10, pip installs 0.3.0, which
101
+ has no `content` extra: the converter below needs 3.11 or newer.
102
+
103
+ To read and write memo text as Markdown, add the optional `content` extra, which
104
+ brings a Markdown <-> HTML converter, `easyvista_python_client.content`. Its
105
+ Markdown is CommonMark with GFM tables:
106
+
107
+ ```bash
108
+ pip install "easyvista-python-client[content]"
109
+ ```
110
+
85
111
  ## Usage (sync)
86
112
 
87
113
  ```python
@@ -89,6 +115,7 @@ from easyvista_python_client import (
89
115
  EasyvistaClient,
90
116
  EasyvistaConfig,
91
117
  PostRequest,
118
+ WorkflowEffect,
92
119
  ev_equals_filter,
93
120
  )
94
121
 
@@ -122,12 +149,10 @@ with EasyvistaClient(config) as client:
122
149
  for t in client.iter_tickets(search=open_status, page_size=100, max_records=1000):
123
150
  ... # async: `async for t in client.iter_tickets(...)`
124
151
 
125
- # close it with your instance's "closed" status GUID. Every argument is
126
- # optional -- `client.close_ticket(ticket.rfc_number)` sends the close with
127
- # no status of its own, but where that lands the ticket is not established
128
- # by this package; see the user guide before relying on it.
152
+ # close only when closing is the intent: it interrupts the ticket's workflow.
129
153
  client.close_ticket(
130
154
  ticket.rfc_number,
155
+ allow_workflow_effect=WorkflowEffect.INTERRUPTS,
131
156
  status_guid="{00000000-0000-0000-0000-000000000000}",
132
157
  delete_actions=1,
133
158
  comment="Resolved",
@@ -185,7 +210,15 @@ client.end_action(
185
210
  > action only ends it; ending the ticket's open workflow step advances the
186
211
  > workflow and moves the ticket's status. Naming `action_id` is therefore
187
212
  > required — the vendor's id-less "end every open action" form is behind an
188
- > explicit `end_all=True`.
213
+ > explicit `end_all=True`. Ending a workflow step, or ending every open action
214
+ > with `end_all=True`, is refused unless the call passes
215
+ > `allow_workflow_effect=WorkflowEffect.ADVANCES`. Ending an action you
216
+ > created yourself needs no opt-in, with one unmeasured exception: whether an
217
+ > action `create_action` creates under the workflow step carries a
218
+ > `WORKFLOW_ID` has not been measured, and if it does, `end_action` refuses to
219
+ > end it without `ADVANCES` (the safe direction).
220
+ > The vendor documents no status setter, and this package has none: a ticket's
221
+ > status follows its workflow (user guide, "Changing a ticket's status").
189
222
 
190
223
  ## Assets and documents
191
224