-
Notifications
You must be signed in to change notification settings - Fork 25
Expand file tree
/
Copy pathjql.py
More file actions
110 lines (85 loc) · 3.3 KB
/
Copy pathjql.py
File metadata and controls
110 lines (85 loc) · 3.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
"""Parse JQL templates from references/jql-release.md.
Reads the markdown file at runtime so the CLI and agent share one source of truth.
Supports placeholder rendering ({{RELEASE_VERSION}}, {{ISSUE_TYPE}}) and
URL-encoded Jira search links.
"""
from __future__ import annotations
import re
from pathlib import Path
from urllib.parse import quote
JIRA_SEARCH_BASE = "https://issues.redhat.com/issues/?jql="
_REFERENCES_DIR = Path(__file__).resolve().parent.parent / "references"
_JQL_FILE = _REFERENCES_DIR / "jql-release.md"
_TEMPLATE_CACHE: dict[str, str] | None = None
def _parse_jql_file(path: Path | None = None) -> dict[str, str]:
"""Parse ## headings and ```jql code blocks from jql-release.md."""
path = path or _JQL_FILE
text = path.read_text()
templates: dict[str, str] = {}
current_name: str | None = None
jql_lines: list[str] | None = None
for line in text.splitlines():
heading = re.match(r"^##\s+(\S+)", line)
if heading:
current_name = heading.group(1)
continue
if current_name and line.strip() == "```jql":
jql_lines = []
continue
if current_name and jql_lines is not None and line.strip() == "```":
templates[current_name] = " ".join(jql_lines).strip()
current_name = None
jql_lines = None
continue
if jql_lines is not None:
jql_lines.append(line.strip())
return templates
def load_templates(path: Path | None = None) -> dict[str, str]:
"""Load and cache JQL templates from jql-release.md."""
global _TEMPLATE_CACHE
if path is not None:
return _parse_jql_file(path)
if _TEMPLATE_CACHE is None:
_TEMPLATE_CACHE = _parse_jql_file()
return _TEMPLATE_CACHE
def get_template(name: str, path: Path | None = None) -> str:
"""Get a single JQL template by name. Raises KeyError if not found."""
templates = load_templates(path)
if name not in templates:
available = ", ".join(sorted(templates))
raise KeyError(f"Unknown JQL template '{name}'. Available: {available}")
return templates[name]
def render(
name: str,
*,
version: str | None = None,
issue_type: str | None = None,
cloud_id: str | None = None,
path: Path | None = None,
) -> str:
"""Render a JQL template with placeholder substitution."""
jql = get_template(name, path)
if version is not None:
jql = jql.replace("{{RELEASE_VERSION}}", version)
if issue_type is not None:
jql = jql.replace("{{ISSUE_TYPE}}", issue_type)
if cloud_id is not None:
jql = jql.replace("{{CLOUD_ID}}", cloud_id)
return jql
def jira_url(jql: str) -> str:
"""Build a Jira search URL from a JQL string."""
return JIRA_SEARCH_BASE + quote(jql, safe="")
def render_with_url(
name: str,
*,
version: str | None = None,
issue_type: str | None = None,
cloud_id: str | None = None,
path: Path | None = None,
) -> tuple[str, str]:
"""Render a JQL template and return (jql, jira_url)."""
jql = render(name, version=version, issue_type=issue_type, cloud_id=cloud_id, path=path)
return jql, jira_url(jql)
def list_templates(path: Path | None = None) -> list[str]:
"""Return sorted list of available template names."""
return sorted(load_templates(path))