easyvista-python-client 0.4.2__tar.gz → 0.5.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 (77) hide show
  1. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/CHANGELOG.md +72 -1
  2. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/PKG-INFO +1 -1
  3. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/docs/content.rst +54 -0
  4. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/docs/user_guide.rst +3 -2
  5. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/docs/vendor-api-reference.md +10 -4
  6. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/_async/client.py +107 -8
  7. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/_sync/client.py +107 -8
  8. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/_version.py +1 -1
  9. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/content/__init__.py +6 -2
  10. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/content/conversion.py +235 -21
  11. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/resources/requests.py +68 -0
  12. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/workflow.py +8 -1
  13. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/pyproject.toml +1 -1
  14. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/skills/easyvista-asset-workflow/SKILL.md +1 -1
  15. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/skills/easyvista-client-setup/SKILL.md +1 -1
  16. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/skills/easyvista-directory/SKILL.md +1 -1
  17. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/skills/easyvista-document-workflow/SKILL.md +1 -1
  18. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/skills/easyvista-instance-discovery/SKILL.md +1 -1
  19. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/skills/easyvista-reporting-and-context/SKILL.md +1 -1
  20. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/skills/easyvista-search-syntax/SKILL.md +1 -1
  21. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/skills/easyvista-ticket-actions/SKILL.md +12 -6
  22. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/skills/easyvista-ticket-workflow/SKILL.md +35 -1
  23. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/.gitignore +0 -0
  24. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/.pre-commit-config.yaml +0 -0
  25. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/.readthedocs.yaml +0 -0
  26. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/CONTRIBUTING.md +0 -0
  27. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/LICENSE +0 -0
  28. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/README.md +0 -0
  29. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/docs/_static/.gitkeep +0 -0
  30. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/docs/_templates/.gitkeep +0 -0
  31. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/docs/api_reference.rst +0 -0
  32. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/docs/conf.py +0 -0
  33. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/docs/development.rst +0 -0
  34. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/docs/index.rst +0 -0
  35. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/docs/installation.rst +0 -0
  36. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/docs/publishing.rst +0 -0
  37. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/docs/sponsoring.rst +0 -0
  38. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/__init__.py +0 -0
  39. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/_async/__init__.py +0 -0
  40. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/_async/_concurrency.py +0 -0
  41. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/_async/_transport.py +0 -0
  42. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/_fields.py +0 -0
  43. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/_html.py +0 -0
  44. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/_sync/__init__.py +0 -0
  45. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/_sync/_concurrency.py +0 -0
  46. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/_sync/_transport.py +0 -0
  47. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/_transport.py +0 -0
  48. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/config.py +0 -0
  49. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/context.py +0 -0
  50. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/directory.py +0 -0
  51. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/discovery.py +0 -0
  52. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/exceptions.py +0 -0
  53. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/field_model.py +0 -0
  54. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/filters.py +0 -0
  55. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/models/__init__.py +0 -0
  56. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/models/action.py +0 -0
  57. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/models/asset.py +0 -0
  58. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/models/common.py +0 -0
  59. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/models/department.py +0 -0
  60. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/models/document.py +0 -0
  61. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/models/employee.py +0 -0
  62. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/models/generic.py +0 -0
  63. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/models/request.py +0 -0
  64. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/pagination.py +0 -0
  65. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/py.typed +0 -0
  66. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/references.py +0 -0
  67. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/reporting.py +0 -0
  68. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/resources/__init__.py +0 -0
  69. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/resources/actions.py +0 -0
  70. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/resources/assets.py +0 -0
  71. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/resources/departments.py +0 -0
  72. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/resources/descriptor.py +0 -0
  73. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/resources/discovery.py +0 -0
  74. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/resources/documents.py +0 -0
  75. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/resources/employees.py +0 -0
  76. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/easyvista_python_client/timestamps.py +0 -0
  77. {easyvista_python_client-0.4.2 → easyvista_python_client-0.5.0}/skills/README.md +0 -0
@@ -15,6 +15,75 @@ is the error. Tags carry no `v` prefix.
15
15
 
16
16
  ## [Unreleased]
17
17
 
