blob: 8fa78dd8aa03eac4ff176d18cad3db9fab69250a [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Normalize Bitbucket Cloud and Data Center responses."""
from __future__ import annotations
from datetime import UTC, datetime
from typing import Any
READ_ONLY_LABELS = ["bitbucket", "read-only", "partial-change-request"]
def repository(kind: str, raw: dict[str, Any]) -> dict[str, Any]:
"""Normalize repository metadata from Bitbucket Cloud or Data Center."""
if kind == "cloud":
return {
"backend": "bitbucket-cloud",
"id": _string(raw.get("uuid") or raw.get("full_name") or raw.get("slug")),
"name": raw.get("name"),
"slug": raw.get("slug"),
"description": raw.get("description"),
"is_private": raw.get("is_private"),
"main_branch": _cloud_main_branch(raw),
"links": _cloud_links(raw),
"capabilities": {
"repository_metadata": "read",
"pull_requests": "read",
"issues": "not_implemented",
"writes": "not_implemented",
},
"raw": raw,
}
return {
"backend": "bitbucket-datacenter",
"id": _string(raw.get("id") or raw.get("slug") or raw.get("name")),
"name": raw.get("name"),
"slug": raw.get("slug"),
"description": raw.get("description"),
"is_private": _datacenter_private(raw),
"main_branch": _datacenter_main_branch(raw),
"links": _datacenter_links(raw),
"capabilities": {
"repository_metadata": "read",
"pull_requests": "read",
"issues": "not_implemented",
"writes": "not_implemented",
},
"raw": raw,
}
def pull_request_summary(kind: str, raw: dict[str, Any]) -> dict[str, Any]:
"""Normalize one pull request as a read-only change-request summary."""
if kind == "cloud":
return {
"backend": "bitbucket-cloud",
"id": _string(raw.get("id")),
"title": raw.get("title"),
"author": _cloud_user(raw.get("author")),
"state": _normalize_state(raw.get("state")),
"created": _cloud_timestamp(raw.get("created_on")),
"updated": _cloud_timestamp(raw.get("updated_on")),
"source": _cloud_branch(raw.get("source")),
"target": _cloud_branch(raw.get("destination")),
"permalink": _cloud_link(raw, "html"),
"labels": READ_ONLY_LABELS,
}
return {
"backend": "bitbucket-datacenter",
"id": _string(raw.get("id")),
"title": raw.get("title"),
"author": _datacenter_user(raw.get("author")),
"state": _normalize_state(raw.get("state")),
"created": _epoch_millis_to_iso(raw.get("createdDate")),
"updated": _epoch_millis_to_iso(raw.get("updatedDate")),
"source": _datacenter_branch(raw.get("fromRef")),
"target": _datacenter_branch(raw.get("toRef")),
"permalink": _datacenter_link(raw),
"labels": READ_ONLY_LABELS,
}
def pull_request(kind: str, raw: dict[str, Any]) -> dict[str, Any]:
"""Normalize one pull request as a read-only change-request proposal."""
summary = pull_request_summary(kind, raw)
summary["description"] = raw.get("description")
summary["mergeable"] = "unknown"
summary["checks"] = "none"
summary["diff"] = None
summary["commits"] = None
summary["raw"] = raw
return summary
def pull_request_list(kind: str, raw: dict[str, Any]) -> dict[str, Any]:
"""Normalize a Bitbucket pull-request list response."""
values = raw.get("values")
if not isinstance(values, list):
values = []
return {
"backend": "bitbucket-cloud" if kind == "cloud" else "bitbucket-datacenter",
"coverage": "read-only-partial-change-request",
"pull_requests": [pull_request_summary(kind, item) for item in values if isinstance(item, dict)],
"raw": raw,
}
def pull_request_discussion(kind: str, raw: dict[str, Any]) -> dict[str, Any]:
"""Normalize pull request discussion/comments from Bitbucket."""
values = raw.get("values")
if not isinstance(values, list):
values = []
comments: list[dict[str, Any]] = []
for item in values:
if not isinstance(item, dict):
continue
if kind == "cloud":
comments.append(_cloud_comment(item))
else:
comments.extend(_datacenter_comment_activity(item))
return {
"backend": "bitbucket-cloud" if kind == "cloud" else "bitbucket-datacenter",
"coverage": "partial-read-only",
"pull_request_id": _string(raw.get("pull_request_id")),
"comments": comments,
"participants": _participants(comments),
"unresolved_count": None,
"raw": raw,
}
def pull_request_status(kind: str, raw: dict[str, Any]) -> dict[str, Any]:
"""Normalize pull request build/status checks from Bitbucket."""
values = raw.get("values")
if not isinstance(values, list):
values = []
check_details = [
_cloud_status_check(item) if kind == "cloud" else _datacenter_status_check(item)
for item in values
if isinstance(item, dict)
]
return {
"backend": "bitbucket-cloud" if kind == "cloud" else "bitbucket-datacenter",
"coverage": "partial-read-only",
"pull_request_id": _string(raw.get("pull_request_id")),
"commit": _string(raw.get("commit")),
"state": _pull_request_state(kind, raw.get("pull_request")),
"checks": _aggregate_checks(check_details),
"mergeable": "unknown",
"check_details": check_details,
"raw": raw,
}
def pull_request_commits(kind: str, raw: dict[str, Any]) -> dict[str, Any]:
"""Normalize pull request commits from Bitbucket."""
values = raw.get("values")
if not isinstance(values, list):
values = []
commits = [
_cloud_commit(item) if kind == "cloud" else _datacenter_commit(item)
for item in values
if isinstance(item, dict)
]
return {
"backend": "bitbucket-cloud" if kind == "cloud" else "bitbucket-datacenter",
"coverage": "partial-read-only",
"pull_request_id": _string(raw.get("pull_request_id")),
"commits": commits,
"raw": raw,
}
def pull_request_diff(kind: str, raw: dict[str, Any]) -> dict[str, Any]:
"""Normalize pull request diff text from Bitbucket."""
return {
"backend": "bitbucket-cloud" if kind == "cloud" else "bitbucket-datacenter",
"coverage": "partial-read-only",
"pull_request_id": _string(raw.get("pull_request_id")),
"diff": _string(raw.get("body")) or "",
"content_type": _string(raw.get("content_type")),
"url": _string(raw.get("url")),
"raw": raw,
}
def pull_request_reviews(kind: str, raw: dict[str, Any]) -> dict[str, Any]:
"""Normalize pull request review-state activity from Bitbucket."""
pull_request_raw = raw.get("pull_request")
pull_request_data = pull_request_raw if isinstance(pull_request_raw, dict) else {}
values = raw.get("values")
if not isinstance(values, list):
values = []
review_events: list[dict[str, Any]] = []
for item in values:
if not isinstance(item, dict):
continue
event = _cloud_review_event(item) if kind == "cloud" else _datacenter_review_event(item)
if event is not None:
review_events.append(event)
reviewers = (
_cloud_reviewers(pull_request_data) if kind == "cloud" else _datacenter_reviewers(pull_request_data)
)
current_approvals = _current_review_signals(reviewers, "approved")
current_changes_requested = _current_review_signals(reviewers, "changes_requested")
latest_events = _latest_review_events(review_events)
event_approvals = [event for event in latest_events if event.get("kind") == "approval"]
event_changes_requested = [event for event in latest_events if event.get("kind") == "changes_requested"]
current_approval_authors = {item.get("author") for item in current_approvals}
current_change_authors = {item.get("author") for item in current_changes_requested}
approvals = current_approvals or [
item for item in event_approvals if item.get("author") not in current_change_authors
]
changes_requested = current_changes_requested or [
item for item in event_changes_requested if item.get("author") not in current_approval_authors
]
return {
"backend": "bitbucket-cloud" if kind == "cloud" else "bitbucket-datacenter",
"coverage": "partial-read-only",
"pull_request_id": _string(raw.get("pull_request_id")),
"review_decision": _review_decision_from_signals(
reviewers, latest_events, approvals, changes_requested
),
"reviewers": reviewers,
"approvals": approvals,
"changes_requested": changes_requested,
"review_requests": _review_requests(reviewers),
"review_events": review_events,
"raw": raw,
}
def _cloud_reviewers(raw: dict[str, Any]) -> list[dict[str, Any]]:
reviewers = raw.get("reviewers")
participants = raw.get("participants")
normalized: dict[str, dict[str, Any]] = {}
if isinstance(reviewers, list):
for item in reviewers:
if isinstance(item, dict):
reviewer = _cloud_reviewer(item, requested=True)
key = reviewer.get("user")
if isinstance(key, str):
normalized[key] = reviewer
if isinstance(participants, list):
for item in participants:
if isinstance(item, dict):
participant = _cloud_reviewer(item, requested=False)
key = participant.get("user")
if not isinstance(key, str):
continue
existing = normalized.get(key)
if existing is None:
normalized[key] = participant
elif (
existing.get("review_state") == "pending" and participant.get("review_state") != "unknown"
):
# A participants[] entry refines a requested reviewer's state,
# but only when it carries a definite signal (approved /
# changes_requested). Never downgrade a pending request to
# "unknown" — that would drop the reviewer from review_requests
# and collapse review_decision to "unknown".
normalized[key] = participant
return list(normalized.values())
def _cloud_reviewer(raw: dict[str, Any], *, requested: bool) -> dict[str, Any]:
state = _cloud_reviewer_state(raw, requested=requested)
return {
"user": _cloud_user(raw.get("user") or raw),
"approved": state == "approved",
"review_state": state,
"role": _string(raw.get("role")),
"raw": raw,
}
def _cloud_reviewer_state(raw: dict[str, Any], *, requested: bool) -> str:
if raw.get("approved") is True:
return "approved"
state = _string(raw.get("state") or raw.get("status"))
normalized = state.upper() if state is not None else ""
if normalized in {"CHANGES_REQUESTED", "NEEDS_WORK"}:
return "changes_requested"
if normalized in {"APPROVED"}:
return "approved"
if normalized in {"UNAPPROVED", "PENDING", "REVIEW_REQUESTED"}:
return "pending"
return "pending" if requested else "unknown"
def _datacenter_reviewers(raw: dict[str, Any]) -> list[dict[str, Any]]:
reviewers = raw.get("reviewers")
if not isinstance(reviewers, list):
return []
return [_datacenter_reviewer(item) for item in reviewers if isinstance(item, dict)]
def _datacenter_reviewer(raw: dict[str, Any]) -> dict[str, Any]:
review_state = _datacenter_reviewer_state(raw)
return {
"user": _datacenter_user(raw.get("user") or raw),
"approved": review_state == "approved",
"status": _string(raw.get("status")),
"review_state": review_state,
"role": _string(raw.get("role")),
"raw": raw,
}
def _datacenter_reviewer_state(raw: dict[str, Any]) -> str:
approved = raw.get("approved")
if approved is True:
return "approved"
status = _string(raw.get("status"))
normalized = status.upper() if status is not None else ""
if normalized == "APPROVED":
return "approved"
if normalized in {"NEEDS_WORK", "CHANGES_REQUESTED"}:
return "changes_requested"
if normalized in {"UNAPPROVED", "PENDING", "NOT_APPROVED"}:
return "pending"
return "unknown"
def _cloud_review_event(raw: dict[str, Any]) -> dict[str, Any] | None:
approval = raw.get("approval")
if isinstance(approval, dict):
return {
"kind": "approval",
"author": _cloud_user(approval.get("user")),
"date": _cloud_timestamp(approval.get("date")),
"raw": raw,
}
changes_requested = raw.get("changes_requested")
if isinstance(changes_requested, dict):
return {
"kind": "changes_requested",
"author": _cloud_user(changes_requested.get("user")),
"date": _cloud_timestamp(changes_requested.get("date")),
"raw": raw,
}
approval_removed = raw.get("approval_removed") or raw.get("unapproval")
if isinstance(approval_removed, dict):
return {
"kind": "approval_removed",
"author": _cloud_user(approval_removed.get("user")),
"date": _cloud_timestamp(approval_removed.get("date")),
"raw": raw,
}
update = raw.get("update")
if isinstance(update, dict):
return {
"kind": "updated",
"author": _cloud_user(update.get("author")),
"date": _cloud_timestamp(update.get("date")),
"raw": raw,
}
return None
def _datacenter_review_event(raw: dict[str, Any]) -> dict[str, Any] | None:
action = _string(raw.get("action") or raw.get("type"))
normalized_action = action.upper() if action is not None else ""
if normalized_action == "APPROVED":
return _datacenter_activity_event("approval", raw)
if normalized_action in {"UNAPPROVED", "APPROVAL_REMOVED"}:
return _datacenter_activity_event("approval_removed", raw)
if normalized_action in {"NEEDS_WORK", "CHANGES_REQUESTED"}:
return _datacenter_activity_event("changes_requested", raw)
if normalized_action in {"REVIEWED", "UPDATED", "RESCOPED"}:
return _datacenter_activity_event(normalized_action.lower(), raw)
return None
def _datacenter_activity_event(kind: str, raw: dict[str, Any]) -> dict[str, Any]:
return {
"kind": kind,
"author": _datacenter_user(raw.get("user")),
"date": _epoch_millis_to_iso(raw.get("createdDate")),
"raw": raw,
}
def _current_review_signals(
reviewers: list[dict[str, Any]],
review_state: str,
) -> list[dict[str, Any]]:
return [
{
"kind": review_state,
"author": reviewer.get("user"),
"status": reviewer.get("status"),
"raw": reviewer.get("raw"),
}
for reviewer in reviewers
if reviewer.get("review_state") == review_state
]
def _review_decision_from_signals(
reviewers: list[dict[str, Any]],
latest_events: Any,
approvals: list[dict[str, Any]],
changes_requested: list[dict[str, Any]],
) -> str:
if changes_requested:
return "changes_requested"
if approvals:
return "approved"
return _review_decision(reviewers, latest_events)
def _review_decision(
reviewers: list[dict[str, Any]],
latest_events: list[dict[str, Any]],
) -> str:
reviewer_states = {reviewer.get("review_state") for reviewer in reviewers}
if "changes_requested" in reviewer_states:
return "changes_requested"
if "approved" in reviewer_states:
return "approved"
if "pending" in reviewer_states:
return "review_required"
event_states = {event.get("kind") for event in latest_events}
if "changes_requested" in event_states:
return "changes_requested"
if "approval" in event_states:
return "approved"
if "approval_removed" in event_states:
return "review_required"
return "unknown"
def _latest_review_events(review_events: list[dict[str, Any]]) -> list[dict[str, Any]]:
latest: dict[str, tuple[str, int, dict[str, Any]]] = {}
for index, event in enumerate(review_events):
kind = event.get("kind")
if kind not in {"approval", "approval_removed", "changes_requested"}:
continue
author = event.get("author")
if not isinstance(author, str):
continue
date = _string(event.get("date")) or ""
previous = latest.get(author)
if previous is None or (date, index) >= (previous[0], previous[1]):
latest[author] = (date, index, event)
return [item[2] for item in latest.values()]
def _review_requests(reviewers: list[dict[str, Any]]) -> list[dict[str, Any]]:
return [reviewer for reviewer in reviewers if reviewer.get("review_state") == "pending"]
def _cloud_commit(raw: dict[str, Any]) -> dict[str, Any]:
return {
"hash": _string(raw.get("hash")),
"message": _string(raw.get("message")),
"author": _cloud_commit_author(raw.get("author")),
"date": _cloud_timestamp(raw.get("date")),
"links": _cloud_links(raw),
"raw": raw,
}
def _datacenter_commit(raw: dict[str, Any]) -> dict[str, Any]:
return {
"hash": _string(raw.get("id") or raw.get("displayId")),
"display_hash": _string(raw.get("displayId")),
"message": _string(raw.get("message")),
"author": _datacenter_commit_author(raw.get("author")),
"date": _epoch_millis_to_iso(raw.get("authorTimestamp") or raw.get("committerTimestamp")),
"links": _datacenter_links(raw),
"raw": raw,
}
def _cloud_commit_author(raw: object) -> str | None:
if not isinstance(raw, dict):
return None
user = raw.get("user")
if isinstance(user, dict):
display_name = _cloud_user(user)
if display_name:
return display_name
raw_author = raw.get("raw")
if isinstance(raw_author, str):
return raw_author
return None
def _datacenter_commit_author(raw: object) -> str | None:
if isinstance(raw, dict):
return _datacenter_user(raw)
if isinstance(raw, str):
return raw
return None
def _cloud_status_check(raw: dict[str, Any]) -> dict[str, Any]:
return {
"key": _string(raw.get("key")),
"name": _string(raw.get("name") or raw.get("key")),
"state": _normalize_check_state(raw.get("state")),
"url": _string(raw.get("url")),
"description": _string(raw.get("description")),
"created": _cloud_timestamp(raw.get("created_on")),
"updated": _cloud_timestamp(raw.get("updated_on")),
"raw": raw,
}
def _datacenter_status_check(raw: dict[str, Any]) -> dict[str, Any]:
return {
"key": _string(raw.get("key")),
"name": _string(raw.get("name") or raw.get("key")),
"state": _normalize_check_state(raw.get("state")),
"url": _string(raw.get("url")),
"description": _string(raw.get("description")),
"created": _epoch_millis_to_iso(raw.get("dateAdded")),
"updated": _epoch_millis_to_iso(raw.get("dateUpdated")),
"raw": raw,
}
def _aggregate_checks(check_details: list[dict[str, Any]]) -> str:
states = {check.get("state") for check in check_details}
if not states:
return "none"
if "failure" in states:
return "failing"
if "pending" in states:
return "pending"
if states == {"success"}:
return "passing"
return "pending"
def _pull_request_state(kind: str, raw: object) -> str:
if not isinstance(raw, dict):
return "unknown"
if kind == "cloud":
return _normalize_state(raw.get("state"))
return _normalize_state(raw.get("state"))
def _normalize_check_state(value: object) -> str:
raw_state = _string(value)
state = raw_state.upper() if raw_state is not None else ""
if state in {"SUCCESS", "SUCCESSFUL", "PASSED"}:
return "success"
if state in {"FAILED", "FAILURE", "ERROR"}:
return "failure"
if state in {"INPROGRESS", "IN_PROGRESS", "PENDING"}:
return "pending"
if state in {"STOPPED", "CANCELLED", "CANCELED"}:
return "cancelled"
return "unknown"
def _cloud_comment(raw: dict[str, Any]) -> dict[str, Any]:
"""Normalize one Bitbucket Cloud pull request comment."""
body = _content_text(raw.get("content"))
author = _cloud_user(raw.get("user"))
created = _cloud_timestamp(raw.get("created_on"))
updated = _cloud_timestamp(raw.get("updated_on"))
return {
"id": _string(raw.get("id")),
"author": author,
"date": created,
"created": created,
"updated": updated,
"body": body,
"kind": "comment",
"deleted": _bool_or_none(raw.get("deleted")),
"inline": _cloud_inline(raw.get("inline")),
"raw": raw,
}
def _datacenter_comment_activity(raw: dict[str, Any]) -> list[dict[str, Any]]:
"""Normalize comment-bearing Bitbucket Data Center activity, including replies."""
action = str(raw.get("action") or raw.get("type") or "").upper()
if action and action != "COMMENTED":
return []
comment = raw.get("comment")
if not isinstance(comment, dict):
return []
return _datacenter_comment_tree(comment, raw)
def _datacenter_comment_tree(
raw: dict[str, Any],
activity: dict[str, Any],
parent_id: str | None = None,
) -> list[dict[str, Any]]:
normalized = _datacenter_comment(raw, activity, parent_id)
replies: list[dict[str, Any]] = []
for reply in raw.get("comments") or []:
if isinstance(reply, dict):
replies.extend(_datacenter_comment_tree(reply, activity, normalized["id"]))
return [normalized, *replies]
def _datacenter_comment(
raw: dict[str, Any],
activity: dict[str, Any],
parent_id: str | None,
) -> dict[str, Any]:
created = _epoch_millis_to_iso(raw.get("createdDate") or activity.get("createdDate"))
updated = _epoch_millis_to_iso(raw.get("updatedDate") or activity.get("updatedDate"))
return {
"id": _string(raw.get("id") or activity.get("id")),
"parent_id": parent_id,
"author": _datacenter_user(raw.get("author") or activity.get("user")),
"date": created,
"created": created,
"updated": updated,
"body": _string(raw.get("text")),
"kind": "comment",
"deleted": _bool_or_none(raw.get("deleted")),
"inline": _datacenter_inline(raw.get("anchor")),
"raw": raw,
}
def _participants(comments: list[dict[str, Any]]) -> list[str]:
"""Return sorted unique discussion participants derived from comments."""
names: set[str] = set()
for comment in comments:
author = comment.get("author")
if isinstance(author, str):
names.add(author)
return sorted(names)
def _content_text(raw: object) -> str | None:
"""Extract Bitbucket Cloud raw comment text without truthiness fallback."""
if not isinstance(raw, dict):
return None
for key in ("raw", "markup", "html"):
if key in raw:
return _string(raw.get(key))
return None
def _bool_or_none(value: object) -> bool | None:
"""Normalize optional booleans."""
return value if isinstance(value, bool) else None
def _cloud_inline(raw: object) -> dict[str, Any] | None:
"""Normalize Bitbucket Cloud inline comment location."""
if not isinstance(raw, dict):
return None
inline: dict[str, Any] = {}
if isinstance(raw.get("path"), str):
inline["path"] = raw["path"]
if isinstance(raw.get("from"), int):
inline["from_line"] = raw["from"]
if isinstance(raw.get("to"), int):
inline["to_line"] = raw["to"]
return inline or None
def _datacenter_inline(raw: object) -> dict[str, Any] | None:
"""Normalize Bitbucket Data Center inline comment location."""
if not isinstance(raw, dict):
return None
inline: dict[str, Any] = {}
if isinstance(raw.get("path"), str):
inline["path"] = raw["path"]
if isinstance(raw.get("from"), int):
inline["from_line"] = raw["from"]
if isinstance(raw.get("to"), int):
inline["to_line"] = raw["to"]
if "to_line" not in inline and isinstance(raw.get("line"), int):
inline["to_line"] = raw["line"]
return inline or None
def _string(value: object) -> str | None:
"""Convert a value to string while preserving missing values as None."""
if value is None:
return None
return str(value)
def _normalize_state(value: object) -> str:
"""Normalize backend-specific PR states to change-request lifecycle words."""
state = str(value or "").lower()
if state in {"open", "opened"}:
return "open"
if state in {"merged", "fulfilled"}:
return "merged"
if state in {"declined", "superseded"}:
return "declined"
return state or "unknown"
def _cloud_timestamp(value: object) -> str | None:
"""Return a Cloud timestamp string when present."""
return _string(value)
def _epoch_millis_to_iso(value: object) -> str | None:
"""Convert Bitbucket Data Center epoch milliseconds to UTC ISO-8601."""
if isinstance(value, int | float):
return datetime.fromtimestamp(value / 1000, tz=UTC).isoformat().replace("+00:00", "Z")
return _string(value)
def _cloud_main_branch(raw: dict[str, Any]) -> str | None:
mainbranch = raw.get("mainbranch")
if isinstance(mainbranch, dict):
value = mainbranch.get("name")
return _string(value)
return _string(mainbranch)
def _cloud_links(raw: dict[str, Any]) -> dict[str, str]:
links = raw.get("links")
if not isinstance(links, dict):
return {}
normalized: dict[str, str] = {}
for name, value in links.items():
if isinstance(value, dict) and isinstance(value.get("href"), str):
normalized[name] = value["href"]
return normalized
def _cloud_link(raw: dict[str, Any], name: str) -> str | None:
links = _cloud_links(raw)
return links.get(name)
def _cloud_user(raw: object) -> str | None:
if not isinstance(raw, dict):
return None
return _string(raw.get("display_name") or raw.get("nickname") or raw.get("username") or raw.get("uuid"))
def _cloud_branch(raw: object) -> str | None:
if not isinstance(raw, dict):
return None
branch = raw.get("branch")
if isinstance(branch, dict):
return _string(branch.get("name"))
return None
def _datacenter_private(raw: dict[str, Any]) -> bool | None:
public = raw.get("public")
if isinstance(public, bool):
return not public
return None
def _datacenter_main_branch(raw: dict[str, Any]) -> str | None:
branch = raw.get("defaultBranch")
if isinstance(branch, dict):
return _string(branch.get("displayId") or branch.get("id"))
return _string(branch)
def _datacenter_links(raw: dict[str, Any]) -> dict[str, str]:
links = raw.get("links")
if not isinstance(links, dict):
return {}
normalized: dict[str, str] = {}
for name, value in links.items():
if isinstance(value, list) and value:
first = value[0]
if isinstance(first, dict) and isinstance(first.get("href"), str):
normalized[name] = first["href"]
return normalized
def _datacenter_link(raw: dict[str, Any]) -> str | None:
return _datacenter_links(raw).get("self")
def _datacenter_user(raw: object) -> str | None:
if not isinstance(raw, dict):
return None
user = raw.get("user")
if isinstance(user, dict):
return _string(user.get("displayName") or user.get("name") or user.get("emailAddress"))
return _string(raw.get("displayName") or raw.get("name") or raw.get("emailAddress"))
def _datacenter_branch(raw: object) -> str | None:
if not isinstance(raw, dict):
return None
return _string(raw.get("displayId") or raw.get("id"))