easyvista-python-client 0.1.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.
- easyvista_python_client-0.1.0/.gitignore +43 -0
- easyvista_python_client-0.1.0/.pre-commit-config.yaml +79 -0
- easyvista_python_client-0.1.0/.readthedocs.yaml +17 -0
- easyvista_python_client-0.1.0/CHANGELOG.md +192 -0
- easyvista_python_client-0.1.0/CONTRIBUTING.md +94 -0
- easyvista_python_client-0.1.0/LICENSE +21 -0
- easyvista_python_client-0.1.0/PKG-INFO +178 -0
- easyvista_python_client-0.1.0/README.md +125 -0
- easyvista_python_client-0.1.0/docs/_static/.gitkeep +0 -0
- easyvista_python_client-0.1.0/docs/_templates/.gitkeep +0 -0
- easyvista_python_client-0.1.0/docs/api_reference.rst +123 -0
- easyvista_python_client-0.1.0/docs/conf.py +68 -0
- easyvista_python_client-0.1.0/docs/development.rst +97 -0
- easyvista_python_client-0.1.0/docs/index.rst +26 -0
- easyvista_python_client-0.1.0/docs/installation.rst +46 -0
- easyvista_python_client-0.1.0/docs/publishing.rst +65 -0
- easyvista_python_client-0.1.0/docs/sponsoring.rst +5 -0
- easyvista_python_client-0.1.0/docs/user_guide.rst +555 -0
- easyvista_python_client-0.1.0/easyvista_python_client/__init__.py +74 -0
- easyvista_python_client-0.1.0/easyvista_python_client/_async/__init__.py +13 -0
- easyvista_python_client-0.1.0/easyvista_python_client/_async/_concurrency.py +78 -0
- easyvista_python_client-0.1.0/easyvista_python_client/_async/_transport.py +266 -0
- easyvista_python_client-0.1.0/easyvista_python_client/_async/client.py +790 -0
- easyvista_python_client-0.1.0/easyvista_python_client/_fields.py +26 -0
- easyvista_python_client-0.1.0/easyvista_python_client/_html.py +41 -0
- easyvista_python_client-0.1.0/easyvista_python_client/_sync/__init__.py +13 -0
- easyvista_python_client-0.1.0/easyvista_python_client/_sync/_concurrency.py +50 -0
- easyvista_python_client-0.1.0/easyvista_python_client/_sync/_transport.py +266 -0
- easyvista_python_client-0.1.0/easyvista_python_client/_sync/client.py +790 -0
- easyvista_python_client-0.1.0/easyvista_python_client/_transport.py +29 -0
- easyvista_python_client-0.1.0/easyvista_python_client/config.py +71 -0
- easyvista_python_client-0.1.0/easyvista_python_client/context.py +116 -0
- easyvista_python_client-0.1.0/easyvista_python_client/directory.py +53 -0
- easyvista_python_client-0.1.0/easyvista_python_client/exceptions.py +53 -0
- easyvista_python_client-0.1.0/easyvista_python_client/field_model.py +74 -0
- easyvista_python_client-0.1.0/easyvista_python_client/filters.py +82 -0
- easyvista_python_client-0.1.0/easyvista_python_client/models/__init__.py +1 -0
- easyvista_python_client-0.1.0/easyvista_python_client/models/action.py +65 -0
- easyvista_python_client-0.1.0/easyvista_python_client/models/asset.py +36 -0
- easyvista_python_client-0.1.0/easyvista_python_client/models/common.py +78 -0
- easyvista_python_client-0.1.0/easyvista_python_client/models/department.py +68 -0
- easyvista_python_client-0.1.0/easyvista_python_client/models/document.py +32 -0
- easyvista_python_client-0.1.0/easyvista_python_client/models/employee.py +67 -0
- easyvista_python_client-0.1.0/easyvista_python_client/models/request.py +172 -0
- easyvista_python_client-0.1.0/easyvista_python_client/pagination.py +84 -0
- easyvista_python_client-0.1.0/easyvista_python_client/py.typed +0 -0
- easyvista_python_client-0.1.0/easyvista_python_client/references.py +146 -0
- easyvista_python_client-0.1.0/easyvista_python_client/reporting.py +143 -0
- easyvista_python_client-0.1.0/easyvista_python_client/resources/__init__.py +1 -0
- easyvista_python_client-0.1.0/easyvista_python_client/resources/actions.py +72 -0
- easyvista_python_client-0.1.0/easyvista_python_client/resources/assets.py +47 -0
- easyvista_python_client-0.1.0/easyvista_python_client/resources/departments.py +57 -0
- easyvista_python_client-0.1.0/easyvista_python_client/resources/descriptor.py +99 -0
- easyvista_python_client-0.1.0/easyvista_python_client/resources/documents.py +78 -0
- easyvista_python_client-0.1.0/easyvista_python_client/resources/employees.py +57 -0
- easyvista_python_client-0.1.0/easyvista_python_client/resources/requests.py +91 -0
- easyvista_python_client-0.1.0/pyproject.toml +226 -0
- easyvista_python_client-0.1.0/skills/README.md +45 -0
- easyvista_python_client-0.1.0/skills/easyvista-asset-workflow/SKILL.md +132 -0
- easyvista_python_client-0.1.0/skills/easyvista-client-setup/SKILL.md +186 -0
- easyvista_python_client-0.1.0/skills/easyvista-directory/SKILL.md +152 -0
- easyvista_python_client-0.1.0/skills/easyvista-document-workflow/SKILL.md +96 -0
- easyvista_python_client-0.1.0/skills/easyvista-reporting-and-context/SKILL.md +183 -0
- easyvista_python_client-0.1.0/skills/easyvista-search-syntax/SKILL.md +173 -0
- easyvista_python_client-0.1.0/skills/easyvista-ticket-actions/SKILL.md +134 -0
- easyvista_python_client-0.1.0/skills/easyvista-ticket-workflow/SKILL.md +196 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
secrets/*
|
|
2
|
+
secrets/
|
|
3
|
+
.env
|
|
4
|
+
.env.*
|
|
5
|
+
__pycache__/
|
|
6
|
+
*.py[cod]
|
|
7
|
+
.venv/
|
|
8
|
+
venv/
|
|
9
|
+
.pytest_cache/
|
|
10
|
+
.mypy_cache/
|
|
11
|
+
.ruff_cache/
|
|
12
|
+
.coverage
|
|
13
|
+
htmlcov/
|
|
14
|
+
dist/
|
|
15
|
+
build/
|
|
16
|
+
*.egg-info/
|
|
17
|
+
|
|
18
|
+
# Sphinx build output
|
|
19
|
+
docs/_build/
|
|
20
|
+
|
|
21
|
+
# Local agent/tooling state. These must be listed in THIS file specifically:
|
|
22
|
+
# hatchling reads only the root .gitignore when assembling the sdist, and
|
|
23
|
+
# ignores both nested .gitignore files and the user's global ignore file.
|
|
24
|
+
.claude/
|
|
25
|
+
.superpowers/
|
|
26
|
+
|
|
27
|
+
# Internal planning docs. Mirrors the sister package (glpi_python_client
|
|
28
|
+
# gitignores docs/superpowers/* too). Kept on disk for local use; never
|
|
29
|
+
# published, because they narrate the private preprod instance.
|
|
30
|
+
docs/superpowers/
|
|
31
|
+
|
|
32
|
+
# Instance-specific artifacts generated from, or describing, a private
|
|
33
|
+
# EasyVista instance. Useful locally, not publishable: they carry the
|
|
34
|
+
# instance host/account, the end customer's org structure, and a map of
|
|
35
|
+
# that instance's bespoke customization.
|
|
36
|
+
docs/API_Info.md
|
|
37
|
+
docs/easyvista-test-profile-blocked-operations.md
|
|
38
|
+
docs/easyvista-field-inventory.md
|
|
39
|
+
|
|
40
|
+
# One-off API-exploration scripts. They hardcode a real client's records and
|
|
41
|
+
# print real employees' names and e-mail addresses. Kept on disk so they stay
|
|
42
|
+
# usable locally; never published.
|
|
43
|
+
scripts/probe_*.py
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
3
|
+
rev: v0.16.0
|
|
4
|
+
hooks:
|
|
5
|
+
- id: ruff-check
|
|
6
|
+
- id: ruff-format
|
|
7
|
+
- repo: https://github.com/pre-commit/mirrors-mypy
|
|
8
|
+
rev: v2.3.0
|
|
9
|
+
hooks:
|
|
10
|
+
- id: mypy
|
|
11
|
+
additional_dependencies: ["pydantic>=2.8", "httpx", "tenacity"]
|
|
12
|
+
# Must mirror [tool.mypy] exclude in pyproject.toml. pre-commit passes
|
|
13
|
+
# filenames explicitly, and mypy ignores its own `exclude` for files named
|
|
14
|
+
# on the command line -- so this regex is the ONLY thing keeping the hook
|
|
15
|
+
# in step with the project's type-checking policy. `integration_tests/`
|
|
16
|
+
# needs its own alternative: pyproject's bare `tests/` matches it as a
|
|
17
|
+
# substring, but the anchored `(^|/)tests/` here does not.
|
|
18
|
+
exclude: (^|/)tests/|(^|/)testing/|^integration_tests/
|
|
19
|
+
- repo: local
|
|
20
|
+
hooks:
|
|
21
|
+
# Fails on a _sync/ tree left stale by an edit to _async/. Runs at
|
|
22
|
+
# commit time rather than pre-push so the generated tree is never
|
|
23
|
+
# committed out of step with its source. `language: python` (not
|
|
24
|
+
# `system`) so pre-commit builds this hook its own managed venv --
|
|
25
|
+
# bootstrapped from whatever interpreter ran pre-commit itself, never
|
|
26
|
+
# by searching PATH for a bare `python` -- so it is unaffected by a
|
|
27
|
+
# `python` on PATH that isn't this project's interpreter (as on
|
|
28
|
+
# Windows, where it commonly resolves to the Microsoft Store stub).
|
|
29
|
+
# unasync_build.py imports only the stdlib plus `unasync`, so that is
|
|
30
|
+
# the hook's entire additional_dependencies list.
|
|
31
|
+
#
|
|
32
|
+
# Both pins below are exact, not ranges: this hook is a byte-equality
|
|
33
|
+
# gate between _async/ and the checked-in _sync/, and unasync (via
|
|
34
|
+
# tokenize_rt, which actually re-tokenizes and reconstructs the source
|
|
35
|
+
# bytes) is the generator that equality is measured against. A patch
|
|
36
|
+
# release of either that changes whitespace reconstruction would fail
|
|
37
|
+
# this hook on an untouched commit with a misleading "out of date"
|
|
38
|
+
# message, and the obvious "fix" -- regenerate -- produces a large
|
|
39
|
+
# unreviewed diff. Bump these deliberately, in their own commit, with
|
|
40
|
+
# --check re-run by hand; do not relax back to a range.
|
|
41
|
+
- id: unasync-check
|
|
42
|
+
name: generated sync tree is up to date
|
|
43
|
+
entry: python unasync_build.py --check
|
|
44
|
+
language: python
|
|
45
|
+
additional_dependencies: ["unasync==0.6.0", "tokenize-rt==6.2.0"]
|
|
46
|
+
types: [python]
|
|
47
|
+
pass_filenames: false
|
|
48
|
+
# `_sync/` as a whole is excluded from ruff (see [tool.ruff]
|
|
49
|
+
# extend-exclude in pyproject.toml) because it is generated and the
|
|
50
|
+
# formatter would fight the generator forever. But two files under it
|
|
51
|
+
# are hand-written on both sides -- unasync_build.HAND_WRITTEN -- and
|
|
52
|
+
# the directory-wide exclusion silently skips them too, so nothing has
|
|
53
|
+
# ever formatted or linted them. This runs ruff on exactly those paths,
|
|
54
|
+
# read from HAND_WRITTEN itself so it stays correct if that set
|
|
55
|
+
# changes. Also `language: python` for the reason given above.
|
|
56
|
+
# `ruff==0.16.0` is pinned to match the `astral-sh/ruff-pre-commit`
|
|
57
|
+
# `rev` above exactly, so the two hand-written twins are never
|
|
58
|
+
# formatted one way by that hook's ruff and another way by this one --
|
|
59
|
+
# they just never overlap in file scope, but they use one ruff.
|
|
60
|
+
#
|
|
61
|
+
# `unasync`/`tokenize-rt` are exact-pinned here too, matching the
|
|
62
|
+
# unasync-check hook above -- this script only imports unasync_build
|
|
63
|
+
# for HAND_WRITTEN/SYNC_DIR and never calls _generate()/_check(), so it
|
|
64
|
+
# is not itself a byte-equality gate, but it still needs unasync_build's
|
|
65
|
+
# `import unasync` to succeed, and there is no reason to let a second,
|
|
66
|
+
# independently-resolved copy of either package float in this repo's
|
|
67
|
+
# hooks.
|
|
68
|
+
- id: hand-written-sync-lint
|
|
69
|
+
name: format and lint the hand-written _sync/ twins
|
|
70
|
+
entry: python scripts/lint_hand_written_sync.py
|
|
71
|
+
language: python
|
|
72
|
+
additional_dependencies:
|
|
73
|
+
["unasync==0.6.0", "tokenize-rt==6.2.0", "ruff==0.16.0"]
|
|
74
|
+
# Mirrors unasync_build.HAND_WRITTEN by hand; there is no way to read
|
|
75
|
+
# that set before pre-commit decides which files to run hooks on, so
|
|
76
|
+
# keep this pattern in step with HAND_WRITTEN if it ever gains a
|
|
77
|
+
# third entry.
|
|
78
|
+
files: ^easyvista_python_client/_sync/(_concurrency\.py|tests/test_concurrency\.py)$
|
|
79
|
+
pass_filenames: false
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
While the package is pre-1.0, breaking changes may land between minor versions;
|
|
8
|
+
a deprecation policy will follow the 1.0 release.
|
|
9
|
+
|
|
10
|
+
## [Unreleased]
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Python 3.13 and 3.14 are now tested and declared supported (classifiers, and
|
|
15
|
+
the CI/release matrices, now span 3.10--3.14). No code changed: the suite
|
|
16
|
+
passes unmodified on both, with the same statement count and coverage as on
|
|
17
|
+
3.10, and the generated `_sync/` tree regenerates byte-identically under each
|
|
18
|
+
interpreter's tokenizer.
|
|
19
|
+
- `.github/workflows/release.yml`: releases are built and published to PyPI by
|
|
20
|
+
CI when a GitHub release is published, using Trusted Publishing (no API token
|
|
21
|
+
in the repository). The workflow re-runs the test matrix and the quality gates,
|
|
22
|
+
refuses to build when the release tag, `pyproject.toml` and
|
|
23
|
+
`easyvista_python_client.__version__` disagree, runs `twine check`, and then
|
|
24
|
+
triggers a Read the Docs build for the release. `workflow_dispatch` rehearses
|
|
25
|
+
everything except the upload. See `docs/publishing.rst`.
|
|
26
|
+
- `skills/`: eight Agent Skills covering client setup, search syntax, tickets,
|
|
27
|
+
ticket actions, documents, assets, the directory and reporting/context, with
|
|
28
|
+
an index in `skills/README.md`. Shipped in the source distribution, not in
|
|
29
|
+
the wheel.
|
|
30
|
+
- `scripts/tests/test_skills_contract.py`: checks every skill's frontmatter and
|
|
31
|
+
code snippets against the real public API, so a rename fails CI.
|
|
32
|
+
- Public `filters.py`: `ev_equals_filter`, `ev_in_filter`, `escape_ev_value`, and
|
|
33
|
+
`is_safe_ev_value` for building EasyVista `search` expressions safely.
|
|
34
|
+
- `Request` now declares fields that were previously reachable only as untyped
|
|
35
|
+
`extra="allow"` data — each verified present on live single-ticket GETs:
|
|
36
|
+
`title`, `request_id`, `external_reference`, `sd_catalog_id`, `urgency_id`,
|
|
37
|
+
`impact_id`, `severity_id`, `request_origin_id`, `department_id`,
|
|
38
|
+
`location_id`, `requestor_id`, `recipient_id`, `owner_id`, `submit_date_ut`,
|
|
39
|
+
and `last_update`.
|
|
40
|
+
- `RequestUpdate.title` — a ticket's title can now be changed after creation
|
|
41
|
+
(`PUT /requests/{rfc}`), not only set at create time.
|
|
42
|
+
- `EasyvistaClient.download_document` / `AsyncEasyvistaClient.download_document`
|
|
43
|
+
fetch an attachment's bytes. An absolute download URL is followed only when
|
|
44
|
+
its scheme and host match the configured `server`: every request carries the
|
|
45
|
+
instance's Bearer token, so a URL naming another host is refused rather than
|
|
46
|
+
followed.
|
|
47
|
+
- `Request` now declares the official time-limit fields as typed attributes:
|
|
48
|
+
`creation_date_ut`, `max_resolution_date_ut`, `expected_date_ut`,
|
|
49
|
+
`end_date_ut`, `sla_id` and `time_used_to_solve_request`. As with the existing
|
|
50
|
+
timestamps, they are verified *returned* and no datetime parsing is claimed.
|
|
51
|
+
The instance-specific `E_GTR_*` / `E_GTI_*` family stays undeclared and
|
|
52
|
+
reachable through `classify_fields().custom`.
|
|
53
|
+
- `EasyvistaClient.get_action` / `AsyncEasyvistaClient.get_action` fetch a single
|
|
54
|
+
action. The item-level record carries Memo links that `list_actions` omits —
|
|
55
|
+
including `DESCRIPTION`, which is where an action's note text actually lives.
|
|
56
|
+
- `Action.description` and `Action.href`, plus an `action_id` derived from `href`
|
|
57
|
+
when the API omits it. The derivation is deliberately narrow: it uses `href`'s
|
|
58
|
+
trailing segment only when that segment is numeric, which is the case for an
|
|
59
|
+
item-level `GET actions/{id}`, and it never overwrites an `ACTION_ID` the API
|
|
60
|
+
did send. It does **not** fire for a create response — `POST
|
|
61
|
+
requests/{rfc}/actions` echoes an HREF naming the **parent request**, so the
|
|
62
|
+
tail is an RFC number rather than an id. A created action's id is therefore not
|
|
63
|
+
recoverable from its create response at all; diff `list_actions` across the
|
|
64
|
+
create to identify it (verified live).
|
|
65
|
+
- `get_ticket_context(..., resolve_action_bodies=True)` resolves each action's
|
|
66
|
+
note text. Pass `False` to skip it — it costs two extra requests per action.
|
|
67
|
+
|
|
68
|
+
### Removed
|
|
69
|
+
|
|
70
|
+
- **Breaking:** `PostRequest.catalog_guid` and `Request.catalog_guid` are gone.
|
|
71
|
+
`CATALOG_GUID` is absent from every sampled live ticket (0/25 single-ticket
|
|
72
|
+
GETs), from the documented create body, and from the vendor field inventory —
|
|
73
|
+
it could never populate. `PostRequest(catalog_guid=...)` previously validated
|
|
74
|
+
and was sent to the API; it now raises (`extra="forbid"`) instead of being
|
|
75
|
+
silently accepted. Use `catalog_code` to name a catalog on create.
|
|
76
|
+
|
|
77
|
+
### Fixed
|
|
78
|
+
|
|
79
|
+
- `find_departments` and `list_actions` interpolated caller values into a `search` expression
|
|
80
|
+
unescaped. Because `,` is an EasyVista combinator, a crafted value could silently widen the
|
|
81
|
+
result set (verified live: a department lookup returned 2 records instead of 1). Both now
|
|
82
|
+
validate the value.
|
|
83
|
+
- `TicketContext.to_markdown` rendered every action with an empty body. It read
|
|
84
|
+
the text from `Action.comment`, but `COMMENT` is a distinct field that never
|
|
85
|
+
carries it; the note supplied as `PostAction.description` comes back through
|
|
86
|
+
the action's `DESCRIPTION` Memo, which is reachable only via an item-level
|
|
87
|
+
`GET actions/{id}`. Verified against a live instance.
|
|
88
|
+
- **Every mapped exception's message no longer interpolates the raw HTTP response
|
|
89
|
+
body.** For a body this client does not recognize (an nginx or WAF HTML page, a
|
|
90
|
+
plain-text 503, any unmodelled shape), the message previously ended with that
|
|
91
|
+
body's literal text — which then surfaced verbatim wherever the exception was
|
|
92
|
+
rendered (`str(exc)`, a traceback, a test runner's failure summary), regardless
|
|
93
|
+
of what the body actually contained. The message now reports only the byte
|
|
94
|
+
count. **Added:** `EasyvistaError.body` (`bytes | None`) carries the raw
|
|
95
|
+
response body, so the content dropped from the message is not lost — it is the
|
|
96
|
+
only way left to retrieve an unrecognized body. `.status_code`, `.ev_code` and
|
|
97
|
+
`.ev_message` are unaffected: a *recognized* EasyVista error body (one with a
|
|
98
|
+
parseable `error`/`error_code` shape) reads exactly as it did before.
|
|
99
|
+
|
|
100
|
+
### Changed
|
|
101
|
+
|
|
102
|
+
- `AsyncEasyvistaClient.get_ticket_context` and `get_department_context` now issue their
|
|
103
|
+
independent sub-requests **concurrently** instead of one after another. The async client
|
|
104
|
+
previously awaited every call in sequence, so it was no faster than the synchronous one
|
|
105
|
+
(measured against a live instance on a ticket with 19 actions: 14.65s async, 13.44s sync,
|
|
106
|
+
5.31s after this change). Same requests, same results, same degradation on 403/404 — only
|
|
107
|
+
the issue order changed. Peak in-flight is 4 sub-resource requests then at most 8
|
|
108
|
+
concurrent action-body resolutions for a ticket, and 7 branches for a department.
|
|
109
|
+
Two deltas worth knowing: on a **failing** bundle the siblings already in flight are
|
|
110
|
+
awaited before the error propagates, so an error path can issue more requests than it did
|
|
111
|
+
before (bounded by the fan-out width, and all of them reads); and when two branches fail,
|
|
112
|
+
the exception raised is the first in source order — the one the sequential version would
|
|
113
|
+
have raised — rather than whichever failed soonest. `create_tickets` stays deliberately
|
|
114
|
+
sequential: those are writes, and a mid-batch failure must leave a knowable prefix.
|
|
115
|
+
- `TicketContext.to_markdown()` now titles a lone narrative block `## Description` whichever
|
|
116
|
+
memo it arrived in. A ticket's body does not always live in `DESCRIPTION` — on the verified
|
|
117
|
+
instance that memo is unused and `COMMENT` carries the body (and `RequestUpdate.description`
|
|
118
|
+
writes `COMMENT` on any instance), so the rendered document titled a ticket's main text
|
|
119
|
+
"Comment", which misleads an LLM or a RAG chunker splitting on headings. The renderer no longer
|
|
120
|
+
assumes either mapping: when only one memo has text it is the body and is titled
|
|
121
|
+
`## Description`; when both do, the distinction is real and each keeps its own heading, byte
|
|
122
|
+
identical to before. An instance that populates `DESCRIPTION` is unaffected. The
|
|
123
|
+
`TicketContext.description` / `.comment` attributes are unchanged and still name their
|
|
124
|
+
source memo.
|
|
125
|
+
- **Documentation of observed behaviour, not a code change:** a `description` supplied to
|
|
126
|
+
`PostRequest` at create time is not readable back through either the `DESCRIPTION` or the
|
|
127
|
+
`COMMENT` Memo on the verified instance. `RequestUpdate.description` writes the ticket's
|
|
128
|
+
`COMMENT` Memo, not `DESCRIPTION` — verified live (0/15 sampled tickets, portal-created
|
|
129
|
+
included, have a non-empty `DESCRIPTION`; 15/15 have a non-empty `COMMENT`). Read the body
|
|
130
|
+
text back with `TicketContext.comment` (or `resolve_memo("requests/{rfc}/comment")`
|
|
131
|
+
directly), not `Request.description`. Both fields stay as they are; nothing was renamed.
|
|
132
|
+
- **Documentation correction:** the `search` operator `~` was documented as "contains". It is
|
|
133
|
+
**exact match**, identical to `:` — verified against a live instance. Examples implying
|
|
134
|
+
substring matching (`ASSET_TAG~LAPTOP`) were wrong and have been replaced. The unverified
|
|
135
|
+
`!~` / `!` / `is_null` / `is_not_null` operators are no longer documented as fact.
|
|
136
|
+
- **Documentation correction:** the README's and user guide's tutorial examples filtered with
|
|
137
|
+
`ev_equals_filter("STATUS_EN", "Open")`. `STATUS_EN` is a sub-key of the nested `STATUS`
|
|
138
|
+
object, not a top-level column, so EasyVista silently ignored the condition and every example
|
|
139
|
+
returned *all* tickets, not just open ones. This was a documentation defect, not a library bug
|
|
140
|
+
— the library does not special-case field names, so nothing in the shipped code was broken.
|
|
141
|
+
Replaced with `ev_equals_filter("STATUS_ID", 3)` throughout, and the user guide now documents
|
|
142
|
+
which returned fields are actually searchable and the third (HTTP 590 type-mismatch) search
|
|
143
|
+
outcome.
|
|
144
|
+
- `Request.status_id`, along with the model's other numeric identity/classification fields, now
|
|
145
|
+
uses an `OptionalInt` type that tolerates the API's `""` for an absent numeric; `status_id`
|
|
146
|
+
previously raised a validation error on that value.
|
|
147
|
+
- `get_department_context` now raises `ValueError` for a blank or unrenderable `department_id`
|
|
148
|
+
rather than building a malformed search (defence in depth; not a demonstrated exploitable path).
|
|
149
|
+
- The synchronous client is now **generated** from the asynchronous one with
|
|
150
|
+
`unasync`. `easyvista_python_client/_async/` is the only hand-written client
|
|
151
|
+
source; `_sync/` is produced by `python unasync_build.py`, checked in, and
|
|
152
|
+
verified in CI. Sync/async parity is enforced by a build gate instead of by
|
|
153
|
+
convention.
|
|
154
|
+
- **Internal module paths moved.** `easyvista_python_client.client` and
|
|
155
|
+
`easyvista_python_client.async_client` no longer exist. Import from the
|
|
156
|
+
package root instead — `from easyvista_python_client import EasyvistaClient,
|
|
157
|
+
AsyncEasyvistaClient` — which is unchanged and has always been the supported
|
|
158
|
+
surface. `easyvista_python_client._transport.RequestSpec` is also unchanged.
|
|
159
|
+
- `EasyvistaClient.ticket_statistics` now collects its page of tickets into a
|
|
160
|
+
list before aggregating, rather than streaming the iterator. No behavioural
|
|
161
|
+
difference at the default `max_records=100`; at `max_records=None` peak
|
|
162
|
+
memory is now proportional to the result set.
|
|
163
|
+
- `EasyvistaClient.get_ticket_context` now lists documents before resolving
|
|
164
|
+
action bodies, rather than after. The same requests are issued and the
|
|
165
|
+
result is identical; only their order on the wire changed.
|
|
166
|
+
|
|
167
|
+
## [0.1.0] - 2026-07-15
|
|
168
|
+
|
|
169
|
+
Initial public release.
|
|
170
|
+
|
|
171
|
+
### Added
|
|
172
|
+
|
|
173
|
+
- Synchronous `EasyvistaClient` and asynchronous `AsyncEasyvistaClient` over the
|
|
174
|
+
EasyVista Service Manager REST API, with Bearer or HTTP Basic authentication
|
|
175
|
+
and `EasyvistaConfig.from_env()`.
|
|
176
|
+
- Tickets (requests): create, batch create, get, update, search, close, and
|
|
177
|
+
offset-following `iter_tickets()` pagination.
|
|
178
|
+
- Assets, actions, and documents (base64-in-JSON upload, attachment listing).
|
|
179
|
+
- Departments and employees directory, including fuzzy `find_departments()`
|
|
180
|
+
and department context.
|
|
181
|
+
- `TicketContext.get_ticket_context()` with an href-free `to_markdown()`
|
|
182
|
+
renderer.
|
|
183
|
+
- Reporting helpers: `count_tickets`, `ticket_statistics`, and the pure
|
|
184
|
+
`aggregate_tickets()` core.
|
|
185
|
+
- Reference normalization (`Reference`, `localized_label`) and a generic field
|
|
186
|
+
model (`FieldClassification`) separating official from custom `e_*` fields.
|
|
187
|
+
- Typed exception hierarchy rooted at `EasyvistaError`, carrying the EasyVista
|
|
188
|
+
status/error code, with non-retryable validation errors (HTTP 590, code 2013).
|
|
189
|
+
- `py.typed` marker — the package ships inline type information.
|
|
190
|
+
|
|
191
|
+
[Unreleased]: https://github.com/baraline/easyvista_python_client/compare/v0.1.0...HEAD
|
|
192
|
+
[0.1.0]: https://github.com/baraline/easyvista_python_client/releases/tag/v0.1.0
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thank you for improving `easyvista-python-client`.
|
|
4
|
+
|
|
5
|
+
The full guide lives in [docs/development.rst](docs/development.rst) (published as
|
|
6
|
+
the [development guide](https://easyvista-python-client.readthedocs.io/en/latest/development.html));
|
|
7
|
+
this page is the short version.
|
|
8
|
+
|
|
9
|
+
## Development Setup
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
python -m venv .venv
|
|
13
|
+
.venv\Scripts\activate
|
|
14
|
+
python -m pip install -e ".[dev]"
|
|
15
|
+
python -m pre_commit install
|
|
16
|
+
python -m pre_commit run --all-files
|
|
17
|
+
python -m pytest -m "not integration"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quality Checks
|
|
21
|
+
|
|
22
|
+
Run these before opening a pull request:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
python -m pre_commit run --all-files
|
|
26
|
+
python -m pytest -m "not integration"
|
|
27
|
+
python -m ruff check .
|
|
28
|
+
python -m mypy easyvista_python_client
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Coverage is enforced at 95% and `--cov` is always on via `addopts`, so a
|
|
32
|
+
single-file run needs `--no-cov` to avoid a spurious under-coverage failure:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
python -m pytest easyvista_python_client/tests/test_client.py --no-cov
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Documentation build
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
python -m sphinx -W --keep-going -b html docs docs/_build/html
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Live integration tests
|
|
45
|
+
|
|
46
|
+
`integration_tests/` lives at the repository root, apart from the unit tests
|
|
47
|
+
inside the package, because it calls a **real EasyVista instance that you
|
|
48
|
+
supply**. It never runs in CI — CI runs `pytest -m "not integration"`.
|
|
49
|
+
|
|
50
|
+
Credentials come from `EASYVISTA_TEST_*` environment variables, falling back to
|
|
51
|
+
files under `secrets/` (both gitignored). With none configured the suite skips
|
|
52
|
+
cleanly, so `pytest` on a fresh checkout is offline and green.
|
|
53
|
+
|
|
54
|
+
> **These tests are not read-only.** They create tickets and close them in
|
|
55
|
+
> teardown. Once your credentials are present they run as part of a plain
|
|
56
|
+
> `pytest` — use `pytest -m "not integration"` for a unit-only run. Point them at
|
|
57
|
+
> a preprod or test instance, never production.
|
|
58
|
+
|
|
59
|
+
Never commit an instance host, account id, or token — see the note below.
|
|
60
|
+
|
|
61
|
+
## Design Guidelines
|
|
62
|
+
|
|
63
|
+
- Keep API calls behind `EasyvistaClient` / `AsyncEasyvistaClient` methods. The
|
|
64
|
+
two surfaces are hand-maintained in parallel: a method added to one must be
|
|
65
|
+
added to the other with the same name and signature.
|
|
66
|
+
- Prefer declaring a new documented endpoint as a resource descriptor plus a
|
|
67
|
+
model over hand-writing a builder.
|
|
68
|
+
- Prefer field-validated Pydantic models for request and response payloads.
|
|
69
|
+
- Keep instance-specific values out of the library, the docs, and the tests.
|
|
70
|
+
Catalog codes, department/urgency/impact ids, and status GUIDs vary per
|
|
71
|
+
EasyVista instance; use obviously synthetic placeholders in examples and
|
|
72
|
+
fixtures rather than values copied from a real instance.
|
|
73
|
+
- Add tests for payload serialization and response normalization when adding
|
|
74
|
+
endpoints. Unit tests live beside the code: one test module per source
|
|
75
|
+
module, in that package's `tests/` directory (`models/request.py` is covered
|
|
76
|
+
by `models/tests/test_request.py`). Tests that span several modules, and any
|
|
77
|
+
shared fixture, belong in `easyvista_python_client/testing/`. Neither
|
|
78
|
+
directory ships in the wheel or the sdist.
|
|
79
|
+
- "One test module per source module" is a ceiling, not a floor: a source
|
|
80
|
+
module whose behaviour is fully exercised through a caller's tests (for
|
|
81
|
+
example a model asserted only via the resource and client tests that build
|
|
82
|
+
it) does not need its own near-empty test file. Check coverage before adding
|
|
83
|
+
one.
|
|
84
|
+
|
|
85
|
+
## Agent skills
|
|
86
|
+
|
|
87
|
+
A change to the public API must update the affected `skills/*/SKILL.md`, and a
|
|
88
|
+
release that bumps `__version__` must bump every skill's `metadata.version`.
|
|
89
|
+
`scripts/tests/test_skills_contract.py` is the gate: it parses every `SKILL.md`
|
|
90
|
+
and asserts each symbol, client method, keyword argument and model field the
|
|
91
|
+
skill names still exists on the public surface. Run it with
|
|
92
|
+
`pytest scripts/tests/test_skills_contract.py --no-cov` (see the coverage note
|
|
93
|
+
above — a single-file run without `--no-cov` fails the 95% gate even when
|
|
94
|
+
every test passes).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 easyvista-python-client contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: easyvista-python-client
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Typed Python client for the EasyVista Service Manager REST API
|
|
5
|
+
Project-URL: Homepage, https://github.com/baraline/easyvista_python_client
|
|
6
|
+
Project-URL: Documentation, https://easyvista-python-client.readthedocs.io/en/latest/
|
|
7
|
+
Project-URL: Issues, https://github.com/baraline/easyvista_python_client/issues
|
|
8
|
+
Project-URL: Source, https://github.com/baraline/easyvista_python_client
|
|
9
|
+
Author: easyvista-python-client contributors
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: api,client,easyvista,itsm,rest,service-manager
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Requires-Dist: httpx>=0.27
|
|
27
|
+
Requires-Dist: pydantic>=2.8
|
|
28
|
+
Requires-Dist: tenacity>=8.2
|
|
29
|
+
Requires-Dist: typing-extensions>=4.7; python_version < '3.11'
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
32
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
33
|
+
Requires-Dist: numpydoc>=1.8; extra == 'dev'
|
|
34
|
+
Requires-Dist: pre-commit>=4.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
36
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
37
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
38
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
39
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
40
|
+
Requires-Dist: sphinx-rtd-theme>=2.0; extra == 'dev'
|
|
41
|
+
Requires-Dist: sphinx<8.2,>=7.2; extra == 'dev'
|
|
42
|
+
Requires-Dist: tokenize-rt==6.2.0; extra == 'dev'
|
|
43
|
+
Requires-Dist: tomli>=2.0; (python_version < '3.11') and extra == 'dev'
|
|
44
|
+
Requires-Dist: twine>=5.1; extra == 'dev'
|
|
45
|
+
Requires-Dist: unasync==0.6.0; extra == 'dev'
|
|
46
|
+
Requires-Dist: vulture>=2.11; extra == 'dev'
|
|
47
|
+
Provides-Extra: docs
|
|
48
|
+
Requires-Dist: numpydoc>=1.8; extra == 'docs'
|
|
49
|
+
Requires-Dist: sphinx-rtd-theme>=2.0; extra == 'docs'
|
|
50
|
+
Requires-Dist: sphinx<8.2,>=7.2; extra == 'docs'
|
|
51
|
+
Requires-Dist: tomli>=2.0; (python_version < '3.11') and extra == 'docs'
|
|
52
|
+
Description-Content-Type: text/markdown
|
|
53
|
+
|
|
54
|
+
# easyvista-python-client
|
|
55
|
+
|
|
56
|
+
Typed Python client for the EasyVista Service Manager REST API. Sync + async,
|
|
57
|
+
Pydantic models, Bearer or Basic auth.
|
|
58
|
+
|
|
59
|
+
While the package is preparing for 1.0, alot of potential breaking change might happen between versions. A deprecation policy will be put in place once 1.0 is out and the package have been stabilized.
|
|
60
|
+
|
|
61
|
+
## Documentation
|
|
62
|
+
|
|
63
|
+
Full documentation: https://easyvista-python-client.readthedocs.io/
|
|
64
|
+
|
|
65
|
+
Build it locally with `pip install -e ".[docs]"` then
|
|
66
|
+
`sphinx-build -b html -W docs docs/_build/html`.
|
|
67
|
+
|
|
68
|
+
## Install
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pip install easyvista-python-client
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Usage (sync)
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from easyvista_python_client import (
|
|
78
|
+
EasyvistaClient,
|
|
79
|
+
EasyvistaConfig,
|
|
80
|
+
PostRequest,
|
|
81
|
+
ev_equals_filter,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
config = EasyvistaConfig(server="https://my.easyvista.com", account="12345", token="...")
|
|
85
|
+
with EasyvistaClient(config) as client:
|
|
86
|
+
# catalog_code, the *_id values and the close status_guid are instance-specific.
|
|
87
|
+
ticket = client.create_ticket(
|
|
88
|
+
PostRequest(
|
|
89
|
+
catalog_code="INC_STANDARD",
|
|
90
|
+
title="Printer down",
|
|
91
|
+
description="The 3rd-floor printer is offline",
|
|
92
|
+
origin=7,
|
|
93
|
+
department_id=9,
|
|
94
|
+
urgency_id=8,
|
|
95
|
+
impact_id=28,
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
fetched = client.get_ticket(ticket.rfc_number)
|
|
99
|
+
open_status = ev_equals_filter("STATUS_ID", 3)
|
|
100
|
+
results = client.search_tickets(search=open_status, max_rows=50)
|
|
101
|
+
|
|
102
|
+
# page through everything with the iterator (follows the API's offset paging)
|
|
103
|
+
for t in client.iter_tickets(search=open_status, page_size=100, max_records=1000):
|
|
104
|
+
... # async: `async for t in client.iter_tickets(...)`
|
|
105
|
+
|
|
106
|
+
# close it with your instance's "closed" status GUID
|
|
107
|
+
client.close_ticket(
|
|
108
|
+
ticket.rfc_number,
|
|
109
|
+
status_guid="{00000000-0000-0000-0000-000000000000}",
|
|
110
|
+
delete_actions=1,
|
|
111
|
+
comment="Resolved",
|
|
112
|
+
)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
> Minimum fields for a create are catalog-specific (server-side). `catalog_code` + `title`
|
|
116
|
+
> work for incident catalogs; a missing mandatory field raises `EasyvistaValidationError`
|
|
117
|
+
> (HTTP 590, code 2013) — it is not retried.
|
|
118
|
+
|
|
119
|
+
## Assets and documents
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from pathlib import Path
|
|
123
|
+
from easyvista_python_client import (
|
|
124
|
+
EasyvistaClient,
|
|
125
|
+
EasyvistaConfig,
|
|
126
|
+
PostAsset,
|
|
127
|
+
ev_equals_filter,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
with EasyvistaClient(EasyvistaConfig.from_env()) as client:
|
|
131
|
+
asset = client.create_asset(PostAsset(catalog_id=3153, asset_tag="LAPTOP-001"))
|
|
132
|
+
tag_filter = ev_equals_filter("ASSET_TAG", "LAPTOP-001")
|
|
133
|
+
found = client.search_assets(search=tag_filter, max_rows=50)
|
|
134
|
+
|
|
135
|
+
# attach a file to a ticket (uploaded as base64 inside the JSON body)
|
|
136
|
+
pdf = Path("report.pdf")
|
|
137
|
+
client.add_document("I240101_0001", filename=pdf.name, content=pdf.read_bytes())
|
|
138
|
+
attachments = client.list_documents("I240101_0001")
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Usage (async)
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from easyvista_python_client import AsyncEasyvistaClient, EasyvistaConfig
|
|
145
|
+
|
|
146
|
+
async with AsyncEasyvistaClient(EasyvistaConfig.from_env()) as client:
|
|
147
|
+
ticket = await client.get_ticket("I240101_0001")
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Configuration via environment
|
|
151
|
+
|
|
152
|
+
Set `EASYVISTA_URL` (or `EASYVISTA_SERVER`), `EASYVISTA_ACCOUNT`, and either
|
|
153
|
+
`EASYVISTA_TOKEN` / `EASYVISTA_TOKEN_FILE` or `EASYVISTA_LOGIN` + `EASYVISTA_PASSWORD`,
|
|
154
|
+
then call `EasyvistaConfig.from_env()`.
|
|
155
|
+
|
|
156
|
+
## Agent skills
|
|
157
|
+
|
|
158
|
+
`skills/` holds Agent Skills for driving this client from an AI agent — one per
|
|
159
|
+
domain (client setup, search syntax, tickets, actions, documents, assets,
|
|
160
|
+
directory, reporting and context). Each is a directory with a `SKILL.md`
|
|
161
|
+
following the Agent Skills specification; see [skills/README.md](skills/README.md)
|
|
162
|
+
for the index.
|
|
163
|
+
|
|
164
|
+
They are source-tree material: present in the git repository and the source
|
|
165
|
+
distribution, absent from the installed wheel.
|
|
166
|
+
|
|
167
|
+
## Contributing
|
|
168
|
+
|
|
169
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and quality checks.
|
|
170
|
+
|
|
171
|
+
## License
|
|
172
|
+
|
|
173
|
+
MIT — see [LICENSE](LICENSE).
|
|
174
|
+
|
|
175
|
+
## Sponsoring
|
|
176
|
+
|
|
177
|
+
The development of this package is indirectly supported by
|
|
178
|
+
[Novahé](https://www.novahe.fr/) & [Constellation](https://www.constellation.fr/).
|