18
+ ## [0.5.0] - 2026-11-10
19
+
20
+ Two more of the vendor's ticket requests, and a guard that names one more
21
+ column. Neither new method takes a status: the package still has no status
22
+ setter.
23
+
24
+ ### Added
25
+
26
+ - **`suspend_ticket(rfc_number, *, allow_workflow_effect, comment=None,
27
+ done_by_id=None)`** and **`restart_ticket(...)`** (same signature), the
28
+ vendor's suspend and restart requests
29
+ ([suspend](https://docs.easyvista.com/docs/rest-api-suspend-an-incident-request.md),
30
+ [restart](https://docs.easyvista.com/docs/rest-api-reopen-an-incident-request.md)):
31
+ `PUT requests/{rfc_number}` with a `suspended` / `restarted` object. The guard
32
+ already named both bodies `WorkflowEffect.UNKNOWN`, so
33
+ `allow_workflow_effect` is a required keyword and the call is refused before
34
+ any request unless it includes `UNKNOWN`; an allowed request is sent once,
35
+ never retried. `comment` and `done_by_id` are left out of the body when
36
+ `None`. The return value is parsed as `close_ticket`'s is.
37
+
38
+ ### Changed
39
+
40
+ - **BREAKING:** the workflow guard now names `STATUS_ID_ON_CREATE` on an
41
+ existing action (`update_action`, or `PUT actions/{id}` through `send`), as
42
+ it does `STATUS_ID_ON_TERMINATE`. A write that passed before is refused
43
+ without `allow_workflow_effect=WorkflowEffect.UNKNOWN`. Measured 2026-11-09
44
+ and 2026-11-10 on one instance (tier 4, may not generalise): a write to it
45
+ on an existing action changed where the ticket's status went. Creating an
46
+ action or a task is unchanged: the server overwrites the column on create.
47
+
48
+ ### Fixed
49
+
50
+ - **The `reassign_action` documentation about `OWNING_GROUP_ID`.** It said the
51
+ ticket's owning group does not follow a reassigned step. Measured 2026-11-04
52
+ to 2026-11-10 on one instance (several tickets, may not generalise): it
53
+ follows after a few seconds to about two minutes (consistent with a
54
+ server-side job; inferred, not observed), and `LAST_UPDATE` moves with it;
55
+ the first reading was taken too soon. The status still does not move, the
56
+ step is not started and no action row is added.
57
+
58
+ ## [0.4.3] - 2026-10-08
59
+
60
+ A patch release of the content reader: a caller can recognise the images a
61
+ memo embeds and decide, link by link, what a read writes. Without the new
62
+ argument a read is unchanged, byte for byte; writing is unchanged, and so is
63
+ everything outside `easyvista_python_client.content`.
64
+
65
+ ### Added
66
+
67
+ - **`from_transport(..., rewrite_link=callback)`.** The callback is called with
68
+ a `Link` for each link and image the read meets outside code -- an image
69
+ before the link around it, with that link's `enclosing_href` -- and its
70
+ answer is written: `None` keeps the reader's output, a `str` is written as
71
+ literal text, a `Link` replaces the target (an empty `href` drops a link and
72
+ keeps its content, or writes an image as its text). The callback is held in
73
+ a context variable, so concurrent reads each use their own. What it raises
74
+ reaches the caller unchanged; a `ValueError` is not taken for markdownify's
75
+ and answered with the text fallback.
76
+ - **`Link` and `RewriteLink`**, exported from `easyvista_python_client.content`.
77
+ - **`EasyvistaContentConverter.document_id_of(src)`** returns the
78
+ `DOCUMENT_ID` of an embedded image's `src`
79
+ (`@@EMBEDDED_IMAGE_PATH@@<DOCUMENT_ID>`, how EasyVista's editor embeds a
80
+ pasted image), and **`document_image(document_id, alt=...)`** the Markdown
81
+ of one, spelled as the reader spells it. A `Link` for such an image carries
82
+ its `document_id`. Measured on one instance on 2026-10-07: an image written
83
+ that way through the API displays, for an attachment pasted in the editor
84
+ and for one uploaded with `add_document`.
85
+ - `EasyvistaContentConverter.EMBEDDED_IMAGE_PREFIX`.
86
+
18
87
  ## [0.4.2] - 2026-10-07
19
88
 
20
89
  A patch release of the content writer: a link written into a memo opens in a
@@ -1661,7 +1730,9 @@ Initial public release.
1661
1730
  status/error code, with non-retryable validation errors (HTTP 590, code 2013).
1662
1731
  - `py.typed` marker — the package ships inline type information.
1663
1732
 
1664
- [Unreleased]: https://github.com/baraline/easyvista_python_client/compare/0.4.2...HEAD
1733
+ [Unreleased]: https://github.com/baraline/easyvista_python_client/compare/0.5.0...HEAD
1734
+ [0.5.0]: https://github.com/baraline/easyvista_python_client/compare/0.4.3...0.5.0
1735
+ [0.4.3]: https://github.com/baraline/easyvista_python_client/compare/0.4.2...0.4.3
1665
1736
  [0.4.2]: https://github.com/baraline/easyvista_python_client/compare/0.4.1...0.4.2
1666
1737
  [0.4.1]: https://github.com/baraline/easyvista_python_client/compare/0.4.0...0.4.1
1667
1738
  [0.4.0]: https://github.com/baraline/easyvista_python_client/compare/0.3.0...0.4.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: easyvista-python-client
3
- Version: 0.4.2
3
+ Version: 0.5.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/
@@ -234,6 +234,60 @@ reported as
234
234
  :class:`~easyvista_python_client.exceptions.EasyvistaContentError`. A block
235
235
  quote nested 100,000 levels deep rendered without one (measured 2026-10-02).
236
236
 
237
+ Embedded images, and rewriting links as they are read
238
+ -----------------------------------------------------
239
+
240
+ EasyVista's editor keeps an image pasted into a memo as an attachment of the
241
+ request, named like ``image_emb_001.png``, and embeds it as
242
+ ``<img src="@@EMBEDDED_IMAGE_PATH@@<DOCUMENT_ID>" />``. Without a callback the
243
+ reader writes that as any other image, ``![](@@EMBEDDED_IMAGE_PATH@@...)``,
244
+ which displays nowhere but in EasyVista. Three helpers let a caller do better.
245
+
246
+ ``EasyvistaContentConverter.document_id_of(src)`` returns the ``DOCUMENT_ID``
247
+ an image's ``src`` embeds, or ``None``. ``document_image(document_id, alt=...)``
248
+ returns the Markdown of such an image, spelled exactly as the reader spells
249
+ it, so ``to_transport`` writes the editor's form and ``from_transport`` reads
250
+ it back unchanged. **Tier 4** -- measured on one instance on 2026-10-07: an
251
+ image written that way through the API displays, both for an attachment
252
+ pasted in the editor and for one uploaded with ``add_document``. The memo
253
+ displays it only while the attachment is on the same request.
254
+
255
+ ``from_transport(..., rewrite_link=callback)`` calls ``callback`` with a
256
+ :class:`~easyvista_python_client.content.Link` for each link and each image it
257
+ meets outside code, an image before the link around it. A ``Link`` carries the
258
+ ``href`` (an image's ``src``), the ``text`` (an image's ``alt``), the
259
+ ``title``, whether it is an ``image``, an embedded image's ``document_id``, and
260
+ for an image inside a link that link's ``enclosing_href``. What the callback
261
+ answers is written:
262
+
263
+ * ``None``: what the reader writes without a callback;
264
+ * a ``str``: that text, literally -- escaped as any text the memo displays, so
265
+ it cannot become markup;
266
+ * a ``Link``: a link's ``href`` and ``title`` around the link's content as
267
+ read, or an image's ``href`` as its ``src``, ``text`` as its ``alt`` and
268
+ ``title``;
269
+ * a ``Link`` with an empty ``href``: a link is dropped and its content kept, an
270
+ image is written as its ``text``.
271
+
272
+ .. code-block:: python
273
+
274
+ from easyvista_python_client.content import EasyvistaContentConverter, Link
275
+
276
+ def name_attachments(link: Link) -> Link | str | None:
277
+ if link.document_id is not None:
278
+ return f"[attachment {link.document_id}]"
279
+ return None
280
+
281
+ EasyvistaContentConverter.from_transport(memo, rewrite_link=name_attachments)
282
+
283
+ The callback is held for the duration of one read, per thread and per task,
284
+ so concurrent reads each use their own, and a read inside a callback has its
285
+ own too. What the callback raises reaches the caller unchanged -- a
286
+ ``ValueError`` included, which the reader would otherwise take for one of
287
+ markdownify's and answer by reading the body as its text. An answer of any
288
+ other type raises ``TypeError``. It is not called for a value passed through
289
+ as Markdown (``plain_text_is_markdown=True``), nor for a body read as its text.
290
+
237
291
  It is not a sanitiser
238
292
  ---------------------
239
293
 
@@ -473,8 +473,9 @@ rule on your instance can run on any write.
473
473
  To escalate the open workflow step to another group without ending it, use
474
474
  :meth:`~easyvista_python_client.EasyvistaClient.reassign_action` (measured
475
475
  2026-10-02, one instance, so it may not generalise: the step stays open and the
476
- status does not move; the ticket's owning group, read on one ticket, did not
477
- follow).
476
+ status does not move; measured 2026-11-04 to 2026-11-10, several tickets, the
477
+ ticket's owning group follows the step's new group after a few seconds to about
478
+ two minutes).
478
479
 
479
480
  Creating a ticket starts its workflow -- the vendor's create page lists "The
480
481
  workflow associated with the ticket is started." among what a create does (tier
@@ -287,10 +287,16 @@ again five seconds later); the step **stayed open** (`END_DATE_UT` empty); the t
287
287
  status and `END_DATE_UT` **did not move**; the open actions were unchanged; and **no new
288
288
  action rows** appeared. On the **first ticket only** the step was recorded as a type-20
289
289
  action with `WORKFLOW_ID` set, its `WORKFLOW_ID` was unchanged, `DONE_BY_ID` stayed
290
- empty and no ticket field changed; and that is also the only ticket on which the
291
- ticket's own `OWNING_GROUP_ID` was read — it **stayed at the old group**, so the
292
- ticket's owning group did not follow the action's. Reassigning to a person
293
- (`done_by_id`) was **not measured**. Whether the UI wizard's notifications fire is not
290
+ empty and no ticket field changed. The ticket's own `OWNING_GROUP_ID` was read on
291
+ that ticket only, and had not moved; that reading was taken too soon. Measured
292
+ 2026-11-04 to 2026-11-10 on one instance (several tickets, may not generalise): the
293
+ ticket's `OWNING_GROUP_ID` **follows the step's new group** after a delay of a few
294
+ seconds to about two minutes (consistent with a server-side job; inferred, not
295
+ observed), and `LAST_UPDATE` moves with it; the status still does not move, the step
296
+ is not started and no action row is added. Also measured 2026-11-04 on one ticket:
297
+ after `done_by_id` was written on the step, the ticket's `OWNER_ID` followed about
298
+ 1 min 45 s later. The person's effect on the step was not measured beyond that one
299
+ `OWNER_ID` observation. Whether the UI wizard's notifications fire is not
294
300
  observable from the API.
295
301
 
296
302
  **Business rules can fire on any write** — "On Insert/On Update" of any record
@@ -610,6 +610,99 @@ class AsyncEasyvistaClient:
610
610
  )
