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


#!/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: try PATCH shapes 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 update_bodies(author):
    return [
        ("attributes_list_string", {"attributes": [{"code": "assigned_to", "value": author}]}),
        ("attributeValues_list_string", {"attributeValues": [{"code": "assigned_to", "value": author}]}),
        ("attributes_list_user", {"attributes": [{"code": "assigned_to", "value": {"login": author}}]}),
        ("attributeValues_list_user", {"attributeValues": [{"code": "assigned_to", "value": {"login": author}}]}),
    ]


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

    attempts = []
    for name, request_body in update_bodies(author):
        status, body = tt("PATCH", f"rest/api/unit/v1/update/{TT_TICKET}", request_body)
        after = compact(get_unit()) if 200 <= status < 300 else {}
        matches = after.get("assigned_to_login") == author
        attempts.append({
            "name": name,
            "http_status": status,
            "ok": 200 <= status < 300,
            "matches": matches,
            "request_body": request_body,
            "body": body,
            "assigned_to_after": after.get("assigned_to_login", ""),
        })
        dump("s46.executor-update.attempts.json", attempts)
        print("[INFO] " + name + " http=" + str(status) + " assigned_to_after=" + after.get("assigned_to_login", "") + " matches=" + str(matches))
        if matches:
            after["executor_update_matches_author"] = True
            after["executor_update_payload_shape"] = name
            dump("s46.after.compact.json", after)
            print("[OK] executor update worked with " + name)
            return

    die("no tested PATCH shape changed assigned_to to author")


if __name__ == "__main__":
    main()