Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,35 @@ is the error. Tags carry no `v` prefix.

## [Unreleased]

## [0.4.3] - 2026-10-08

A patch release of the content reader: a caller can recognise the images a
memo embeds and decide, link by link, what a read writes. Without the new
argument a read is unchanged, byte for byte; writing is unchanged, and so is
everything outside `easyvista_python_client.content`.

### Added

- **`from_transport(..., rewrite_link=callback)`.** The callback is called with
a `Link` for each link and image the read meets outside code -- an image
before the link around it, with that link's `enclosing_href` -- and its
answer is written: `None` keeps the reader's output, a `str` is written as
literal text, a `Link` replaces the target (an empty `href` drops a link and
keeps its content, or writes an image as its text). The callback is held in
a context variable, so concurrent reads each use their own. What it raises
reaches the caller unchanged; a `ValueError` is not taken for markdownify's
and answered with the text fallback.
- **`Link` and `RewriteLink`**, exported from `easyvista_python_client.content`.
- **`EasyvistaContentConverter.document_id_of(src)`** returns the
`DOCUMENT_ID` of an embedded image's `src`
(`@@EMBEDDED_IMAGE_PATH@@<DOCUMENT_ID>`, how EasyVista's editor embeds a
pasted image), and **`document_image(document_id, alt=...)`** the Markdown
of one, spelled as the reader spells it. A `Link` for such an image carries
its `document_id`. Measured on one instance on 2026-10-07: an image written
that way through the API displays, for an attachment pasted in the editor
and for one uploaded with `add_document`.
- `EasyvistaContentConverter.EMBEDDED_IMAGE_PREFIX`.

## [0.4.2] - 2026-10-07

A patch release of the content writer: a link written into a memo opens in a
Expand Down Expand Up @@ -1661,7 +1690,8 @@ Initial public release.
status/error code, with non-retryable validation errors (HTTP 590, code 2013).
- `py.typed` marker — the package ships inline type information.

[Unreleased]: https://github.com/baraline/easyvista_python_client/compare/0.4.2...HEAD
[Unreleased]: https://github.com/baraline/easyvista_python_client/compare/0.4.3...HEAD
[0.4.3]: https://github.com/baraline/easyvista_python_client/compare/0.4.2...0.4.3
[0.4.2]: https://github.com/baraline/easyvista_python_client/compare/0.4.1...0.4.2
[0.4.1]: https://github.com/baraline/easyvista_python_client/compare/0.4.0...0.4.1
[0.4.0]: https://github.com/baraline/easyvista_python_client/compare/0.3.0...0.4.0
Expand Down
54 changes: 54 additions & 0 deletions docs/content.rst
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,60 @@ reported as
:class:`~easyvista_python_client.exceptions.EasyvistaContentError`. A block
quote nested 100,000 levels deep rendered without one (measured 2026-10-02).

Embedded images, and rewriting links as they are read
-----------------------------------------------------

EasyVista's editor keeps an image pasted into a memo as an attachment of the
request, named like ``image_emb_001.png``, and embeds it as
``<img src="@@EMBEDDED_IMAGE_PATH@@<DOCUMENT_ID>" />``. Without a callback the
reader writes that as any other image, ``![](@@EMBEDDED_IMAGE_PATH@@...)``,
which displays nowhere but in EasyVista. Three helpers let a caller do better.

``EasyvistaContentConverter.document_id_of(src)`` returns the ``DOCUMENT_ID``
an image's ``src`` embeds, or ``None``. ``document_image(document_id, alt=...)``
returns the Markdown of such an image, spelled exactly as the reader spells
it, so ``to_transport`` writes the editor's form and ``from_transport`` reads
it back unchanged. **Tier 4** -- measured on one instance on 2026-10-07: an
image written that way through the API displays, both for an attachment
pasted in the editor and for one uploaded with ``add_document``. The memo
displays it only while the attachment is on the same request.

``from_transport(..., rewrite_link=callback)`` calls ``callback`` with a
:class:`~easyvista_python_client.content.Link` for each link and each image it
meets outside code, an image before the link around it. A ``Link`` carries the
``href`` (an image's ``src``), the ``text`` (an image's ``alt``), the
``title``, whether it is an ``image``, an embedded image's ``document_id``, and
for an image inside a link that link's ``enclosing_href``. What the callback
answers is written:

* ``None``: what the reader writes without a callback;
* a ``str``: that text, literally -- escaped as any text the memo displays, so
it cannot become markup;
* a ``Link``: a link's ``href`` and ``title`` around the link's content as
read, or an image's ``href`` as its ``src``, ``text`` as its ``alt`` and
``title``;
* a ``Link`` with an empty ``href``: a link is dropped and its content kept, an
image is written as its ``text``.

.. code-block:: python

from easyvista_python_client.content import EasyvistaContentConverter, Link

def name_attachments(link: Link) -> Link | str | None:
if link.document_id is not None:
return f"[attachment {link.document_id}]"
return None

EasyvistaContentConverter.from_transport(memo, rewrite_link=name_attachments)

The callback is held for the duration of one read, per thread and per task,
so concurrent reads each use their own, and a read inside a callback has its
own too. What the callback raises reaches the caller unchanged -- a
``ValueError`` included, which the reader would otherwise take for one of
markdownify's and answer by reading the body as its text. An answer of any
other type raises ``TypeError``. It is not called for a value passed through
as Markdown (``plain_text_is_markdown=True``), nor for a body read as its text.

It is not a sanitiser
---------------------

Expand Down
2 changes: 1 addition & 1 deletion easyvista_python_client/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
live only in ``__init__``.
"""

__version__ = "0.4.2"
__version__ = "0.4.3"
8 changes: 6 additions & 2 deletions easyvista_python_client/content/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@

from __future__ import annotations

from easyvista_python_client.content.conversion import EasyvistaContentConverter
from easyvista_python_client.content.conversion import (
EasyvistaContentConverter,
Link,
RewriteLink,
)

__all__ = ["EasyvistaContentConverter"]
__all__ = ["EasyvistaContentConverter", "Link", "RewriteLink"]
Loading
Loading