611
611
  return parse(await self._transport.send(spec.allowing(allow_workflow_effect)))
612
612
 
613
+ async def suspend_ticket(
614
+ self,
615
+ rfc_number: str,
616
+ *,
617
+ allow_workflow_effect: WorkflowEffect | Iterable[WorkflowEffect],
618
+ comment: str | None = None,
619
+ done_by_id: int | None = None,
620
+ ) -> Request:
621
+ """Suspend a ticket -- the vendor's suspend request.
622
+
623
+ Sends ``PUT requests/{rfc}`` with a ``suspended`` body, as the vendor
624
+ documents it:
625
+ https://docs.easyvista.com/docs/rest-api-suspend-an-incident-request.md
626
+ (tier 1, re-read 2026-11-10). The page documents two optional
627
+ parameters, ``comment`` and ``done_by_id`` (an employee id), and no
628
+ target status. Both are left out of the body when ``None``. A ticket
629
+ that is already suspended is refused by the server (400, "The request
630
+ is already suspended. Operation aborted.").
631
+
632
+ Measured 2026-11-04 to 2026-11-10 on one instance (tier 4, may not
633
+ generalise): the ticket moved to the instance's suspended status and
634
+ one already-ended suspension action carrying the comment was added;
635
+ the workflow step stayed open. A status key sent in the body was
636
+ ignored (measured 2026-11-04). The request takes no status, and the
637
+ package has no status setter.
638
+
639
+ ``allow_workflow_effect`` is **required**: nothing documents what a
640
+ suspend does to the workflow, so the write is named
641
+ ``WorkflowEffect.UNKNOWN`` and refused with
642
+ :class:`~easyvista_python_client.EasyvistaWorkflowEffectRefused`
643
+ before any request is made unless the call allows it. The allowed
644
+ request is sent once, never retried. The return value is parsed like
645
+ :meth:`close_ticket`'s: the vendor answers 201 with an ``HREF`` only, so
646
+ re-read with :meth:`get_ticket` to see the status::
647
+
648
+ client.suspend_ticket(
649
+ rfc,
650
+ allow_workflow_effect=WorkflowEffect.UNKNOWN,
651
+ comment="Waiting for the requester",
652
+ )
653
+ """
654
+ spec, parse = requests_res.build_suspend_ticket(
655
+ rfc_number,
656
+ comment=comment,
657
+ done_by_id=done_by_id,
658
+ context=self._validation_context,
659
+ )
660
+ return parse(await self._transport.send(spec.allowing(allow_workflow_effect)))
661
+
662
+ async def restart_ticket(
663
+ self,
664
+ rfc_number: str,
665
+ *,
666
+ allow_workflow_effect: WorkflowEffect | Iterable[WorkflowEffect],
667
+ comment: str | None = None,
668
+ done_by_id: int | None = None,
669
+ ) -> Request:
670
+ """Restart a suspended ticket -- the vendor's restart request.
671
+
672
+ Sends ``PUT requests/{rfc}`` with a ``restarted`` body, as the vendor
673
+ documents it:
674
+ https://docs.easyvista.com/docs/rest-api-reopen-an-incident-request.md
675
+ (tier 1, re-read 2026-11-10). The page documents two optional
676
+ parameters, ``comment`` and ``done_by_id`` (an employee id), and no
677
+ target status; its example marks ``comment`` as required while its
678
+ table lists it as optional, so this method leaves it out of the body
679
+ when ``None``. On a ticket that is not suspended the server answers
680
+ 400 with "The request is not suspended. Operation aborted.".
681
+
682
+ Measured 2026-11-04 to 2026-11-10 on one instance (tier 4, may not
683
+ generalise): the request added one end-of-suspension action and, in
684
+ the tested case, returned the ticket to the status it was suspended
685
+ from; a restart on a ticket that was not suspended was refused with
686
+ the documented message, as HTTP 590 rather than the documented 400.
687
+ A status key sent in the body was ignored
688
+ (measured 2026-11-04). The request takes no status, and the package
689
+ has no status setter.
690
+
691
+ ``allow_workflow_effect`` is **required** and is
692
+ ``WorkflowEffect.UNKNOWN``, as for :meth:`suspend_ticket`: refused
693
+ before any request unless allowed, and sent once, never retried. The
694
+ return value is parsed like :meth:`close_ticket`'s (an ``HREF`` only)::
695
+
696
+ client.restart_ticket(rfc, allow_workflow_effect=WorkflowEffect.UNKNOWN)
697
+ """
698
+ spec, parse = requests_res.build_restart_ticket(
699
+ rfc_number,
700
+ comment=comment,
701
+ done_by_id=done_by_id,
702
+ context=self._validation_context,
703
+ )
704
+ return parse(await self._transport.send(spec.allowing(allow_workflow_effect)))
705
+
613
706
  # --- actions -------------------------------------------------------------
