Auto-commit local changes before build (2026-02-03 14:13:08)
This commit is contained in:
parent
ddc6eaa12a
commit
a4a6a60d45
@ -1 +1 @@
|
|||||||
v20260203-06-autotask-ticketnotes-child-endpoint
|
v20260203-07-autotask-notes-endpoint-fix
|
||||||
|
|||||||
@ -111,6 +111,7 @@ class AutotaskClient:
|
|||||||
path: str,
|
path: str,
|
||||||
params: Optional[Dict[str, Any]] = None,
|
params: Optional[Dict[str, Any]] = None,
|
||||||
json_body: Optional[Dict[str, Any]] = None,
|
json_body: Optional[Dict[str, Any]] = None,
|
||||||
|
parse_json: bool = True,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
zone = self.get_zone_info()
|
zone = self.get_zone_info()
|
||||||
base = zone.api_url.rstrip("/")
|
base = zone.api_url.rstrip("/")
|
||||||
@ -161,12 +162,48 @@ class AutotaskClient:
|
|||||||
if resp.status_code == 404:
|
if resp.status_code == 404:
|
||||||
raise AutotaskError(f"Resource not found (HTTP 404) for path: {path}", status_code=404)
|
raise AutotaskError(f"Resource not found (HTTP 404) for path: {path}", status_code=404)
|
||||||
if resp.status_code >= 400:
|
if resp.status_code >= 400:
|
||||||
raise AutotaskError(f"Autotask API error (HTTP {resp.status_code}).", status_code=resp.status_code)
|
snippet = (resp.text or "").strip().replace("\r", " ").replace("\n", " ")
|
||||||
|
if len(snippet) > 500:
|
||||||
|
snippet = snippet[:500] + "..."
|
||||||
|
msg = f"Autotask API error (HTTP {resp.status_code})."
|
||||||
|
if snippet:
|
||||||
|
msg += f" Body: {snippet}"
|
||||||
|
raise AutotaskError(msg, status_code=resp.status_code)
|
||||||
|
|
||||||
|
# Successful responses may return an empty body (204) or non-JSON content for some write endpoints.
|
||||||
|
body_text = resp.text or ""
|
||||||
|
content_type = (resp.headers.get("Content-Type") or "").lower()
|
||||||
|
|
||||||
|
if not parse_json:
|
||||||
|
if resp.status_code == 204 or body_text.strip() == "":
|
||||||
|
return None
|
||||||
|
if "json" in content_type:
|
||||||
|
try:
|
||||||
|
return resp.json()
|
||||||
|
except Exception:
|
||||||
|
return body_text
|
||||||
|
return body_text
|
||||||
|
|
||||||
|
if resp.status_code == 204 or body_text.strip() == "":
|
||||||
|
return None
|
||||||
|
|
||||||
|
if "json" not in content_type:
|
||||||
|
raise AutotaskError(
|
||||||
|
f"Autotask API response is not JSON (Content-Type: {content_type or 'unknown'}).",
|
||||||
|
status_code=resp.status_code,
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return resp.json()
|
return resp.json()
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise AutotaskError("Autotask API response is not valid JSON.") from exc
|
snippet = body_text.strip().replace("\r", " ").replace("\n", " ")
|
||||||
|
if len(snippet) > 500:
|
||||||
|
snippet = snippet[:500] + "..."
|
||||||
|
raise AutotaskError(
|
||||||
|
"Autotask API response is not valid JSON." + (f" Body: {snippet}" if snippet else ""),
|
||||||
|
status_code=resp.status_code,
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
def _as_items_list(self, payload: Any) -> List[Dict[str, Any]]:
|
def _as_items_list(self, payload: Any) -> List[Dict[str, Any]]:
|
||||||
"""Normalize common Autotask REST payload shapes to a list of dicts."""
|
"""Normalize common Autotask REST payload shapes to a list of dicts."""
|
||||||
@ -522,68 +559,74 @@ class AutotaskClient:
|
|||||||
|
|
||||||
|
|
||||||
def create_ticket_note(self, note_payload: Dict[str, Any]) -> Dict[str, Any]:
|
def create_ticket_note(self, note_payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Create a TicketNote for a Ticket.
|
"""Create a user-visible note on a Ticket.
|
||||||
|
|
||||||
Autotask TicketNotes are a child collection of Tickets. In some tenants, creating notes via the
|
Preferred route (validated in Postman for this tenant):
|
||||||
root entity endpoint (POST /TicketNotes) is not supported, while creating via the parent ticket
|
- POST /Tickets/{id}/Notes
|
||||||
child URL may work (POST /Tickets/{id}/TicketNotes).
|
|
||||||
|
|
||||||
Callers can keep a fallback (for example updating the Ticket description) if both routes fail.
|
Some tenants return an empty body (204) or non-JSON content on success; callers should verify
|
||||||
"""
|
creation via TicketNotes queries if needed.
|
||||||
|
"""
|
||||||
|
|
||||||
if not isinstance(note_payload, dict):
|
if not isinstance(note_payload, dict):
|
||||||
raise AutotaskError("Invalid ticket note payload.")
|
raise AutotaskError("Invalid ticket note payload.")
|
||||||
|
|
||||||
try:
|
|
||||||
tid = int(note_payload.get("ticketID") or note_payload.get("ticketId") or 0)
|
|
||||||
except Exception:
|
|
||||||
tid = 0
|
|
||||||
if tid <= 0:
|
|
||||||
raise AutotaskError("Invalid ticketID in ticket note payload.")
|
|
||||||
|
|
||||||
title = str(note_payload.get("title") or "Backupchecks")
|
|
||||||
description = str(
|
|
||||||
note_payload.get("description")
|
|
||||||
or note_payload.get("note")
|
|
||||||
or note_payload.get("body")
|
|
||||||
or ""
|
|
||||||
)
|
|
||||||
|
|
||||||
pub_val = note_payload.get("publish")
|
|
||||||
# REST uses an integer picklist; in practice '1' corresponds to "ALL" / all Autotask users.
|
|
||||||
if isinstance(pub_val, bool):
|
|
||||||
publish = 1 if pub_val else 1
|
|
||||||
else:
|
|
||||||
try:
|
try:
|
||||||
publish = int(pub_val) if pub_val is not None else 1
|
tid = int(note_payload.get("ticketID") or note_payload.get("ticketId") or 0)
|
||||||
except Exception:
|
except Exception:
|
||||||
publish = 1
|
tid = 0
|
||||||
|
if tid <= 0:
|
||||||
|
raise AutotaskError("Invalid ticketID in ticket note payload.")
|
||||||
|
|
||||||
child_payload = {
|
title = str(note_payload.get("title") or "Backupchecks")
|
||||||
"title": title,
|
description = str(
|
||||||
"description": description,
|
note_payload.get("description")
|
||||||
"publish": publish,
|
or note_payload.get("note")
|
||||||
}
|
or note_payload.get("body")
|
||||||
|
or ""
|
||||||
|
)
|
||||||
|
|
||||||
# Preferred: parent-child URL
|
pub_val = note_payload.get("publish")
|
||||||
data = self._request("POST", f"Tickets/{tid}/TicketNotes", json_body=child_payload)
|
# REST uses an integer picklist; in practice "1" corresponds to publishing to all users.
|
||||||
|
if isinstance(pub_val, bool):
|
||||||
|
publish = 1 if pub_val else 1
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
publish = int(pub_val) if pub_val is not None else 1
|
||||||
|
except Exception:
|
||||||
|
publish = 1
|
||||||
|
|
||||||
if isinstance(data, dict):
|
child_payload = {
|
||||||
if "item" in data and isinstance(data.get("item"), dict):
|
"title": title,
|
||||||
return data["item"]
|
# Some tenants expect "description", others expect "note". We send both.
|
||||||
if "items" in data and isinstance(data.get("items"), list) and data.get("items"):
|
"description": description,
|
||||||
first = data.get("items")[0]
|
"note": description,
|
||||||
if isinstance(first, dict):
|
"noteType": 1,
|
||||||
return first
|
"publish": publish,
|
||||||
if "id" in data:
|
}
|
||||||
return data
|
|
||||||
|
|
||||||
items = self._as_items_list(data)
|
# Validated: parent-child Notes endpoint
|
||||||
if items:
|
data = self._request("POST", f"Tickets/{tid}/Notes", json_body=child_payload, parse_json=False)
|
||||||
return items[0]
|
|
||||||
|
|
||||||
raise AutotaskError("Autotask did not return a created ticket note object.")
|
# Successful note creation may return no body; allow callers to verify via queries.
|
||||||
|
if data is None or isinstance(data, str):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
if isinstance(data, dict):
|
||||||
|
if "item" in data and isinstance(data.get("item"), dict):
|
||||||
|
return data["item"]
|
||||||
|
if "items" in data and isinstance(data.get("items"), list) and data.get("items"):
|
||||||
|
first = data.get("items")[0]
|
||||||
|
if isinstance(first, dict):
|
||||||
|
return first
|
||||||
|
if "id" in data:
|
||||||
|
return data
|
||||||
|
|
||||||
|
items = self._as_items_list(data)
|
||||||
|
if items:
|
||||||
|
return items[0]
|
||||||
|
|
||||||
|
return {}
|
||||||
def get_ticket_note(self, note_id: int) -> Dict[str, Any]:
|
def get_ticket_note(self, note_id: int) -> Dict[str, Any]:
|
||||||
"""Retrieve a TicketNote by ID via GET /TicketNotes/{id}."""
|
"""Retrieve a TicketNote by ID via GET /TicketNotes/{id}."""
|
||||||
|
|
||||||
|
|||||||
@ -1815,7 +1815,7 @@ def api_run_checks_autotask_resolve_note():
|
|||||||
"""Post a user-visible 'should be resolved' update to an existing Autotask ticket.
|
"""Post a user-visible 'should be resolved' update to an existing Autotask ticket.
|
||||||
|
|
||||||
This step does NOT close the ticket in Autotask.
|
This step does NOT close the ticket in Autotask.
|
||||||
Primary behaviour: create a TicketNote via POST /TicketNotes so the message is clearly visible.
|
Primary behaviour: create a Ticket note via POST /Tickets/{id}/Notes so the message is clearly visible.
|
||||||
Fallback behaviour: if TicketNote create is not supported (HTTP 404), append the marker text
|
Fallback behaviour: if TicketNote create is not supported (HTTP 404), append the marker text
|
||||||
to the Ticket description via PUT /Tickets and verify persistence.
|
to the Ticket description via PUT /Tickets and verify persistence.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -493,6 +493,13 @@ Changes:
|
|||||||
- Removed usage of the unsupported POST /TicketNotes endpoint.
|
- Removed usage of the unsupported POST /TicketNotes endpoint.
|
||||||
- Ensured the created note is user-visible in Autotask and clearly marks the ticket as resolved by Backupchecks.
|
- Ensured the created note is user-visible in Autotask and clearly marks the ticket as resolved by Backupchecks.
|
||||||
|
|
||||||
|
## v20260203-07-autotask-notes-endpoint-fix
|
||||||
|
|
||||||
|
- Fixed Autotask ticket note creation using the POST /Tickets/{TicketID}/Notes endpoint.
|
||||||
|
- Updated response handling to support empty or non-JSON success responses without JSON parsing errors.
|
||||||
|
- Improved backend error handling so Autotask write errors return valid JSON instead of breaking the request.
|
||||||
|
- Made the ticket note payload more robust to support tenant-specific field requirements.
|
||||||
|
|
||||||
***
|
***
|
||||||
|
|
||||||
## v0.1.21
|
## v0.1.21
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user