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.
Files changed (66) hide show
  1. easyvista_python_client-0.1.0/.gitignore +43 -0
  2. easyvista_python_client-0.1.0/.pre-commit-config.yaml +79 -0
  3. easyvista_python_client-0.1.0/.readthedocs.yaml +17 -0
  4. easyvista_python_client-0.1.0/CHANGELOG.md +192 -0
  5. easyvista_python_client-0.1.0/CONTRIBUTING.md +94 -0
  6. easyvista_python_client-0.1.0/LICENSE +21 -0
  7. easyvista_python_client-0.1.0/PKG-INFO +178 -0
  8. easyvista_python_client-0.1.0/README.md +125 -0
  9. easyvista_python_client-0.1.0/docs/_static/.gitkeep +0 -0
  10. easyvista_python_client-0.1.0/docs/_templates/.gitkeep +0 -0
  11. easyvista_python_client-0.1.0/docs/api_reference.rst +123 -0
  12. easyvista_python_client-0.1.0/docs/conf.py +68 -0
  13. easyvista_python_client-0.1.0/docs/development.rst +97 -0
  14. easyvista_python_client-0.1.0/docs/index.rst +26 -0
  15. easyvista_python_client-0.1.0/docs/installation.rst +46 -0
  16. easyvista_python_client-0.1.0/docs/publishing.rst +65 -0
  17. easyvista_python_client-0.1.0/docs/sponsoring.rst +5 -0
  18. easyvista_python_client-0.1.0/docs/user_guide.rst +555 -0
  19. easyvista_python_client-0.1.0/easyvista_python_client/__init__.py +74 -0
  20. easyvista_python_client-0.1.0/easyvista_python_client/_async/__init__.py +13 -0
  21. easyvista_python_client-0.1.0/easyvista_python_client/_async/_concurrency.py +78 -0
  22. easyvista_python_client-0.1.0/easyvista_python_client/_async/_transport.py +266 -0
  23. easyvista_python_client-0.1.0/easyvista_python_client/_async/client.py +790 -0
  24. easyvista_python_client-0.1.0/easyvista_python_client/_fields.py +26 -0
  25. easyvista_python_client-0.1.0/easyvista_python_client/_html.py +41 -0
  26. easyvista_python_client-0.1.0/easyvista_python_client/_sync/__init__.py +13 -0
  27. easyvista_python_client-0.1.0/easyvista_python_client/_sync/_concurrency.py +50 -0
  28. easyvista_python_client-0.1.0/easyvista_python_client/_sync/_transport.py +266 -0
  29. easyvista_python_client-0.1.0/easyvista_python_client/_sync/client.py +790 -0
  30. easyvista_python_client-0.1.0/easyvista_python_client/_transport.py +29 -0
  31. easyvista_python_client-0.1.0/easyvista_python_client/config.py +71 -0
  32. easyvista_python_client-0.1.0/easyvista_python_client/context.py +116 -0
  33. easyvista_python_client-0.1.0/easyvista_python_client/directory.py +53 -0
  34. easyvista_python_client-0.1.0/easyvista_python_client/exceptions.py +53 -0
  35. easyvista_python_client-0.1.0/easyvista_python_client/field_model.py +74 -0
  36. easyvista_python_client-0.1.0/easyvista_python_client/filters.py +82 -0
  37. easyvista_python_client-0.1.0/easyvista_python_client/models/__init__.py +1 -0
  38. easyvista_python_client-0.1.0/easyvista_python_client/models/action.py +65 -0
  39. easyvista_python_client-0.1.0/easyvista_python_client/models/asset.py +36 -0
  40. easyvista_python_client-0.1.0/easyvista_python_client/models/common.py +78 -0
  41. easyvista_python_client-0.1.0/easyvista_python_client/models/department.py +68 -0
  42. easyvista_python_client-0.1.0/easyvista_python_client/models/document.py +32 -0
  43. easyvista_python_client-0.1.0/easyvista_python_client/models/employee.py +67 -0
  44. easyvista_python_client-0.1.0/easyvista_python_client/models/request.py +172 -0
  45. easyvista_python_client-0.1.0/easyvista_python_client/pagination.py +84 -0
  46. easyvista_python_client-0.1.0/easyvista_python_client/py.typed +0 -0
  47. easyvista_python_client-0.1.0/easyvista_python_client/references.py +146 -0
  48. easyvista_python_client-0.1.0/easyvista_python_client/reporting.py +143 -0
  49. easyvista_python_client-0.1.0/easyvista_python_client/resources/__init__.py +1 -0
  50. easyvista_python_client-0.1.0/easyvista_python_client/resources/actions.py +72 -0
  51. easyvista_python_client-0.1.0/easyvista_python_client/resources/assets.py +47 -0
  52. easyvista_python_client-0.1.0/easyvista_python_client/resources/departments.py +57 -0
  53. easyvista_python_client-0.1.0/easyvista_python_client/resources/descriptor.py +99 -0
  54. easyvista_python_client-0.1.0/easyvista_python_client/resources/documents.py +78 -0
  55. easyvista_python_client-0.1.0/easyvista_python_client/resources/employees.py +57 -0
  56. easyvista_python_client-0.1.0/easyvista_python_client/resources/requests.py +91 -0
  57. easyvista_python_client-0.1.0/pyproject.toml +226 -0
  58. easyvista_python_client-0.1.0/skills/README.md +45 -0
  59. easyvista_python_client-0.1.0/skills/easyvista-asset-workflow/SKILL.md +132 -0
  60. easyvista_python_client-0.1.0/skills/easyvista-client-setup/SKILL.md +186 -0
  61. easyvista_python_client-0.1.0/skills/easyvista-directory/SKILL.md +152 -0
  62. easyvista_python_client-0.1.0/skills/easyvista-document-workflow/SKILL.md +96 -0
  63. easyvista_python_client-0.1.0/skills/easyvista-reporting-and-context/SKILL.md +183 -0
  64. easyvista_python_client-0.1.0/skills/easyvista-search-syntax/SKILL.md +173 -0
  65. easyvista_python_client-0.1.0/skills/easyvista-ticket-actions/SKILL.md +134 -0
  66. 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,17 @@
1
+ version: 2
2
+
3
+ build:
4
+ os: ubuntu-24.04
5
+ tools:
6
+ python: "3.12"
7
+
8
+ sphinx:
9
+ configuration: docs/conf.py
10
+ fail_on_warning: true
11
+
12
+ python:
13
+ install:
14
+ - method: pip
15
+ path: .
16
+ extra_requirements:
17
+ - docs
@@ -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/).