614
707
  async def create_action(
615
708
  self,
@@ -947,14 +1040,20 @@ class AsyncEasyvistaClient:
947
1040
  the **first ticket only** the step was recorded as a type-20 action
948
1041
  with ``WORKFLOW_ID`` set; its ``WORKFLOW_ID`` was unchanged,
949
1042
  ``DONE_BY_ID`` stayed empty, and no ticket field changed.
950
- **The ticket's own ``OWNING_GROUP_ID`` does not follow the action's
951
- group** -- it stayed 57 on the first ticket, the only one read for it,
952
- so reassigning a step is not reassigning the ticket. The lower-case
953
- key ``group_id`` was the one sent; the upper-case spelling was never
954
- needed. The person (``done_by_id``) was **not measured**: the same
955
- body shape is sent, but nothing here shows what the instance does
956
- with it. Whether the UI wizard's notifications fire is not observable
957
- from the API.
1043
+ The ticket's own ``OWNING_GROUP_ID`` was read at the time on one
1044
+ ticket only and had not moved; that reading was taken too soon.
1045
+ Measured 2026-11-04 to 2026-11-10 on one instance (several tickets,
1046
+ may not generalise): the ticket's ``OWNING_GROUP_ID`` follows the
1047
+ step's new group after a delay of a few seconds to about two minutes,
1048
+ consistent with a server-side job (inferred, not observed), and
1049
+ ``LAST_UPDATE`` moves with it. What still holds: the status does not
1050
+ move, the step is not started and no action row is added. Also
1051
+ measured 2026-11-04 on one ticket: after ``done_by_id`` was written on
1052
+ the step, the ticket's ``OWNER_ID`` followed about 1 min 45 s later.
1053
+ The lower-case key ``group_id`` was the one sent; the upper-case
1054
+ spelling was never needed. The person's effect on the step was not
1055
+ measured beyond that one ``OWNER_ID`` observation. Whether the UI
1056
+ wizard's notifications fire is not observable from the API.
958
1057
 
959
1058
  Not refused by the workflow guard: the group and the person are data
960
1059
  the workflow reads, not workflow state. Re-read with :meth:`get_action`
@@ -610,6 +610,99 @@ class EasyvistaClient:
610
610
  )
