Skip to content

Repository files navigation

easyvista-python-client

CI Coverage License Python Docs

Typed Python client for the EasyVista Service Manager REST API. Sync + async, Pydantic models, Bearer or Basic auth.

While the package is preparing for 1.0, breaking changes may land between minor versions; a deprecation policy will follow the 1.0 release. A patch release never carries one. Each breaking change is marked **BREAKING** in its CHANGELOG.md section with the reasoning, so read the section for the version you are moving to.

Documentation

Full documentation: https://easyvista-python-client.readthedocs.io/

Build it locally with pip install -e ".[docs]" then sphinx-build -b html -W docs docs/_build/html.

Install

pip install easyvista-python-client

Python 3.11 or newer. 0.4.0 dropped 3.10; on 3.10, pip installs 0.3.0, which has no content extra: the converter below needs 3.11 or newer.

To read and write memo text as Markdown, add the optional content extra, which brings a Markdown <-> HTML converter, easyvista_python_client.content. Its Markdown is CommonMark with GFM tables:

pip install "easyvista-python-client[content]"

Usage (sync)

from easyvista_python_client import (
    EasyvistaClient,
    EasyvistaConfig,
    PostRequest,
    WorkflowEffect,
    ev_equals_filter,
)

# `account` is the instance id in the API root (https://host/api/v1/12345), not a username.
config = EasyvistaConfig(server="https://my.easyvista.com", account="12345", token="...")
with EasyvistaClient(config) as client:
    # catalog_code, the *_id values and the close status_guid are
    # instance-specific -- `client.describe_instance()` finds them for you.
    # `external_reference` is your own marker, and it is what lets you
    # reconcile a create that failed: see the note below this block.
    ticket = client.create_ticket(
        PostRequest(
            catalog_code="INC_STANDARD",
            title="Printer down",
            description="The 3rd-floor printer is offline",
            # The vendor documents `origin` as a channel NAME, not an id --
            # the one create field with a portable form. An int is also
            # accepted (measured on one instance) and passes through as sent.
            origin="Phone",
            department_id=9,
            urgency_id=8,
            impact_id=28,
            external_reference="MYAPP-0001",  # your own marker; set it always
        )
    )
    fetched = client.get_ticket(ticket.rfc_number)
    open_status = ev_equals_filter("STATUS_ID", 3)
    results = client.search_tickets(search=open_status, max_rows=50)

    # page through everything with the iterator (follows the API's offset paging)
    for t in client.iter_tickets(search=open_status, page_size=100, max_records=1000):
        ...  # async: `async for t in client.iter_tickets(...)`

    # close only when closing is the intent: it interrupts the ticket's workflow.
    client.close_ticket(
        ticket.rfc_number,
        allow_workflow_effect=WorkflowEffect.INTERRUPTS,
        status_guid="{00000000-0000-0000-0000-000000000000}",
        delete_actions=1,
        comment="Resolved",
    )

