| # 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")) |