vulnotes 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,75 @@
1
+ """File attachments. Permissions: ``ro:reports`` / ``rw:reports``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import builtins
6
+ from typing import Any
7
+
8
+ from .._utils import FileTypes, omit_none, prepare_file
9
+ from ._base import JSON, Resource
10
+
11
+
12
+ class Attachments(Resource):
13
+ def list(
14
+ self,
15
+ *,
16
+ template_id: str | None = None,
17
+ company_id: str | None = None,
18
+ report_id: str | None = None,
19
+ vulnerability_id: str | None = None,
20
+ note_id: str | None = None,
21
+ ) -> builtins.list[dict[str, Any]]:
22
+ """List attachments, optionally filtered by associated entity."""
23
+ params = omit_none(
24
+ {
25
+ "templateId": template_id,
26
+ "companyId": company_id,
27
+ "reportId": report_id,
28
+ "vulnerabilityId": vulnerability_id,
29
+ "noteId": note_id,
30
+ }
31
+ )
32
+ return self._client.get("/attachments", params=params)
33
+
34
+ def upload(
35
+ self,
36
+ file: FileTypes,
37
+ *,
38
+ template_id: str | None = None,
39
+ company_id: str | None = None,
40
+ report_id: str | None = None,
41
+ vulnerability_id: str | None = None,
42
+ note_id: str | None = None,
43
+ user_id: str | None = None,
44
+ ) -> dict[str, Any]:
45
+ """Upload a file attachment. Requires ``rw:reports``.
46
+
47
+ Args:
48
+ file: A filesystem path, raw bytes, an open binary file object,
49
+ or a ``(filename, fileobj_or_bytes)`` tuple.
50
+ """
51
+ data = omit_none(
52
+ {
53
+ "templateId": template_id,
54
+ "companyId": company_id,
55
+ "reportId": report_id,
56
+ "vulnerabilityId": vulnerability_id,
57
+ "noteId": note_id,
58
+ "userId": user_id,
59
+ }
60
+ )
61
+ prepared = prepare_file(file, default_name="attachment.bin")
62
+ try:
63
+ return self._client.post(
64
+ "/attachments/upload", data=data, files={"file": prepared}
65
+ )
66
+ finally:
67
+ fh = prepared[1]
68
+ if hasattr(fh, "close"):
69
+ fh.close()
70
+
71
+ def list_for_report(self, report_id: str) -> builtins.list[dict[str, Any]]:
72
+ return self._client.get(f"/attachments/report/{report_id}")
73
+
74
+ def delete(self, attachment_id: str) -> JSON:
75
+ return self._client.delete(f"/attachments/{attachment_id}")
@@ -0,0 +1,98 @@
1
+ """Review comment threads on report snapshots.
2
+
3
+ Permissions: ``ro:reports`` / ``rw:reports``.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import builtins
9
+ from typing import Any
10
+
11
+ from .._utils import omit_none
12
+ from ._base import JSON, Resource
13
+
14
+
15
+ class Comments(Resource):
16
+ def list(
17
+ self,
18
+ report_id: str,
19
+ snapshot_id: str,
20
+ *,
21
+ resolved: bool | None = None,
22
+ type: str | None = None,
23
+ ref: str | None = None,
24
+ ) -> builtins.list[dict[str, Any]]:
25
+ """List comments on a snapshot, optionally filtered by resolved state,
26
+ attachment type, or attachment ref.
27
+
28
+ Returns the comment list; use :meth:`counts` for the aggregate counts.
29
+ """
30
+ params = omit_none(
31
+ {
32
+ "resolved": str(resolved).lower() if resolved is not None else None,
33
+ "type": type,
34
+ "ref": ref,
35
+ }
36
+ )
37
+ resp = self._client.get(
38
+ f"/reports/{report_id}/snapshots/{snapshot_id}/comments", params=params
39
+ )
40
+ if isinstance(resp, dict):
41
+ return resp.get("comments", [])
42
+ return resp
43
+
44
+ def create(
45
+ self,
46
+ report_id: str,
47
+ snapshot_id: str,
48
+ content: str,
49
+ *,
50
+ attachment_type: str | None = None,
51
+ attachment_ref: str | None = None,
52
+ finding_field: str | None = None,
53
+ parent_comment: str | None = None,
54
+ page_number: int | None = None,
55
+ bounding_box: dict[str, Any] | None = None,
56
+ ) -> dict[str, Any]:
57
+ """Create a review comment (or a reply, via ``parent_comment``)."""
58
+ body = omit_none(
59
+ {
60
+ "content": content,
61
+ "attachmentType": attachment_type,
62
+ "attachmentRef": attachment_ref,
63
+ "findingField": finding_field,
64
+ "parentComment": parent_comment,
65
+ "pageNumber": page_number,
66
+ "boundingBox": bounding_box,
67
+ }
68
+ )
69
+ return self._client.post(
70
+ f"/reports/{report_id}/snapshots/{snapshot_id}/comments", json=body
71
+ )
72
+
73
+ def counts(self, report_id: str, snapshot_id: str) -> dict[str, Any]:
74
+ """Get comment counts for a snapshot
75
+ (``total`` / ``unresolved`` / ``resolved`` / ``byType``)."""
76
+ return self._client.get(
77
+ f"/reports/{report_id}/snapshots/{snapshot_id}/comments/counts"
78
+ )
79
+
80
+ def annotations(self, report_id: str, snapshot_id: str) -> JSON:
81
+ """Get PDF annotations for a snapshot, as ``{"annotations": [...],
82
+ "byPage": {...}}``."""
83
+ return self._client.get(
84
+ f"/reports/{report_id}/snapshots/{snapshot_id}/annotations"
85
+ )
86
+
87
+ def update(self, report_id: str, comment_id: str, content: str) -> dict[str, Any]:
88
+ """Edit a comment's content."""
89
+ return self._client.put(
90
+ f"/reports/{report_id}/comments/{comment_id}", json={"content": content}
91
+ )
92
+
93
+ def delete(self, report_id: str, comment_id: str) -> JSON:
94
+ return self._client.delete(f"/reports/{report_id}/comments/{comment_id}")
95
+
96
+ def toggle_resolved(self, report_id: str, comment_id: str) -> dict[str, Any]:
97
+ """Toggle a comment's resolved status."""
98
+ return self._client.patch(f"/reports/{report_id}/comments/{comment_id}/resolve")
@@ -0,0 +1,77 @@
1
+ """Client companies. Permissions: ``ro:clients`` / ``rw:clients``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import builtins
6
+ from collections.abc import Iterator
7
+ from typing import Any
8
+
9
+ from .._utils import omit_none
10
+ from ._base import JSON, Resource, page_params, paginate
11
+
12
+
13
+ class Companies(Resource):
14
+ def list(self, *, page: int | None = None, limit: int | None = None) -> JSON:
15
+ """List companies, sorted by creation date descending.
16
+
17
+ Returns a plain list when called without arguments, or a paginated
18
+ envelope (``data`` plus a ``pagination`` object with ``page``,
19
+ ``limit``, ``total``, ``totalPages``, ``hasNextPage``) when ``page``
20
+ or ``limit`` is given.
21
+ """
22
+ return self._client.get("/companies", params=page_params(page, limit))
23
+
24
+ def iter(self, *, limit: int = 100) -> Iterator[dict[str, Any]]:
25
+ """Iterate over every company, fetching pages lazily."""
26
+ return paginate(lambda page, limit: self.list(page=page, limit=limit), limit=limit)
27
+
28
+ def search(self, query: str) -> builtins.list[dict[str, Any]]:
29
+ """Search companies by name (accent-insensitive)."""
30
+ return self._client.get("/companies/search", params={"query": query})
31
+
32
+ def get(self, company_id: str) -> dict[str, Any]:
33
+ return self._client.get(f"/companies/{company_id}")
34
+
35
+ def create(
36
+ self,
37
+ name: str,
38
+ *,
39
+ logo: str | None = None,
40
+ contacts: builtins.list[dict[str, Any]] | None = None,
41
+ ) -> dict[str, Any]:
42
+ """Create a company. Requires ``rw:clients``."""
43
+ body = omit_none({"name": name, "logo": logo, "contacts": contacts})
44
+ return self._client.post("/companies", json=body)
45
+
46
+ def update(
47
+ self,
48
+ company_id: str,
49
+ *,
50
+ name: str | None = None,
51
+ logo: str | None = None,
52
+ contacts: builtins.list[dict[str, Any]] | None = None,
53
+ ) -> dict[str, Any]:
54
+ """Update a company. Only the supplied fields are changed."""
55
+ body = omit_none({"name": name, "logo": logo, "contacts": contacts})
56
+ return self._client.put(f"/companies/{company_id}", json=body)
57
+
58
+ def delete(self, company_id: str) -> JSON:
59
+ return self._client.delete(f"/companies/{company_id}")
60
+
61
+ def portal_access(self, company_id: str) -> dict[str, Any]:
62
+ """Get the company's client-portal access configuration and users."""
63
+ return self._client.get(f"/companies/{company_id}/portal-access")
64
+
65
+ def update_client_user(
66
+ self,
67
+ company_id: str,
68
+ user_id: str,
69
+ *,
70
+ client_role: str | None = None,
71
+ active: bool | None = None,
72
+ ) -> dict[str, Any]:
73
+ """Update a company's client-portal user (role and/or active status)."""
74
+ body = omit_none({"clientRole": client_role, "active": active})
75
+ return self._client.patch(
76
+ f"/companies/{company_id}/client-users/{user_id}", json=body
77
+ )
@@ -0,0 +1,62 @@
1
+ """Findings embedded in a report. Permissions: ``ro:reports`` / ``rw:reports``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import builtins
6
+ from typing import Any
7
+
8
+ from ._base import JSON, Resource
9
+
10
+
11
+ class Findings(Resource):
12
+ def list(self, report_id: str) -> builtins.list[dict[str, Any]]:
13
+ """Get all findings for a report."""
14
+ return self._client.get(f"/reports/{report_id}/findings")
15
+
16
+ def add(self, report_id: str, finding: dict[str, Any]) -> dict[str, Any]:
17
+ """Append a finding to a report. Requires ``rw:reports``.
18
+
19
+ A unique ``id`` is generated automatically. If the report has a
20
+ vulnerability template and ``fields`` is not provided, the field
21
+ definitions are copied from it, and multilingual ``data`` entries are
22
+ initialized for all supported languages.
23
+
24
+ Example:
25
+ >>> client.findings.add(report_id, {
26
+ ... "title": "SQL Injection in /login",
27
+ ... "severity": "Critical",
28
+ ... "cvss": {"vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"},
29
+ ... "data": {"EN": {"title": "SQL Injection in /login",
30
+ ... "description": "..."}},
31
+ ... })
32
+ """
33
+ return self._client.post(f"/reports/{report_id}/findings", json=finding)
34
+
35
+ def replace_all(
36
+ self, report_id: str, findings: builtins.list[dict[str, Any]]
37
+ ) -> builtins.list[dict[str, Any]]:
38
+ """Replace the report's entire findings array (also used for reordering)."""
39
+ return self._client.put(f"/reports/{report_id}/findings", json=findings)
40
+
41
+ def update(
42
+ self, report_id: str, finding_id: str, changes: dict[str, Any]
43
+ ) -> dict[str, Any]:
44
+ """Partially update a finding.
45
+
46
+ Accepted fields: ``title``, ``severity``, ``status``, ``description``,
47
+ ``impact``, ``remediation``, ``references``, ``cvss``, ``cwe``,
48
+ ``affectedSystems``, ``evidence``, ``data``, ``order``, ``fields``,
49
+ ``isComplete``. Other fields are ignored.
50
+
51
+ Args:
52
+ report_id: The report's ObjectId.
53
+ finding_id: The finding's ``id`` field (a generated string, not an ObjectId).
54
+ changes: The fields to change.
55
+ """
56
+ return self._client.put(
57
+ f"/reports/{report_id}/findings/{finding_id}", json=changes
58
+ )
59
+
60
+ def delete(self, report_id: str, finding_id: str) -> JSON:
61
+ """Remove a finding from the report."""
62
+ return self._client.delete(f"/reports/{report_id}/findings/{finding_id}")
@@ -0,0 +1,85 @@
1
+ """Image upload and management. Permissions: ``ro:reports`` / ``rw:reports``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import builtins
6
+ from typing import Any
7
+
8
+ from .._utils import FileTypes, omit_none, prepare_file
9
+ from ._base import JSON, Resource
10
+
11
+
12
+ class Images(Resource):
13
+ def list(
14
+ self,
15
+ *,
16
+ template_id: str | None = None,
17
+ client_id: str | None = None,
18
+ report_id: str | None = None,
19
+ ) -> builtins.list[dict[str, Any]]:
20
+ """List images, optionally filtered by template, client, or report."""
21
+ params = omit_none(
22
+ {"templateId": template_id, "clientId": client_id, "reportId": report_id}
23
+ )
24
+ return self._client.get("/images", params=params)
25
+
26
+ def upload(
27
+ self,
28
+ image: FileTypes,
29
+ *,
30
+ template_id: str | None = None,
31
+ client_id: str | None = None,
32
+ report_id: str | None = None,
33
+ note_id: str | None = None,
34
+ vulnerability_id: str | None = None,
35
+ company_id: str | None = None,
36
+ user_id: str | None = None,
37
+ ) -> dict[str, Any]:
38
+ """Upload an image and associate it with an entity. Requires ``rw:reports``.
39
+
40
+ Args:
41
+ image: A filesystem path, raw bytes, an open binary file object,
42
+ or a ``(filename, fileobj_or_bytes)`` tuple.
43
+ template_id / client_id / report_id / note_id / vulnerability_id /
44
+ company_id / user_id: Entity to associate the image with.
45
+ """
46
+ data = omit_none(
47
+ {
48
+ "templateId": template_id,
49
+ "clientId": client_id,
50
+ "reportId": report_id,
51
+ "noteId": note_id,
52
+ "vulnerabilityId": vulnerability_id,
53
+ "companyId": company_id,
54
+ "userId": user_id,
55
+ }
56
+ )
57
+ prepared = prepare_file(image, default_name="image.png")
58
+ try:
59
+ return self._client.post(
60
+ "/images/upload", data=data, files={"image": prepared}
61
+ )
62
+ finally:
63
+ fh = prepared[1]
64
+ if hasattr(fh, "close"):
65
+ fh.close()
66
+
67
+ def list_for_note(self, note_id: str) -> builtins.list[dict[str, Any]]:
68
+ return self._client.get(f"/images/note/{note_id}")
69
+
70
+ def list_for_template(self, template_id: str) -> builtins.list[dict[str, Any]]:
71
+ return self._client.get(f"/images/template/{template_id}")
72
+
73
+ def list_for_user(self, user_id: str) -> builtins.list[dict[str, Any]]:
74
+ return self._client.get(f"/images/user/{user_id}")
75
+
76
+ def associate_with_template(
77
+ self, template_id: str, image_ids: builtins.list[str]
78
+ ) -> JSON:
79
+ """Associate previously uploaded images with a template."""
80
+ return self._client.post(
81
+ f"/images/template/{template_id}/associate", json={"imageIds": image_ids}
82
+ )
83
+
84
+ def delete(self, image_id: str) -> JSON:
85
+ return self._client.delete(f"/images/{image_id}")
@@ -0,0 +1,71 @@
1
+ """Notes attached to reports. Permissions: ``ro:reports`` / ``rw:reports``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from .._utils import omit_none
8
+ from ._base import JSON, Resource
9
+
10
+
11
+ class Notes(Resource):
12
+ def list_for_report(self, report_id: str) -> list[dict[str, Any]]:
13
+ """Get all notes attached to a report."""
14
+ return self._client.get(f"/notes/report/{report_id}")
15
+
16
+ def create(
17
+ self,
18
+ report_id: str,
19
+ title: str,
20
+ *,
21
+ content: str | None = None,
22
+ is_pinned: bool | None = None,
23
+ tags: list[str] | None = None,
24
+ ) -> dict[str, Any]:
25
+ """Create a note on a report. Requires ``rw:reports``."""
26
+ body = omit_none(
27
+ {"title": title, "content": content, "isPinned": is_pinned, "tags": tags}
28
+ )
29
+ return self._client.post(f"/notes/report/{report_id}", json=body)
30
+
31
+ def get(self, note_id: str) -> dict[str, Any]:
32
+ return self._client.get(f"/notes/{note_id}")
33
+
34
+ def update(
35
+ self,
36
+ note_id: str,
37
+ *,
38
+ title: str | None = None,
39
+ content: str | None = None,
40
+ is_pinned: bool | None = None,
41
+ tags: list[str] | None = None,
42
+ ) -> dict[str, Any]:
43
+ """Update a note. Only the supplied fields are changed."""
44
+ body = omit_none(
45
+ {"title": title, "content": content, "isPinned": is_pinned, "tags": tags}
46
+ )
47
+ return self._client.put(f"/notes/{note_id}", json=body)
48
+
49
+ def delete(self, note_id: str) -> JSON:
50
+ return self._client.delete(f"/notes/{note_id}")
51
+
52
+ def save(
53
+ self,
54
+ note_id: str,
55
+ *,
56
+ content: str | None = None,
57
+ title: str | None = None,
58
+ ) -> dict[str, Any]:
59
+ """Autosave-style update of a note's content and title.
60
+
61
+ Warning: this endpoint always rewrites the title. When ``title`` is
62
+ omitted it is re-extracted from the first ``# heading`` line of the
63
+ content, falling back to ``"Untitled Note"``. Pass ``title``
64
+ explicitly (or use :meth:`update`) to keep the existing one.
65
+ """
66
+ body = omit_none({"content": content, "title": title})
67
+ return self._client.patch(f"/notes/{note_id}/save", json=body)
68
+
69
+ def toggle_pin(self, note_id: str) -> dict[str, Any]:
70
+ """Toggle the note's pinned status."""
71
+ return self._client.patch(f"/notes/{note_id}/toggle-pin")
@@ -0,0 +1,210 @@
1
+ """The resource-planning calendar: engagements and availability.
2
+
3
+ Permissions: ``ro:planning`` to read, ``rw:planning`` to write,
4
+ ``manage:planning`` to delete events or assign users outside your own team.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import datetime as _dt
10
+ from collections.abc import Iterator
11
+ from typing import Any, Union
12
+
13
+ from .._utils import iso, omit_none
14
+ from ._base import JSON, Resource, page_params, paginate
15
+
16
+ DateLike = Union[str, _dt.date, _dt.datetime]
17
+
18
+
19
+ class Planning(Resource):
20
+ # ── calendar ─────────────────────────────────────────────────────────
21
+
22
+ def calendar(self, start: DateLike, end: DateLike) -> dict[str, Any]:
23
+ """Get the combined planning calendar (events + availability) for a date range."""
24
+ return self._client.get(
25
+ "/planning/calendar", params={"start": iso(start), "end": iso(end)}
26
+ )
27
+
28
+ def users(self, *, skills: str | None = None) -> list[dict[str, Any]]:
29
+ """List calendar users, optionally filtered by skills."""
30
+ return self._client.get("/planning/users", params=omit_none({"skills": skills}))
31
+
32
+ # ── events ───────────────────────────────────────────────────────────
33
+
34
+ def list_events(
35
+ self, *, page: int | None = None, limit: int | None = None
36
+ ) -> JSON:
37
+ """List planning events (plain list, or paginated envelope with ``page``/``limit``)."""
38
+ return self._client.get("/planning/events", params=page_params(page, limit))
39
+
40
+ def iter_events(self, *, limit: int = 100) -> Iterator[dict[str, Any]]:
41
+ """Iterate over every visible planning event, fetching pages lazily."""
42
+ return paginate(
43
+ lambda page, limit: self.list_events(page=page, limit=limit), limit=limit
44
+ )
45
+
46
+ def events_in_range(self, start: DateLike, end: DateLike) -> list[dict[str, Any]]:
47
+ """List planning events overlapping a date range."""
48
+ return self._client.get(
49
+ "/planning/events/range", params={"start": iso(start), "end": iso(end)}
50
+ )
51
+
52
+ def check_conflicts(
53
+ self,
54
+ user_id: str,
55
+ start: DateLike,
56
+ end: DateLike,
57
+ *,
58
+ exclude_event_id: str | None = None,
59
+ ) -> dict[str, Any]:
60
+ """Check whether a user is already booked during a date range."""
61
+ params = omit_none(
62
+ {"start": iso(start), "end": iso(end), "excludeEventId": exclude_event_id}
63
+ )
64
+ return self._client.get(f"/planning/events/conflicts/{user_id}", params=params)
65
+
66
+ def get_event(self, event_id: str) -> dict[str, Any]:
67
+ return self._client.get(f"/planning/events/{event_id}")
68
+
69
+ def create_event(
70
+ self,
71
+ title: str,
72
+ start_date: DateLike,
73
+ end_date: DateLike,
74
+ assignees: list[str],
75
+ event_type: str,
76
+ *,
77
+ description: str | None = None,
78
+ client: str | None = None,
79
+ status: str | None = None,
80
+ color: str | None = None,
81
+ notes: str | None = None,
82
+ ) -> dict[str, Any]:
83
+ """Book an engagement on the calendar. Requires ``rw:planning``.
84
+
85
+ Args:
86
+ title: Event title (max 200 chars).
87
+ start_date / end_date: ``end_date`` must be strictly after ``start_date``.
88
+ assignees: User ObjectIds (at least one). Non-admin callers may only
89
+ assign members of their own team.
90
+ event_type: One of ``pentest``, ``training``, ``conference``,
91
+ ``research``, ``other``.
92
+ client: Company ObjectId.
93
+ status: One of ``planned``, ``requirements``, ``in-progress``,
94
+ ``completed``, ``cancelled``.
95
+ color: Hex color, e.g. ``"#3B82F6"``.
96
+ """
97
+ body = omit_none(
98
+ {
99
+ "title": title,
100
+ "startDate": iso(start_date),
101
+ "endDate": iso(end_date),
102
+ "assignees": assignees,
103
+ "eventType": event_type,
104
+ "description": description,
105
+ "client": client,
106
+ "status": status,
107
+ "color": color,
108
+ "notes": notes,
109
+ }
110
+ )
111
+ return self._client.post("/planning/events", json=body)
112
+
113
+ def update_event(
114
+ self,
115
+ event_id: str,
116
+ *,
117
+ title: str | None = None,
118
+ start_date: DateLike | None = None,
119
+ end_date: DateLike | None = None,
120
+ assignees: list[str] | None = None,
121
+ event_type: str | None = None,
122
+ description: str | None = None,
123
+ client: str | None = None,
124
+ status: str | None = None,
125
+ color: str | None = None,
126
+ notes: str | None = None,
127
+ ) -> dict[str, Any]:
128
+ """Update a planning event. Only the supplied fields are changed."""
129
+ body = omit_none(
130
+ {
131
+ "title": title,
132
+ "startDate": iso(start_date),
133
+ "endDate": iso(end_date),
134
+ "assignees": assignees,
135
+ "eventType": event_type,
136
+ "description": description,
137
+ "client": client,
138
+ "status": status,
139
+ "color": color,
140
+ "notes": notes,
141
+ }
142
+ )
143
+ return self._client.put(f"/planning/events/{event_id}", json=body)
144
+
145
+ def delete_event(self, event_id: str) -> JSON:
146
+ """Delete a planning event. Requires ``manage:planning``."""
147
+ return self._client.delete(f"/planning/events/{event_id}")
148
+
149
+ # ── availability ─────────────────────────────────────────────────────
150
+
151
+ def list_availability(
152
+ self, *, page: int | None = None, limit: int | None = None
153
+ ) -> JSON:
154
+ """List availability entries (plain list, or paginated envelope)."""
155
+ return self._client.get("/planning/availability", params=page_params(page, limit))
156
+
157
+ def availability_in_range(
158
+ self, start: DateLike, end: DateLike
159
+ ) -> list[dict[str, Any]]:
160
+ """List availability entries overlapping a date range."""
161
+ return self._client.get(
162
+ "/planning/availability/range", params={"start": iso(start), "end": iso(end)}
163
+ )
164
+
165
+ def create_availability(
166
+ self,
167
+ type: str,
168
+ start_date: DateLike,
169
+ end_date: DateLike,
170
+ *,
171
+ description: str | None = None,
172
+ ) -> dict[str, Any]:
173
+ """Mark availability for the API key's owner. Requires ``rw:planning``.
174
+
175
+ Args:
176
+ type: One of ``vacation``, ``conference``, ``sick``, ``training``, ``other``.
177
+ start_date / end_date: ``end_date`` must be strictly after ``start_date``.
178
+ """
179
+ body = omit_none(
180
+ {
181
+ "type": type,
182
+ "startDate": iso(start_date),
183
+ "endDate": iso(end_date),
184
+ "description": description,
185
+ }
186
+ )
187
+ return self._client.post("/planning/availability", json=body)
188
+
189
+ def update_availability(
190
+ self,
191
+ availability_id: str,
192
+ *,
193
+ type: str | None = None,
194
+ start_date: DateLike | None = None,
195
+ end_date: DateLike | None = None,
196
+ description: str | None = None,
197
+ ) -> dict[str, Any]:
198
+ """Update an availability entry. Only the supplied fields are changed."""
199
+ body = omit_none(
200
+ {
201
+ "type": type,
202
+ "startDate": iso(start_date),
203
+ "endDate": iso(end_date),
204
+ "description": description,
205
+ }
206
+ )
207
+ return self._client.put(f"/planning/availability/{availability_id}", json=body)
208
+
209
+ def delete_availability(self, availability_id: str) -> JSON:
210
+ return self._client.delete(f"/planning/availability/{availability_id}")