Загрузка данных
import os
import random
import sys
import uuid
from datetime import datetime
from html import escape
from atlassian import Confluence
from bs4 import BeautifulSoup
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
CONFLUENCE_URL = "https://wiki.infotecs.int"
VERIFY_SSL = False
METADATA_PATH = sys.argv[1] if len(sys.argv) > 1 else "metadata"
# Индекс строки, после которой добавляется новая сборка.
OFFSET_ROW = 1
# Название макроса раскрывающегося блока в Confluence.
EXPAND_MACRO_TITLE = "NA"
# Для каждого продукта указываем, какие версии выводить в четвёртой колонке.
# Порядок полей здесь определяет порядок их отображения в таблице.
PRODUCT_CONFIG = {
"l2kb10": {
"versions": [
("fpga_ver", "БШМ"),
("dnsd_ver", "ДНСД"),
],
},
"l2qd10": {
"versions": [
("fpga_ver", "БШМ"),
("dnsd_ver", "ДНСД"),
("qss_protoqa", "Protoqa"),
("qss_asn_codec", "qss_asn_codec"),
],
},
"l2qd100": {
"versions": [
("fpga_ver_savona", "БШМ (Savona)"),
("fpga_ver_hem100", "БШМ (HEM100)"),
("dnsd_ver", "ДНСД"),
("qss_protoqa", "Protoqa"),
("qss_asn_codec", "qss_asn_codec"),
],
},
}
# Поддержка альтернативных названий некоторых полей.
# Первым указывается актуальное имя, последним — старое.
FIELD_ALIASES = {
"bug": ("bug", "bugs"),
"qss_protoqa": ("qss_protoqa", "protoqa_ver"),
"qss_asn_codec": ("qss_asn_codec", "qss_asn_codec_ver"),
}
# ---------------------------------------------------------------------------
# Metadata
# ---------------------------------------------------------------------------
def read_metadata(path):
"""Читает metadata в формате key=value."""
metadata = {}
with open(path, "r", encoding="utf-8") as file:
for line_number, raw_line in enumerate(file, start=1):
line = raw_line.strip()
# Пропускаем пустые строки и комментарии.
if not line or line.startswith("#"):
continue
# Разделяем только по первому '='.
# Всё остальное, включая пробелы и '=', остаётся в значении.
key, separator, value = line.partition("=")
if not separator:
raise ValueError(
f"Некорректная строка в metadata, "
f"строка {line_number}: {raw_line.rstrip()!r}. "
"Ожидается формат key=value."
)
key = key.strip()
value = value.strip()
if not key:
raise ValueError(
f"Пустое имя поля в metadata, строка {line_number}."
)
if key in metadata:
raise ValueError(
f"Поле {key!r} повторяется в metadata, "
f"строка {line_number}."
)
metadata[key] = value
return metadata
def get_value(metadata, key):
"""Возвращает непустое значение поля с учётом известных алиасов."""
aliases = FIELD_ALIASES.get(key, (key,))
for alias in aliases:
value = metadata.get(alias, "").strip()
if value:
return value
return ""
def require_value(metadata, key):
"""Возвращает обязательное поле или сообщает об ошибке."""
value = get_value(metadata, key)
if not value:
raise ValueError(
f"В metadata отсутствует обязательное поле {key!r}."
)
return value
# ---------------------------------------------------------------------------
# Confluence
# ---------------------------------------------------------------------------
def create_confluence():
"""Создаёт клиент Confluence."""
confluence = Confluence(
url=CONFLUENCE_URL,
token=os.environ["confluence_token"],
verify_ssl=VERIFY_SSL,
)
if VERIFY_SSL:
print("SSL verification is on")
else:
print("SSL verification is off")
import urllib3
urllib3.disable_warnings(
urllib3.exceptions.InsecureRequestWarning
)
return confluence
def make_user_link(confluence, username):
"""Формирует ссылку на пользователя Confluence."""
if username == "svc_l2_teamcity":
return "<strong>Teamcity dependency</strong>"
user_details = confluence.get_user_details_by_username(username)
user_key = user_details.get("userKey")
if not user_key:
raise ValueError(
f"Не удалось получить userKey пользователя {username!r}."
)
return (
'<ac:link>'
f'<ri:user ri:userkey="{escape(user_key, quote=True)}"/>'
'</ac:link>'
)
# ---------------------------------------------------------------------------
# Confluence macros
# ---------------------------------------------------------------------------
def macro_expand(title, body, macro_type="expand"):
"""Формирует макрос expand или ui-expand."""
macro_id = str(uuid.uuid4())
result = (
'<ac:structured-macro'
f' ac:macro-id="{macro_id}"'
f' ac:name="{escape(macro_type, quote=True)}"'
' ac:schema-version="1">'
)
if macro_type == "ui-expand":
result += (
'<ac:parameter ac:name="expanded">true</ac:parameter>'
)
result += (
f'<ac:parameter ac:name="title">{escape(title)}</ac:parameter>'
'<ac:rich-text-body>'
f'{body}'
'</ac:rich-text-body>'
'</ac:structured-macro>'
)
return result
def macro_tfs(item):
"""Формирует макрос ссылки на задачу TFS."""
return (
'<p>'
'<ac:structured-macro'
' ac:name="work-item-tfs"'
' ac:schema-version="2">'
f'<ac:parameter ac:name="itemID">{escape(item)}</ac:parameter>'
'<ac:parameter ac:name="schemaVersion">1</ac:parameter>'
'<ac:parameter ac:name="host">0</ac:parameter>'
'<ac:parameter ac:name="title">true</ac:parameter>'
'<ac:parameter ac:name="status">true</ac:parameter>'
'</ac:structured-macro>'
'</p>'
)
def macro_checkbox():
"""Формирует незавершённый пункт task-list."""
task_id = str(random.randint(1_000_000, 9_999_999))
return (
'<ac:task-list>'
f'<ac:task><ac:task-id>{task_id}</ac:task-id>'
'<ac:task-status>incomplete</ac:task-status>'
'</ac:task></ac:task-list>'
)
# ---------------------------------------------------------------------------
# HTML / storage format
# ---------------------------------------------------------------------------
def append_fragment(parent, html_fragment):
"""
Добавляет HTML-фрагмент в тег.
Содержимое сначала разбирается BeautifulSoup, после чего его дочерние
элементы вставляются непосредственно в родительский тег.
"""
fragment = BeautifulSoup(html_fragment, "html.parser")
for element in list(fragment.contents):
parent.append(element.extract())
def append_cell_content(cell, content):
"""Добавляет содержимое ячейки, оборачивая обычный текст в <p>."""
if not content:
return
if "<" in content:
append_fragment(cell, content)
else:
append_fragment(cell, f"<p>{escape(content)}</p>")
def make_text_paragraph(label, value):
"""Формирует абзац с выделенным значением."""
return (
f'<p>{escape(label)}: '
f'<strong>{escape(value)}</strong></p>'
)
# ---------------------------------------------------------------------------
# Build information
# ---------------------------------------------------------------------------
def make_build_info(metadata, confluence):
"""
Формирует информацию о сборке для третьей колонки.
Добавляются только непустые поля, присутствующие в metadata.
"""
parts = []
# Разработчик, поставивший сборку.
username = get_value(metadata, "user")
if username:
user_link = make_user_link(confluence, username)
parts.append(f"<p>Поставил сборку: {user_link}</p>")
# Ветка.
hw_branch = get_value(metadata, "hw_branch")
if hw_branch:
parts.append(make_text_paragraph("Из ветки", hw_branch))
# Путь выгрузки.
upload_path = get_value(metadata, "upload_path")
if upload_path:
parts.append(make_text_paragraph("Путь выгрузки", upload_path))
# Тип сборки.
#
# Если присутствуют feature и build_type, сохраняем старый формат:
# base (test-release).
# Если feature отсутствует, выводим только build_type.
feature = get_value(metadata, "feature")
build_type = get_value(metadata, "build_type")
if feature and build_type:
build_type_text = f"{feature} ({build_type})"
else:
build_type_text = feature or build_type
if build_type_text:
parts.append(
make_text_paragraph("Тип сборки", build_type_text)
)
return "".join(parts)
def make_versions(metadata, hwtarget):
"""Формирует содержимое четвёртой колонки по конфигурации продукта."""
parts = []
for field, label in PRODUCT_CONFIG[hwtarget]["versions"]:
value = get_value(metadata, field)
# Если версия не указана, не создаём для неё абзац.
if not value:
continue
parts.append(make_text_paragraph(label, value))
return "".join(parts)
def make_task_and_bug_links(metadata):
"""Формирует ссылки на задачу и баг, если указанные номера числовые."""
parts = []
task = get_value(metadata, "task")
bug = get_value(metadata, "bug")
if task.isnumeric():
parts.append(macro_tfs(task))
if bug.isnumeric():
parts.append(macro_tfs(bug))
return "".join(parts)
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
metadata = read_metadata(METADATA_PATH)
# Проверяем продукт и номер сборки.
hwtarget = require_value(metadata, "hwtarget")
hw_ver = require_value(metadata, "hw_ver")
if hwtarget not in PRODUCT_CONFIG:
supported_targets = ", ".join(PRODUCT_CONFIG)
raise ValueError(
f"Неизвестный hwtarget: {hwtarget!r}. "
f"Поддерживаемые значения: {supported_targets}."
)
# Не выгружаем сборку, предназначенную не для тестирования.
comment = get_value(metadata, "comment")
if comment == "Не тестировать":
print(
"Сборка помечена как 'Не тестировать'. "
"Пропускаем обновление таблицы в Confluence."
)
return
confluence = create_confluence()
# Для третьей колонки.
build_info = make_build_info(metadata, confluence)
info_build_link = (
f'<p>Номер сборки: <strong>{escape(hw_ver)}</strong></p>'
f'{build_info}'
)
# Для четвёртой колонки.
ver_link = make_versions(metadata, hwtarget)
# Для пятой колонки.
task_and_bug_link = make_task_and_bug_links(metadata)
# Для шестой колонки.
comment_link = (
f'<p>{escape(comment)}</p>' if comment else ""
)
# Дата сборки.
date = datetime.today().strftime("%d-%m-%Y")
# Данные новой строки таблицы.
#
# hwtarget намеренно не выводится.
# Порядок колонок соответствует существующей таблице.
row_data = [
date, # 1. Дата сборки
macro_checkbox(), # 2. Проверка разработчиком
info_build_link, # 3. Информация о сборке
ver_link, # 4. Версии компонентов
task_and_bug_link, # 5. Задачи и баги
comment_link, # 6. Примечание
"", # 7. Тестирование автостендом
macro_checkbox(), # 8. Проверка тестировщиком
"", # 9. Сценарий
"", # 10. Результат
"", # 11. Примечания
]
# Получаем страницу Confluence.
page_id = os.environ["wiki_page_thematic_id"]
wiki_page = confluence.get_page_by_id(
page_id,
expand="body.storage",
status=None,
version=None,
)
body_page = wiki_page["body"]["storage"]["value"]
main_title = wiki_page["title"]
main_body = BeautifulSoup(body_page, "html.parser")
# -----------------------------------------------------------------------
# Ищем существующую запись с этим номером сборки.
# -----------------------------------------------------------------------
find_flag = False
rows = main_body.find_all("tr")
for row in rows[OFFSET_ROW + 1:]:
cells = row.find_all("td")
if len(cells) < 3:
continue
strong_tags = cells[2].find_all("strong")
strong_values = [
tag.get_text(strip=True)
for tag in strong_tags
]
if hw_ver not in strong_values:
continue
find_flag = True
if len(cells) < 6:
raise ValueError(
f"Строка сборки {hw_ver} содержит меньше 6 ячеек."
)
# Обновляем только те данные, которые действительно есть.
# Номер сборки уже находится в существующей строке.
if build_info:
append_fragment(
cells[2],
(
"<p>"
f"{macro_expand(EXPAND_MACRO_TITLE, build_info, 'ui-expand')}"
"</p>"
),
)
if ver_link:
append_fragment(
cells[3],
(
"<p>"
f"{macro_expand(EXPAND_MACRO_TITLE, ver_link, 'ui-expand')}"
"</p>"
),
)
if task_and_bug_link:
append_fragment(cells[4], task_and_bug_link)
if comment_link:
append_fragment(cells[5], comment_link)
print(f"Сборка {hw_ver} уже существует. Обновляем запись.")
break
# -----------------------------------------------------------------------
# Если сборка не найдена, создаём новую строку.
# -----------------------------------------------------------------------
if not find_flag:
new_row = main_body.new_tag("tr")
for content in row_data:
new_cell = main_body.new_tag("td")
append_cell_content(new_cell, content)
new_row.append(new_cell)
rows = main_body.find_all("tr")
if len(rows) <= OFFSET_ROW:
raise ValueError(
"Не удалось найти строку таблицы, "
"после которой следует вставить новую сборку."
)
rows[OFFSET_ROW].insert_after(new_row)
print(f"Добавлена новая сборка {hw_ver} ({hwtarget}).")
# Сохраняем страницу.
confluence.update_page(
page_id,
main_title,
str(main_body).replace("\n", ""),
type="page",
representation="storage",
minor_edit=False,
full_width=False,
)
print("Страница Confluence обновлена.")
if __name__ == "__main__":
main()