611
611
  return parse(self._transport.send(spec.allowing(allow_workflow_effect)))
612
612
 
613
+ def suspend_ticket(
614
+ self,
615
+ rfc_number: str,
616
+ *,
617
+ allow_workflow_effect: WorkflowEffect | Iterable[WorkflowEffect],
618
+ comment: str | None = None,
619
+ done_by_id: int | None = None,
620
+ ) -> Request:
621
+ """Suspend a ticket -- the vendor's suspend request.
622
+
623
+ Sends ``PUT requests/{rfc}`` with a ``suspended`` body, as the vendor
624
+ documents it:
625
+ https://docs.easyvista.com/docs/rest-api-suspend-an-incident-request.md
626
+ (tier 1, re-read 2026-11-10). The page documents two optional
627
+ parameters, ``comment`` and ``done_by_id`` (an employee id), and no
628
+ target status. Both are left out of the body when ``None``. A ticket
629
+ that is already suspended is refused by the server (400, "The request
630
+ is already suspended. Operation aborted.").
631
+
632
+ Measured 2026-11-04 to 2026-11-10 on one instance (tier 4, may not
633
+ generalise): the ticket moved to the instance's suspended status and
634
+ one already-ended suspension action carrying the comment was added;
635
+ the workflow step stayed open. A status key sent in the body was
636
+ ignored (measured 2026-11-04). The request takes no status, and the
637
+ package has no status setter.
638
+
639
+ ``allow_workflow_effect`` is **required**: nothing documents what a
640
+ suspend does to the workflow, so the write is named
641
+ ``WorkflowEffect.UNKNOWN`` and refused with
642
+ :class:`~easyvista_python_client.EasyvistaWorkflowEffectRefused`
643
+ before any request is made unless the call allows it. The allowed
644
+ request is sent once, never retried. The return value is parsed like
645
+ :meth:`close_ticket`'s: the vendor answers 201 with an ``HREF`` only, so
646
+ re-read with :meth:`get_ticket` to see the status::
647
+
648
+ client.suspend_ticket(
649
+ rfc,
650
+ allow_workflow_effect=WorkflowEffect.UNKNOWN,
651
+ comment="Waiting for the requester",
652
+ )
653
+ """
654
+ spec, parse = requests_res.build_suspend_ticket(
655
+ rfc_number,
656
+ comment=comment,
657
+ done_by_id=done_by_id,
658
+ context=self._validation_context,
659
+ )
660
+ return parse(self._transport.send(spec.allowing(allow_workflow_effect)))
661
+
662
+ def restart_ticket(
663
+ self,
664
+ rfc_number: str,
665
+ *,
666
+ allow_workflow_effect: WorkflowEffect | Iterable[WorkflowEffect],
667
+ comment: str | None = None,
668
+ done_by_id: int | None = None,
669
+ ) -> Request:
670
+ """Restart a suspended ticket -- the vendor's restart request.
671
+
672
+ Sends ``PUT requests/{rfc}`` with a ``restarted`` body, as the vendor
673
+ documents it:
674
+ https://docs.easyvista.com/docs/rest-api-reopen-an-incident-request.md
675
+ (tier 1, re-read 2026-11-10). The page documents two optional
676
+ parameters, ``comment`` and ``done_by_id`` (an employee id), and no
677
+ target status; its example marks ``comment`` as required while its
678
+ table lists it as optional, so this method leaves it out of the body
679
+ when ``None``. On a ticket that is not suspended the server answers
680
+ 400 with "The request is not suspended. Operation aborted.".
681
+
682
+ Measured 2026-11-04 to 2026-11-10 on one instance (tier 4, may not
683
+ generalise): the request added one end-of-suspension action and, in
684
+ the tested case, returned the ticket to the status it was suspended
685
+ from; a restart on a ticket that was not suspended was refused with
686
+ the documented message, as HTTP 590 rather than the documented 400.
687
+ A status key sent in the body was ignored
688
+ (measured 2026-11-04). The request takes no status, and the package
689
+ has no status setter.
690
+
691
+ ``allow_workflow_effect`` is **required** and is
692
+ ``WorkflowEffect.UNKNOWN``, as for :meth:`suspend_ticket`: refused
693
+ before any request unless allowed, and sent once, never retried. The
694
+ return value is parsed like :meth:`close_ticket`'s (an ``HREF`` only)::
695
+
696
+ client.restart_ticket(rfc, allow_workflow_effect=WorkflowEffect.UNKNOWN)
697
+ """
698
+ spec, parse = requests_res.build_restart_ticket(
699
+ rfc_number,
700
+ comment=comment,
701
+ done_by_id=done_by_id,
702
+ context=self._validation_context,
703
+ )
704
+ return parse(self._transport.send(spec.allowing(allow_workflow_effect)))
705
+
613
706
  # --- actions -------------------------------------------------------------
