Загрузка данных


#!/usr/bin/env python3

import json
import ssl
import sys
import urllib.error
import urllib.request
from pathlib import Path


# Fill before running.
TT_BASE_URL = "https://portal.works.prod.localnet/swtr"
TT_TOKEN = ""
TT_TICKET = ""

# False: read author only. True: PATCH assigned_to to author on a noise-safe ticket.
RUN_EXECUTOR_UPDATE = False

# Use "createdBy" unless UI proves "reporter" is the business author.
AUTHOR_SOURCE = "createdBy"
VERIFY_SSL = False
TIMEOUT_SECONDS = 30


OUT = Path(__file__).resolve().parent


def die(message):
    print("[ERROR] " + message, file=sys.stderr)
    raise SystemExit(1)


def parse(text):
    if not text:
        return {}
    try:
        return json.loads(text)
    except json.JSONDecodeError:
        return text


def dump(name, value):
    path = OUT / name
    path.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    print("[OK] wrote " + str(path))


def tt(method, path, payload=None):
    data = None if payload is None else json.dumps(payload, ensure_ascii=False).encode("utf-8")
    req = urllib.request.Request(
        TT_BASE_URL.rstrip("/") + "/" + path.lstrip("/"),
        data=data,
        method=method,
        headers={
            "Authorization": "Bearer " + TT_TOKEN,
            "accept": "application/json",
            "Content-Type": "application/json",
            "Cookie": "api_swtr_as21=true",
        },
    )
    context = None if VERIFY_SSL else ssl._create_unverified_context()
    try:
        with urllib.request.urlopen(req, timeout=TIMEOUT_SECONDS, context=context) as resp:
            return resp.status, parse(resp.read().decode("utf-8", errors="replace"))
    except urllib.error.HTTPError as err:
        return err.code, parse(err.read().decode("utf-8", errors="replace"))


def get_unit():
    status, body = tt("GET", f"rest/api/unit/v2/{TT_TICKET}?validatorEnabled=false")
    if status < 200 or status >= 300:
        dump("s46.read-error.json", {"http_status": status, "body": body})
        die(f"ticket read failed: HTTP {status}")
    if not isinstance(body, dict):
        die("ticket read response is not a JSON object")
    return body


def attr(unit, code):
    for item in unit.get("attributes") or []:
        if isinstance(item, dict) and item.get("code") == code:
            return item
    selected = unit.get("selected_attributes") or {}
    if isinstance(selected, dict):
        return selected.get(code)
    return None


def login(value):
    if not isinstance(value, dict):
        return ""
    value = value.get("value") if isinstance(value.get("value"), dict) else value
    return str(value.get("login") or "")


def short_attr(item):
    if not isinstance(item, dict):
        return None
    return {
        "code": item.get("code"),
        "name": item.get("name"),
        "type": item.get("type"),
        "value": item.get("value"),
        "valueAsString": item.get("valueAsString"),
    }


def compact(unit):
    assigned_to = attr(unit, "assigned_to")
    reporter = attr(unit, "reporter")
    attrs = [short_attr(attr(unit, c)) for c in ("assigned_to", "reporter", "workflow_status")]
    return {
        "code": unit.get("code"),
        "summary": unit.get("summary"),
        "space": unit.get("space"),
        "suit": unit.get("suit"),
        "createdBy": unit.get("createdBy"),
        "updatedBy": unit.get("updatedBy"),
        "author_candidate_createdBy_login": login(unit.get("createdBy")),
        "attributes": [x for x in attrs if x],
        "assigned_to_login": login(assigned_to),
        "reporter_login": login(reporter),
    }


def pick_author(c):
    if AUTHOR_SOURCE == "createdBy":
        return c["author_candidate_createdBy_login"]
    if AUTHOR_SOURCE == "reporter":
        return c["reporter_login"]
    die('AUTHOR_SOURCE must be "createdBy" or "reporter"')


def main():
    if not TT_TOKEN or not TT_TICKET:
        die("fill TT_TOKEN and TT_TICKET at the top of the script")

    before = compact(get_unit())
    dump("s46.author.compact.json", before)

    author = pick_author(before)
    if not author:
        die("author login is empty")
    print("[INFO] author_login=" + author)
    print("[INFO] assigned_to_before=" + before["assigned_to_login"])

    if not RUN_EXECUTOR_UPDATE:
        print("[OK] read-only run complete")
        return

    request_body = {"attributes": {"assigned_to": author}}
    status, body = tt("PATCH", f"rest/api/unit/v1/update/{TT_TICKET}", request_body)
    dump("s46.executor-update.response.json", {"http_status": status, "ok": 200 <= status < 300, "request_body": request_body, "body": body})
    if status < 200 or status >= 300:
        die(f"executor update failed: HTTP {status}")

    after = compact(get_unit())
    after["executor_update_matches_author"] = after["assigned_to_login"] == author
    dump("s46.after.compact.json", after)
    print("[INFO] assigned_to_after=" + after["assigned_to_login"])
    print("[INFO] matches=" + str(after["executor_update_matches_author"]))
    if not after["executor_update_matches_author"]:
        die("after-read did not confirm assigned_to equals author")


if __name__ == "__main__":
    main()