Auto-commit local changes before build (2026-01-15 16:17:26)

This commit is contained in:
Ivo Oskamp 2026-01-15 16:17:26 +01:00
parent 473044bd67
commit 66f5a57fe0
3 changed files with 84 additions and 16 deletions

View File

@ -1 +1 @@
v20260115-15-autotask-default-ticket-status-setting
v20260115-16-autotask-ticket-create-response-fix

View File

@ -105,19 +105,20 @@ class AutotaskClient:
"Accept": "application/json",
}
def _request(
def _request_raw(
self,
method: str,
path: str,
params: Optional[Dict[str, Any]] = None,
json_body: Optional[Dict[str, Any]] = None,
) -> Any:
) -> requests.Response:
"""Perform an Autotask REST API request and return the raw response."""
zone = self.get_zone_info()
base = zone.api_url.rstrip("/")
url = f"{base}/v1.0/{path.lstrip('/')}"
headers = self._headers()
def do_request(use_basic_auth: bool, extra_headers: Optional[Dict[str, str]] = None):
def do_request(use_basic_auth: bool, extra_headers: Optional[Dict[str, str]] = None) -> requests.Response:
h = dict(headers)
if extra_headers:
h.update(extra_headers)
@ -149,8 +150,7 @@ class AutotaskClient:
raise AutotaskError(
"Authentication failed (HTTP 401). "
"Verify API Username, API Secret, and ApiIntegrationCode. "
f"Environment={self.environment}, ZoneInfoBase={zi_base}, ZoneApiUrl={zone.api_url}."
,
f"Environment={self.environment}, ZoneInfoBase={zi_base}, ZoneApiUrl={zone.api_url}.",
status_code=401,
)
if resp.status_code == 403:
@ -163,6 +163,19 @@ class AutotaskClient:
if resp.status_code >= 400:
raise AutotaskError(f"Autotask API error (HTTP {resp.status_code}).", status_code=resp.status_code)
return resp
def _request(
self,
method: str,
path: str,
params: Optional[Dict[str, Any]] = None,
json_body: Optional[Dict[str, Any]] = None,
) -> Any:
resp = self._request_raw(method=method, path=path, params=params, json_body=json_body)
if not (resp.content or b""):
return {}
try:
return resp.json()
except Exception as exc:
@ -404,6 +417,15 @@ class AutotaskClient:
"""
return self._get_ticket_picklist_values(field_names=["status", "statusid"])
def get_ticket(self, ticket_id: int) -> Dict[str, Any]:
"""Fetch a Ticket by ID via GET /Tickets/<id>."""
if not isinstance(ticket_id, int) or ticket_id <= 0:
raise AutotaskError("Invalid Autotask ticket id.")
data = self._request("GET", f"Tickets/{ticket_id}")
if isinstance(data, dict) and data:
return data
raise AutotaskError("Autotask did not return a ticket object.")
def create_ticket(self, payload: Dict[str, Any]) -> Dict[str, Any]:
"""Create a Ticket in Autotask.
@ -413,20 +435,61 @@ class AutotaskClient:
if not isinstance(payload, dict) or not payload:
raise AutotaskError("Ticket payload is empty.")
data = self._request("POST", "Tickets", json_body=payload)
# Autotask commonly returns the created object or an items list.
resp = self._request_raw("POST", "Tickets", json_body=payload)
data: Any = {}
if resp.content:
try:
data = resp.json()
except Exception:
# Some tenants return an empty body or a non-JSON body on successful POST.
data = {}
ticket_id: Optional[int] = None
# Autotask may return a lightweight create result like {"itemId": 12345}.
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"):
for key in ("itemId", "itemID", "id", "ticketId", "ticketID"):
if key in data and str(data.get(key) or "").isdigit():
ticket_id = int(data[key])
break
# Some variants wrap the created entity.
if ticket_id is None and "item" in data and isinstance(data.get("item"), dict):
item = data.get("item")
if "id" in item and str(item.get("id") or "").isdigit():
ticket_id = int(item["id"])
else:
return item
if ticket_id is None and "items" in data and isinstance(data.get("items"), list) and data.get("items"):
first = data.get("items")[0]
if isinstance(first, dict):
if "id" in first and str(first.get("id") or "").isdigit():
ticket_id = int(first["id"])
else:
return first
if "id" in data:
return data
# Fallback: return normalized first item if possible
# Location header often contains the created entity URL.
if ticket_id is None:
location = (resp.headers.get("Location") or resp.headers.get("location") or "").strip()
if location:
try:
last = location.rstrip("/").split("/")[-1]
if last.isdigit():
ticket_id = int(last)
except Exception:
ticket_id = None
# If we have an ID, fetch the full ticket object so callers can reliably access ticketNumber etc.
if ticket_id is not None:
return self.get_ticket(ticket_id)
# Last-resort fallback: normalize first item if possible.
items = self._as_items_list(data)
if items:
return items[0]
raise AutotaskError("Autotask did not return a created ticket object.")
raise AutotaskError(
f"Autotask did not return a created ticket object (HTTP {resp.status_code})."
)

View File

@ -112,6 +112,11 @@ Changes:
- Extended reference data refresh to include Ticket Statuses and updated diagnostics counters accordingly.
- Added database column for cached ticket statuses and included it in migrations for existing installations.
## v20260115-16-autotask-ticket-create-response-fix
- Fixed Autotask ticket creation handling for tenants that return a lightweight or empty POST /Tickets response.
- Added support for extracting the created ticket ID from itemId/id fields and from the Location header.
- Added a follow-up GET /Tickets/{id} to always retrieve the full created ticket object (ensuring ticketNumber/id are available).
***
## v0.1.21