Загрузка данных
#!/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()