A create needs a subject: catalog_guid (the vendor's preferred identifier) or catalog_code. Anything beyond that is catalog-specific and enforced server-side, so a field a given catalog insists on raises EasyvistaValidationError (HTTP 590, code 2013) — it is not retried, and the message names no field.

Do not retry that 590 blindly. Measured on one instance (2026-08-25), a rejected create may still have created the ticket: 12 attempts returned 3 RFC_NUMBERs and afterwards all 12 tickets existed. A 590 means possibly created, never not created. Set external_reference on every create and reconcile by that marker — it survives the failed insert and is searchable.

Comments and actions

An action is a unit of work, and it is born open — an open action shows in the UI as a pending row with its text not displayed, which reads as though the note was lost. A comment is an action that has been ended.

from easyvista_python_client import PostAction, PostTask

# A COMMENT: `create_task` posts the same record already ended, in one call.
# Put the text in `description` -- the UI renders one field per action and
# `description` shadows `comment`, so text in `comment` beside a populated
# `description` is stored, readable through the API, and displayed to nobody.
client.create_task(
    rfc,
    PostTask(action_type_id=94, group_id=3, description="Investigating now."),
)

# WORK SOMEONE MUST STILL DO: create it open, then end it when it is done.
client.create_action(
    rfc,
    PostAction(action_type_id=94, group_id=3, description="Chase the supplier."),
)
client.end_action(
    rfc,
    action_id=1234,                    # not recoverable from the create response
    start_date="01/09/2026 17:00:00",  # your instance's format, not ISO 8601
    end_date="01/09/2026 17:15:00",
    elapsed_time=15,                   # MINUTES
)

There is no private-comment flag. Visibility is carried by the action type, which is per-deployment — read the ids off existing actions with client.discover("ACTION_TYPE") rather than hardcoding one.

end_action on a workflow action changes the ticket. Ending your own action only ends it; ending the ticket's open workflow step advances the workflow and moves the ticket's status. Naming action_id is therefore required — the vendor's id-less "end every open action" form is behind an explicit end_all=True. Ending a workflow step, or ending every open action with end_all=True, is refused unless the call passes allow_workflow_effect=WorkflowEffect.ADVANCES. Ending an action you created yourself needs no opt-in, with one unmeasured exception: whether an action create_action creates under the workflow step carries a WORKFLOW_ID has not been measured, and if it does, end_action refuses to end it without ADVANCES (the safe direction). The vendor documents no status setter, and this package has none: a ticket's status follows its workflow (user guide, "Changing a ticket's status").

Assets and documents

from pathlib import Path
from easyvista_python_client import (
    EasyvistaClient,
    EasyvistaConfig,
    PostAsset,
    ev_contains_filter,
    ev_equals_filter,
)

with EasyvistaClient(EasyvistaConfig.from_env()) as client:
    asset = client.create_asset(PostAsset(catalog_id=3153, asset_tag="LAPTOP-001"))
    tag_filter = ev_equals_filter("ASSET_TAG", "LAPTOP-001")
    found = client.search_assets(search=tag_filter, max_rows=50)

    # On the instance this package was characterized against, `~` needs an
    # explicit wildcard to mean "contains" -- a bare value is exact match,
    # identical to `:`. ev_contains_filter appends it for you; the vendor
    # documents `~` as plain Contains, so pass wildcard=None if that is your
    # deployment. It raises ValueError if the value carries `_` or `[` (both
    # are metacharacters to `~` itself, with no escape) or `*`/`%` while a
    # wildcard is being appended. For an exact match on a tag like
    # "LAPTOP_01", use ev_equals_filter: `:` does not expand a wildcard.
    partial = client.search_assets(search=ev_contains_filter("ASSET_TAG", "LAPTOP"))

    # attach a file to a ticket (uploaded as base64 inside the JSON body)
    pdf = Path("report.pdf")
    client.add_document("I240101_0001", filename=pdf.name, content=pdf.read_bytes())
    attachments = client.list_documents("I240101_0001")

Usage (async)

import asyncio

from easyvista_python_client import AsyncEasyvistaClient, EasyvistaConfig


async def main():
    async with AsyncEasyvistaClient(EasyvistaConfig.from_env()) as client:
        ticket = await client.get_ticket("I240101_0001")
        print(ticket.rfc_number)


asyncio.run(main())

Configuration via environment

Set EASYVISTA_URL (or EASYVISTA_SERVER), EASYVISTA_ACCOUNT, and either EASYVISTA_TOKEN / EASYVISTA_TOKEN_FILE or EASYVISTA_LOGIN + EASYVISTA_PASSWORD, then call EasyvistaConfig.from_env().

Agent skills

skills/ holds Agent Skills for driving this client from an AI agent — one per domain (client setup, search syntax, tickets, actions, documents, assets, directory, reporting and context). Each is a directory with a SKILL.md following the Agent Skills specification; see skills/README.md for the index.

They are source-tree material: present in the git repository and the source distribution, absent from the installed wheel.

Contributing

See CONTRIBUTING.md for development setup and quality checks.

License

MIT — see LICENSE.

Sponsoring

The development of this package is indirectly supported by Novahé & Constellation.

About

An object-based interface to interact with the Easyvista ITSM using the REST API.

Topics

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages