aerospike-async 0.3.0a18__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aerospike_async-0.3.0a18/.github/scripts/generate_release_notes.py +376 -0
- aerospike_async-0.3.0a18/.github/workflows/build-test.yml +530 -0
- aerospike_async-0.3.0a18/.github/workflows/release.yml +165 -0
- aerospike_async-0.3.0a18/.gitignore +86 -0
- aerospike_async-0.3.0a18/.pre-commit-config.yaml +10 -0
- aerospike_async-0.3.0a18/Cargo.lock +1645 -0
- aerospike_async-0.3.0a18/Cargo.toml +42 -0
- aerospike_async-0.3.0a18/LICENSE +220 -0
- aerospike_async-0.3.0a18/Makefile +85 -0
- aerospike_async-0.3.0a18/PKG-INFO +9 -0
- aerospike_async-0.3.0a18/README.md +332 -0
- aerospike_async-0.3.0a18/aerospike.env.example +44 -0
- aerospike_async-0.3.0a18/benchmarks/__init__.py +4 -0
- aerospike_async-0.3.0a18/benchmarks/_env.py +59 -0
- aerospike_async-0.3.0a18/benchmarks/benchmark.py +386 -0
- aerospike_async-0.3.0a18/bin/get-version +37 -0
- aerospike_async-0.3.0a18/pyproject.toml +28 -0
- aerospike_async-0.3.0a18/python/aerospike_async/__init__.py +47 -0
- aerospike_async-0.3.0a18/python/aerospike_async/__init__.pyi +4 -0
- aerospike_async-0.3.0a18/python/aerospike_async/_aerospike_async_native.pyi +3503 -0
- aerospike_async-0.3.0a18/python/aerospike_async/exceptions/__init__.py +109 -0
- aerospike_async-0.3.0a18/python/aerospike_async/exceptions/__init__.pyi +161 -0
- aerospike_async-0.3.0a18/python/benchmarks/README.md +42 -0
- aerospike_async-0.3.0a18/python/benchmarks/benchmarks.py +54 -0
- aerospike_async-0.3.0a18/python/clean_caches.sh +18 -0
- aerospike_async-0.3.0a18/python/conftest.py +126 -0
- aerospike_async-0.3.0a18/python/examples/README.md +142 -0
- aerospike_async-0.3.0a18/python/examples/async_demo.py +320 -0
- aerospike_async-0.3.0a18/python/examples/basic_examples.py +107 -0
- aerospike_async-0.3.0a18/python/examples/create_index.py +255 -0
- aerospike_async-0.3.0a18/python/examples/create_index_simple.py +151 -0
- aerospike_async-0.3.0a18/python/examples/geo_query_bug_demo.py +296 -0
- aerospike_async-0.3.0a18/python/examples/geospatial.py +235 -0
- aerospike_async-0.3.0a18/python/examples/privilege.py +275 -0
- aerospike_async-0.3.0a18/python/examples/privilege_simple.py +145 -0
- aerospike_async-0.3.0a18/python/examples/role_management.py +312 -0
- aerospike_async-0.3.0a18/python/examples/statement_simple.py +232 -0
- aerospike_async-0.3.0a18/python/examples/tls_example.py +178 -0
- aerospike_async-0.3.0a18/python/examples/user_management.py +248 -0
- aerospike_async-0.3.0a18/python/postprocess_stubs.py +1973 -0
- aerospike_async-0.3.0a18/python/tests/__init__.py +0 -0
- aerospike_async-0.3.0a18/python/tests/integration/__init__.py +0 -0
- aerospike_async-0.3.0a18/python/tests/integration/add_test.py +48 -0
- aerospike_async-0.3.0a18/python/tests/integration/append_test.py +43 -0
- aerospike_async-0.3.0a18/python/tests/integration/batch_test.py +1021 -0
- aerospike_async-0.3.0a18/python/tests/integration/bit_exp_test.py +482 -0
- aerospike_async-0.3.0a18/python/tests/integration/cdt_ordering_test.py +456 -0
- aerospike_async-0.3.0a18/python/tests/integration/cdt_path_test.py +1065 -0
- aerospike_async-0.3.0a18/python/tests/integration/client_test.py +70 -0
- aerospike_async-0.3.0a18/python/tests/integration/conftest.py +63 -0
- aerospike_async-0.3.0a18/python/tests/integration/create_index_expression_test.py +187 -0
- aerospike_async-0.3.0a18/python/tests/integration/delete_bin_test.py +182 -0
- aerospike_async-0.3.0a18/python/tests/integration/delete_test.py +47 -0
- aerospike_async-0.3.0a18/python/tests/integration/exists_test.py +82 -0
- aerospike_async-0.3.0a18/python/tests/integration/exp_operation_test.py +251 -0
- aerospike_async-0.3.0a18/python/tests/integration/expiration_test.py +211 -0
- aerospike_async-0.3.0a18/python/tests/integration/filter_expr_test.py +205 -0
- aerospike_async-0.3.0a18/python/tests/integration/fixtures.py +106 -0
- aerospike_async-0.3.0a18/python/tests/integration/generation_test.py +233 -0
- aerospike_async-0.3.0a18/python/tests/integration/geo_query_test.py +142 -0
- aerospike_async-0.3.0a18/python/tests/integration/get_bins_test.py +414 -0
- aerospike_async-0.3.0a18/python/tests/integration/get_node_test.py +325 -0
- aerospike_async-0.3.0a18/python/tests/integration/get_test.py +89 -0
- aerospike_async-0.3.0a18/python/tests/integration/hll_exp_test.py +227 -0
- aerospike_async-0.3.0a18/python/tests/integration/index_test.py +191 -0
- aerospike_async-0.3.0a18/python/tests/integration/info_test.py +126 -0
- aerospike_async-0.3.0a18/python/tests/integration/list_exp_test.py +196 -0
- aerospike_async-0.3.0a18/python/tests/integration/map_exp_test.py +87 -0
- aerospike_async-0.3.0a18/python/tests/integration/mrt_test.py +265 -0
- aerospike_async-0.3.0a18/python/tests/integration/operate_bit_test.py +1210 -0
- aerospike_async-0.3.0a18/python/tests/integration/operate_hll_test.py +584 -0
- aerospike_async-0.3.0a18/python/tests/integration/operate_list_test.py +1566 -0
- aerospike_async-0.3.0a18/python/tests/integration/operate_map_test.py +1971 -0
- aerospike_async-0.3.0a18/python/tests/integration/operate_test.py +413 -0
- aerospike_async-0.3.0a18/python/tests/integration/partition_filter_test.py +468 -0
- aerospike_async-0.3.0a18/python/tests/integration/prepend_test.py +52 -0
- aerospike_async-0.3.0a18/python/tests/integration/put_test.py +359 -0
- aerospike_async-0.3.0a18/python/tests/integration/query_aggregate_test.py +196 -0
- aerospike_async-0.3.0a18/python/tests/integration/query_background_test.py +164 -0
- aerospike_async-0.3.0a18/python/tests/integration/query_test.py +257 -0
- aerospike_async-0.3.0a18/python/tests/integration/record_exists_action_test.py +265 -0
- aerospike_async-0.3.0a18/python/tests/integration/security_test.py +697 -0
- aerospike_async-0.3.0a18/python/tests/integration/set_xdr_filter_test.py +124 -0
- aerospike_async-0.3.0a18/python/tests/integration/special_value_test.py +245 -0
- aerospike_async-0.3.0a18/python/tests/integration/timeout_test.py +113 -0
- aerospike_async-0.3.0a18/python/tests/integration/tls_test.py +220 -0
- aerospike_async-0.3.0a18/python/tests/integration/touch_test.py +43 -0
- aerospike_async-0.3.0a18/python/tests/integration/truncate_test.py +48 -0
- aerospike_async-0.3.0a18/python/tests/integration/udf/record_example.lua +91 -0
- aerospike_async-0.3.0a18/python/tests/integration/udf/sleep_example.lua +17 -0
- aerospike_async-0.3.0a18/python/tests/integration/udf/sum_example.lua +17 -0
- aerospike_async-0.3.0a18/python/tests/integration/udf_batch_test.py +176 -0
- aerospike_async-0.3.0a18/python/tests/integration/udf_execute_test.py +243 -0
- aerospike_async-0.3.0a18/python/tests/integration/udf_register_test.py +141 -0
- aerospike_async-0.3.0a18/python/tests/integration/udf_remove_test.py +110 -0
- aerospike_async-0.3.0a18/python/tests/unit/__init__.py +0 -0
- aerospike_async-0.3.0a18/python/tests/unit/batch_policy_test.py +282 -0
- aerospike_async-0.3.0a18/python/tests/unit/cdt_path_types_test.py +240 -0
- aerospike_async-0.3.0a18/python/tests/unit/cdt_policy_test.py +221 -0
- aerospike_async-0.3.0a18/python/tests/unit/client_error_test.py +177 -0
- aerospike_async-0.3.0a18/python/tests/unit/client_policy_test.py +78 -0
- aerospike_async-0.3.0a18/python/tests/unit/ctx_test.py +97 -0
- aerospike_async-0.3.0a18/python/tests/unit/enum_test.py +234 -0
- aerospike_async-0.3.0a18/python/tests/unit/exception_test.py +224 -0
- aerospike_async-0.3.0a18/python/tests/unit/exp_operation_test.py +65 -0
- aerospike_async-0.3.0a18/python/tests/unit/filter_expr_test.py +272 -0
- aerospike_async-0.3.0a18/python/tests/unit/filter_test.py +132 -0
- aerospike_async-0.3.0a18/python/tests/unit/flag_input_uniformity_test.py +389 -0
- aerospike_async-0.3.0a18/python/tests/unit/key_test.py +251 -0
- aerospike_async-0.3.0a18/python/tests/unit/mrt_test.py +180 -0
- aerospike_async-0.3.0a18/python/tests/unit/operation_test.py +395 -0
- aerospike_async-0.3.0a18/python/tests/unit/partition_filter_test.py +155 -0
- aerospike_async-0.3.0a18/python/tests/unit/partition_status_test.py +258 -0
- aerospike_async-0.3.0a18/python/tests/unit/policies_test.py +760 -0
- aerospike_async-0.3.0a18/python/tests/unit/query_test.py +60 -0
- aerospike_async-0.3.0a18/python/tests/unit/return_type_test.py +286 -0
- aerospike_async-0.3.0a18/python/tests/unit/tls_test.py +121 -0
- aerospike_async-0.3.0a18/python/tests/unit/user_test.py +116 -0
- aerospike_async-0.3.0a18/python/tests/unit/value_test.py +551 -0
- aerospike_async-0.3.0a18/requirements.txt +4 -0
- aerospike_async-0.3.0a18/src/bin/stub_gen.rs +62 -0
- aerospike_async-0.3.0a18/src/cdt.rs +1710 -0
- aerospike_async-0.3.0a18/src/cluster.rs +487 -0
- aerospike_async-0.3.0a18/src/completion.rs +163 -0
- aerospike_async-0.3.0a18/src/enums.rs +1157 -0
- aerospike_async-0.3.0a18/src/errors.rs +181 -0
- aerospike_async-0.3.0a18/src/expressions.rs +3151 -0
- aerospike_async-0.3.0a18/src/filter.rs +877 -0
- aerospike_async-0.3.0a18/src/lib.rs +2150 -0
- aerospike_async-0.3.0a18/src/operations.rs +2970 -0
- aerospike_async-0.3.0a18/src/policies.rs +2045 -0
- aerospike_async-0.3.0a18/src/record.rs +1338 -0
- aerospike_async-0.3.0a18/src/tasks.rs +321 -0
- aerospike_async-0.3.0a18/src/tls.rs +143 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Copyright 2023-2026 Aerospike, Inc.
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
+
#
|
|
8
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
# See the License for the specific language governing permissions and
|
|
12
|
+
# limitations under the License.
|
|
13
|
+
|
|
14
|
+
"""
|
|
15
|
+
Transform GitHub-generated release notes into a custom format:
|
|
16
|
+
- Release date at top
|
|
17
|
+
- Sections: Breaking Changes, New Features, Improvements, Bug Fixes, Security (omitted if empty)
|
|
18
|
+
- Style normalization: sentence case start, trailing period, expanded contractions
|
|
19
|
+
- JIRA/ticket identifiers (e.g. [CLIENT-1234] or CLIENT-1234:) moved to the end of each line (after PR link)
|
|
20
|
+
- PR links preserved as ([#N](url))
|
|
21
|
+
- Internal-only items (CI, tests-only, etc.) filtered out
|
|
22
|
+
- Full Changelog link at bottom
|
|
23
|
+
|
|
24
|
+
Reads raw markdown from stdin; writes formatted markdown to stdout.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import argparse
|
|
30
|
+
import re
|
|
31
|
+
import sys
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
TICKET_RE = re.compile(r"\[([A-Z][A-Z0-9]*-[0-9]+)\]")
|
|
35
|
+
COLON_TICKET_PREFIX_RE = re.compile(
|
|
36
|
+
r"^([A-Z][A-Z0-9]*-[0-9]+)\s*:\s*",
|
|
37
|
+
)
|
|
38
|
+
PR_SUFFIX_RE = re.compile(
|
|
39
|
+
r"\s+by\s+@[\w-]+\s+in\s+(https?://\S+)", re.IGNORECASE
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Longer phrases first where relevant; word-boundary case-insensitive replacement.
|
|
43
|
+
_CONTRACTION_PAIRS: tuple[tuple[str, str], ...] = (
|
|
44
|
+
(r"shouldn't\b", "should not"),
|
|
45
|
+
(r"couldn't\b", "could not"),
|
|
46
|
+
(r"wouldn't\b", "would not"),
|
|
47
|
+
(r"doesn't\b", "does not"),
|
|
48
|
+
(r"haven't\b", "have not"),
|
|
49
|
+
(r"hasn't\b", "has not"),
|
|
50
|
+
(r"weren't\b", "were not"),
|
|
51
|
+
(r"wasn't\b", "was not"),
|
|
52
|
+
(r"aren't\b", "are not"),
|
|
53
|
+
(r"isn't\b", "is not"),
|
|
54
|
+
(r"won't\b", "will not"),
|
|
55
|
+
(r"can't\b", "cannot"),
|
|
56
|
+
(r"don't\b", "do not"),
|
|
57
|
+
(r"there's\b", "there is"),
|
|
58
|
+
(r"that's\b", "that is"),
|
|
59
|
+
(r"it's\b", "it is"),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
INTERNAL_PATTERNS: tuple[re.Pattern[str], ...] = (
|
|
63
|
+
re.compile(r"\bbump\b.*\btest\b", re.IGNORECASE),
|
|
64
|
+
re.compile(r"\bskip\b.*\btest\b", re.IGNORECASE),
|
|
65
|
+
re.compile(r"\bstub\s+pipeline\b", re.IGNORECASE),
|
|
66
|
+
re.compile(r"\brefactor\b", re.IGNORECASE),
|
|
67
|
+
re.compile(r"\bversion\s+bump\b", re.IGNORECASE),
|
|
68
|
+
re.compile(r"\bCI\b", re.IGNORECASE),
|
|
69
|
+
re.compile(r"\bpipeline\b", re.IGNORECASE),
|
|
70
|
+
re.compile(r"\btest(?:s)?\s+(?:only|fix|update|cleanup)\b", re.IGNORECASE),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def normalize_summary(desc: str) -> str:
|
|
75
|
+
"""Capitalize first letter, ensure trailing sentence end, expand common contractions."""
|
|
76
|
+
s = desc.strip()
|
|
77
|
+
for pattern, repl in _CONTRACTION_PAIRS:
|
|
78
|
+
s = re.sub(pattern, repl, s, flags=re.IGNORECASE)
|
|
79
|
+
s = s.strip()
|
|
80
|
+
if not s:
|
|
81
|
+
return s
|
|
82
|
+
for i, c in enumerate(s):
|
|
83
|
+
if c.isalpha():
|
|
84
|
+
s = s[:i] + c.upper() + s[i + 1 :]
|
|
85
|
+
break
|
|
86
|
+
if s[-1] not in ".!?":
|
|
87
|
+
s = f"{s}."
|
|
88
|
+
return s
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def is_internal(desc: str) -> bool:
|
|
92
|
+
"""True if the item should be omitted from user-facing release notes."""
|
|
93
|
+
for pat in INTERNAL_PATTERNS:
|
|
94
|
+
if pat.search(desc):
|
|
95
|
+
return True
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def is_bug_fix(title: str) -> bool:
|
|
100
|
+
t = title.strip().lower()
|
|
101
|
+
return (
|
|
102
|
+
t.startswith("fix ")
|
|
103
|
+
or t.startswith("fixes ")
|
|
104
|
+
or t.startswith("bug")
|
|
105
|
+
or t.startswith("fix:")
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def is_security(desc: str) -> bool:
|
|
110
|
+
d = desc.lower()
|
|
111
|
+
return "cve-" in d or "security" in d or "vulnerability" in d
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def is_breaking(desc: str) -> bool:
|
|
115
|
+
d = desc.lower()
|
|
116
|
+
return (
|
|
117
|
+
"breaking change" in d
|
|
118
|
+
or bool(re.search(r"\bbreaking\b", d))
|
|
119
|
+
or "drop support" in d
|
|
120
|
+
or "remove deprecated" in d
|
|
121
|
+
or "incompatible" in d
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def is_new_feature(desc: str) -> bool:
|
|
126
|
+
if is_bug_fix(desc):
|
|
127
|
+
return False
|
|
128
|
+
d = desc.strip().lower()
|
|
129
|
+
return (
|
|
130
|
+
d.startswith("add ")
|
|
131
|
+
or d.startswith("new ")
|
|
132
|
+
or d.startswith("implement ")
|
|
133
|
+
or d.startswith("introduce ")
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
_BREAKING_PREFIX_RE = re.compile(
|
|
138
|
+
r"^(?:breaking\s*(?:change\s*)?[:=-]+\s*|breaking\s+)", re.IGNORECASE
|
|
139
|
+
)
|
|
140
|
+
_SECURITY_PREFIX_RE = re.compile(
|
|
141
|
+
r"^(?:security\s*[:=-]+\s*)", re.IGNORECASE
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def strip_section_prefix(desc: str, section: str) -> str:
|
|
146
|
+
"""Remove redundant leading keywords that duplicate the section header."""
|
|
147
|
+
if section == "breaking":
|
|
148
|
+
return _BREAKING_PREFIX_RE.sub("", desc).strip()
|
|
149
|
+
if section == "security":
|
|
150
|
+
return _SECURITY_PREFIX_RE.sub("", desc).strip()
|
|
151
|
+
return desc
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def classify_section(desc: str) -> str:
|
|
155
|
+
"""Return bucket key: breaking, feature, improvement, bug, security."""
|
|
156
|
+
if is_security(desc):
|
|
157
|
+
return "security"
|
|
158
|
+
if is_breaking(desc):
|
|
159
|
+
return "breaking"
|
|
160
|
+
if is_bug_fix(desc):
|
|
161
|
+
return "bug"
|
|
162
|
+
if is_new_feature(desc):
|
|
163
|
+
return "feature"
|
|
164
|
+
return "improvement"
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def warn_if_compound_summary(desc: str) -> None:
|
|
168
|
+
if " + " in desc:
|
|
169
|
+
print(
|
|
170
|
+
f'WARNING: Compound summary detected, consider splitting: "{desc}"',
|
|
171
|
+
file=sys.stderr,
|
|
172
|
+
)
|
|
173
|
+
return
|
|
174
|
+
if desc.count(",") > 2:
|
|
175
|
+
print(
|
|
176
|
+
f'WARNING: Compound summary detected, consider splitting: "{desc}"',
|
|
177
|
+
file=sys.stderr,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def split_description_and_tickets(text: str) -> tuple[str, list[str]]:
|
|
182
|
+
"""Strip ticket brackets from text; return (clean description, ticket keys)."""
|
|
183
|
+
tickets = TICKET_RE.findall(text)
|
|
184
|
+
rest = TICKET_RE.sub("", text).strip()
|
|
185
|
+
rest = re.sub(r" +", " ", rest).strip()
|
|
186
|
+
return rest, tickets
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def extract_leading_colon_ticket(text: str) -> tuple[str, list[str]]:
|
|
190
|
+
"""If text starts with CLIENT-1234:, strip it and return extra ticket keys."""
|
|
191
|
+
m = COLON_TICKET_PREFIX_RE.match(text)
|
|
192
|
+
if not m:
|
|
193
|
+
return text, []
|
|
194
|
+
return text[m.end() :].strip(), [m.group(1)]
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def pr_link_markdown(url: str) -> str:
|
|
198
|
+
m = re.search(r"/pull/(\d+)", url, re.IGNORECASE)
|
|
199
|
+
if m:
|
|
200
|
+
return f"([#{m.group(1)}]({url}))"
|
|
201
|
+
return f"([pull]({url}))"
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def parse_bullet(line: str) -> tuple[str, str] | None:
|
|
205
|
+
"""
|
|
206
|
+
Parse a line like '* Fix something by @user in https://.../pull/99'.
|
|
207
|
+
|
|
208
|
+
Returns (formatted_bullet, description_for_classification) or None.
|
|
209
|
+
"""
|
|
210
|
+
line = line.strip()
|
|
211
|
+
if not line.startswith("* ") and not line.startswith("- "):
|
|
212
|
+
return None
|
|
213
|
+
text = line[2:].strip()
|
|
214
|
+
pr_url = ""
|
|
215
|
+
by_match = PR_SUFFIX_RE.search(text)
|
|
216
|
+
if by_match:
|
|
217
|
+
pr_url = by_match.group(1).rstrip(").,;")
|
|
218
|
+
text = text[: by_match.start()].strip()
|
|
219
|
+
if not text:
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
text, colon_tickets = extract_leading_colon_ticket(text)
|
|
223
|
+
desc, bracket_tickets = split_description_and_tickets(text)
|
|
224
|
+
tickets = colon_tickets + bracket_tickets
|
|
225
|
+
|
|
226
|
+
if not desc:
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
warn_if_compound_summary(desc)
|
|
230
|
+
|
|
231
|
+
if is_internal(desc):
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
desc_norm = normalize_summary(desc)
|
|
235
|
+
if not desc_norm:
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
parts = [desc_norm]
|
|
239
|
+
if pr_url:
|
|
240
|
+
parts.append(pr_link_markdown(pr_url))
|
|
241
|
+
if tickets:
|
|
242
|
+
parts.append(f"[{tickets[0]}]")
|
|
243
|
+
return " ".join(parts), desc_norm
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def run_self_tests() -> None:
|
|
247
|
+
# normalize_summary: capitalize, period, contractions
|
|
248
|
+
assert normalize_summary("fix the bug") == "Fix the bug."
|
|
249
|
+
assert normalize_summary("already done.") == "Already done."
|
|
250
|
+
assert normalize_summary("doesn't work") == "Does not work."
|
|
251
|
+
assert normalize_summary("don't panic!") == "Do not panic!"
|
|
252
|
+
|
|
253
|
+
# is_internal: broadened patterns
|
|
254
|
+
assert is_internal("Bump test dependencies")
|
|
255
|
+
assert is_internal("skip test on windows")
|
|
256
|
+
assert is_internal("Bump timeout on large put test")
|
|
257
|
+
assert is_internal("Skip timeout test in case the timeout does not happen")
|
|
258
|
+
assert is_internal("Refactor lib.rs - split into submodules")
|
|
259
|
+
assert is_internal("Tests only fix for flaky suite")
|
|
260
|
+
assert is_internal("test cleanup for CI")
|
|
261
|
+
assert not is_internal("Fix query timeout for users")
|
|
262
|
+
assert not is_internal("Add batch API support")
|
|
263
|
+
|
|
264
|
+
# extract_leading_colon_ticket: Jira colon format
|
|
265
|
+
t2, k2 = extract_leading_colon_ticket("CLIENT-99: hello [CLIENT-100]")
|
|
266
|
+
desc2, bracket_keys = split_description_and_tickets(t2)
|
|
267
|
+
assert desc2 == "hello" and k2 == ["CLIENT-99"] and bracket_keys == ["CLIENT-100"]
|
|
268
|
+
t, keys = extract_leading_colon_ticket("CLIENT-99: hello world")
|
|
269
|
+
assert t == "hello world" and keys == ["CLIENT-99"]
|
|
270
|
+
|
|
271
|
+
# classify_section
|
|
272
|
+
assert classify_section("Security patch for CVE-2024-1") == "security"
|
|
273
|
+
assert classify_section("Breaking change: drop Python 3.8") == "breaking"
|
|
274
|
+
assert classify_section("Fix null pointer in client") == "bug"
|
|
275
|
+
assert classify_section("Add batch API support") == "feature"
|
|
276
|
+
assert classify_section("Polish error messages") == "improvement"
|
|
277
|
+
|
|
278
|
+
# strip_section_prefix: remove redundant keywords
|
|
279
|
+
assert strip_section_prefix("BREAKING drop Python 3.8.", "breaking") == "drop Python 3.8."
|
|
280
|
+
assert strip_section_prefix("Breaking change: drop Python 3.8.", "breaking") == "drop Python 3.8."
|
|
281
|
+
assert strip_section_prefix("Security patch for CVE-2024-1.", "security") == "Security patch for CVE-2024-1."
|
|
282
|
+
assert strip_section_prefix("Security: fix for CVE-2024-1.", "security") == "fix for CVE-2024-1."
|
|
283
|
+
assert strip_section_prefix("Add batch API support.", "feature") == "Add batch API support."
|
|
284
|
+
|
|
285
|
+
# parse_bullet: bracket ticket preserved at end
|
|
286
|
+
out = parse_bullet("* fix foo by @x in https://github.com/o/r/pull/1")
|
|
287
|
+
assert out is not None
|
|
288
|
+
assert out[0].startswith("Fix foo.")
|
|
289
|
+
|
|
290
|
+
# parse_bullet: internal items filtered
|
|
291
|
+
assert parse_bullet("* Bump timeout on large put test by @x in https://github.com/o/r/pull/1") is None
|
|
292
|
+
assert parse_bullet("* Refactor lib.rs by @x in https://github.com/o/r/pull/2") is None
|
|
293
|
+
|
|
294
|
+
print("generate_release_notes self-tests passed.", file=sys.stderr)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def main() -> None:
|
|
298
|
+
parser = argparse.ArgumentParser(description="Format release notes.")
|
|
299
|
+
parser.add_argument("--date", default="", help="Release date (e.g. 'March 17, 2026')")
|
|
300
|
+
parser.add_argument(
|
|
301
|
+
"--changelog-url",
|
|
302
|
+
default="",
|
|
303
|
+
help="Full Changelog URL (optional; may be parsed from input)",
|
|
304
|
+
)
|
|
305
|
+
parser.add_argument(
|
|
306
|
+
"--test",
|
|
307
|
+
action="store_true",
|
|
308
|
+
help="Run inline self-tests and exit",
|
|
309
|
+
)
|
|
310
|
+
args = parser.parse_args()
|
|
311
|
+
|
|
312
|
+
if args.test:
|
|
313
|
+
run_self_tests()
|
|
314
|
+
return
|
|
315
|
+
|
|
316
|
+
if not args.date:
|
|
317
|
+
parser.error("--date is required unless --test is set")
|
|
318
|
+
|
|
319
|
+
raw = sys.stdin.read()
|
|
320
|
+
breaking: list[str] = []
|
|
321
|
+
features: list[str] = []
|
|
322
|
+
improvements: list[str] = []
|
|
323
|
+
bug_fixes: list[str] = []
|
|
324
|
+
security: list[str] = []
|
|
325
|
+
|
|
326
|
+
changelog_url = args.changelog_url
|
|
327
|
+
for line in raw.splitlines():
|
|
328
|
+
if "Full Changelog" in line or "full changelog" in line.lower():
|
|
329
|
+
url_match = re.search(r"https://[^\s\)]+", line)
|
|
330
|
+
if url_match and not changelog_url:
|
|
331
|
+
changelog_url = url_match.group(0)
|
|
332
|
+
continue
|
|
333
|
+
parsed = parse_bullet(line)
|
|
334
|
+
if parsed is None:
|
|
335
|
+
continue
|
|
336
|
+
formatted, desc_for_category = parsed
|
|
337
|
+
bucket = classify_section(desc_for_category)
|
|
338
|
+
stripped = strip_section_prefix(desc_for_category, bucket)
|
|
339
|
+
if stripped and stripped != desc_for_category:
|
|
340
|
+
stripped = normalize_summary(stripped)
|
|
341
|
+
formatted = formatted.replace(desc_for_category, stripped, 1)
|
|
342
|
+
if bucket == "security":
|
|
343
|
+
security.append(formatted)
|
|
344
|
+
elif bucket == "breaking":
|
|
345
|
+
breaking.append(formatted)
|
|
346
|
+
elif bucket == "bug":
|
|
347
|
+
bug_fixes.append(formatted)
|
|
348
|
+
elif bucket == "feature":
|
|
349
|
+
features.append(formatted)
|
|
350
|
+
else:
|
|
351
|
+
improvements.append(formatted)
|
|
352
|
+
|
|
353
|
+
out = [f"Release Date: {args.date}", ""]
|
|
354
|
+
section_blocks: list[tuple[str, list[str]]] = [
|
|
355
|
+
("## Breaking Changes", breaking),
|
|
356
|
+
("## New Features", features),
|
|
357
|
+
("## Improvements", improvements),
|
|
358
|
+
("## Bug Fixes", bug_fixes),
|
|
359
|
+
("## Security", security),
|
|
360
|
+
]
|
|
361
|
+
for heading, items in section_blocks:
|
|
362
|
+
if items:
|
|
363
|
+
out.append(heading)
|
|
364
|
+
for item in items:
|
|
365
|
+
out.append(f"- {item}")
|
|
366
|
+
out.append("")
|
|
367
|
+
if changelog_url:
|
|
368
|
+
out.append(f"**Full Changelog**: {changelog_url}")
|
|
369
|
+
|
|
370
|
+
sys.stdout.write("\n".join(out))
|
|
371
|
+
if out and not out[-1].endswith("\n"):
|
|
372
|
+
sys.stdout.write("\n")
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
if __name__ == "__main__":
|
|
376
|
+
main()
|