614
707
  def create_action(
615
708
  self,
@@ -947,14 +1040,20 @@ class EasyvistaClient:
947
1040
  the **first ticket only** the step was recorded as a type-20 action
948
1041
  with ``WORKFLOW_ID`` set; its ``WORKFLOW_ID`` was unchanged,
949
1042
  ``DONE_BY_ID`` stayed empty, and no ticket field changed.
950
- **The ticket's own ``OWNING_GROUP_ID`` does not follow the action's
951
- group** -- it stayed 57 on the first ticket, the only one read for it,
952
- so reassigning a step is not reassigning the ticket. The lower-case
953
- key ``group_id`` was the one sent; the upper-case spelling was never
954
- needed. The person (``done_by_id``) was **not measured**: the same
955
- body shape is sent, but nothing here shows what the instance does
956
- with it. Whether the UI wizard's notifications fire is not observable
957
- from the API.
1043
+ The ticket's own ``OWNING_GROUP_ID`` was read at the time on one
1044
+ ticket only and had not moved; that reading was taken too soon.
1045
+ Measured 2026-11-04 to 2026-11-10 on one instance (several tickets,
1046
+ may not generalise): the ticket's ``OWNING_GROUP_ID`` follows the
1047
+ step's new group after a delay of a few seconds to about two minutes,
1048
+ consistent with a server-side job (inferred, not observed), and
1049
+ ``LAST_UPDATE`` moves with it. What still holds: the status does not
1050
+ move, the step is not started and no action row is added. Also
1051
+ measured 2026-11-04 on one ticket: after ``done_by_id`` was written on
1052
+ the step, the ticket's ``OWNER_ID`` followed about 1 min 45 s later.
1053
+ The lower-case key ``group_id`` was the one sent; the upper-case
1054
+ spelling was never needed. The person's effect on the step was not
1055
+ measured beyond that one ``OWNER_ID`` observation. Whether the UI
1056
+ wizard's notifications fire is not observable from the API.
958
1057
 
959
1058
  Not refused by the workflow guard: the group and the person are data
960
1059
  the workflow reads, not workflow state. Re-read with :meth:`get_action`
@@ -5,4 +5,4 @@
5
5
  live only in ``__init__``.
6
6
  """
7
7
 
8
- __version__ = "0.4.2"
8
+ __version__ = "0.5.0"
@@ -13,6 +13,10 @@ stack left to report it otherwise (see ``docs/content.rst``).
13
13
 
14
14
  from __future__ import annotations
15
15
 
16
- from easyvista_python_client.content.conversion import EasyvistaContentConverter
16
+ from easyvista_python_client.content.conversion import (
17
+ EasyvistaContentConverter,
18
+ Link,
19
+ RewriteLink,
20
+ )
17
21
 
18
- __all__ = ["EasyvistaContentConverter"]
22
+ __all__ = ["EasyvistaContentConverter", "Link", "RewriteLink"]