itw-python-builder 0.2.14__tar.gz → 0.2.15__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.
Files changed (25) hide show
  1. {itw_python_builder-0.2.14 → itw_python_builder-0.2.15}/PKG-INFO +4 -4
  2. {itw_python_builder-0.2.14 → itw_python_builder-0.2.15}/README.md +3 -3
  3. {itw_python_builder-0.2.14 → itw_python_builder-0.2.15}/itw_python_builder/task_utils.py +93 -15
  4. {itw_python_builder-0.2.14 → itw_python_builder-0.2.15}/itw_python_builder.egg-info/PKG-INFO +4 -4
  5. {itw_python_builder-0.2.14 → itw_python_builder-0.2.15}/pyproject.toml +1 -1
  6. {itw_python_builder-0.2.14 → itw_python_builder-0.2.15}/LICENSE +0 -0
  7. {itw_python_builder-0.2.14 → itw_python_builder-0.2.15}/itw_python_builder/.pylintrc +0 -0
  8. {itw_python_builder-0.2.14 → itw_python_builder-0.2.15}/itw_python_builder/__init__.py +0 -0
  9. {itw_python_builder-0.2.14 → itw_python_builder-0.2.15}/itw_python_builder/_pyruntime/sitecustomize.py +0 -0
  10. {itw_python_builder-0.2.14 → itw_python_builder-0.2.15}/itw_python_builder/cli.py +0 -0
  11. {itw_python_builder-0.2.14 → itw_python_builder-0.2.15}/itw_python_builder/notify.py +0 -0
  12. {itw_python_builder-0.2.14 → itw_python_builder-0.2.15}/itw_python_builder/ssr_tasks.py +0 -0
  13. {itw_python_builder-0.2.14 → itw_python_builder-0.2.15}/itw_python_builder/tasks.py +0 -0
  14. {itw_python_builder-0.2.14 → itw_python_builder-0.2.15}/itw_python_builder/templates/new_version_email.html +0 -0
  15. {itw_python_builder-0.2.14 → itw_python_builder-0.2.15}/itw_python_builder/templates/server.sitemap.snippet.ts +0 -0
  16. {itw_python_builder-0.2.14 → itw_python_builder-0.2.15}/itw_python_builder/templates/sitemap.routes.ts +0 -0
  17. {itw_python_builder-0.2.14 → itw_python_builder-0.2.15}/itw_python_builder/templates/task_template.md +0 -0
  18. {itw_python_builder-0.2.14 → itw_python_builder-0.2.15}/itw_python_builder/utils.py +0 -0
  19. {itw_python_builder-0.2.14 → itw_python_builder-0.2.15}/itw_python_builder/version.py +0 -0
  20. {itw_python_builder-0.2.14 → itw_python_builder-0.2.15}/itw_python_builder.egg-info/SOURCES.txt +0 -0
  21. {itw_python_builder-0.2.14 → itw_python_builder-0.2.15}/itw_python_builder.egg-info/dependency_links.txt +0 -0
  22. {itw_python_builder-0.2.14 → itw_python_builder-0.2.15}/itw_python_builder.egg-info/entry_points.txt +0 -0
  23. {itw_python_builder-0.2.14 → itw_python_builder-0.2.15}/itw_python_builder.egg-info/requires.txt +0 -0
  24. {itw_python_builder-0.2.14 → itw_python_builder-0.2.15}/itw_python_builder.egg-info/top_level.txt +0 -0
  25. {itw_python_builder-0.2.14 → itw_python_builder-0.2.15}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: itw_python_builder
3
- Version: 0.2.14
3
+ Version: 0.2.15
4
4
  Summary: Standardized Django deployment pipeline with Docker, testing, and SonarQube integration
5
5
  Author-email: IT-Works <contact@it-works.io>
6
6
  License: MIT
@@ -295,11 +295,11 @@ With `/repo`, leading and trailing slashes are stripped automatically, and full
295
295
 
296
296
  ### Milestones, Labels and Duplicates
297
297
 
298
- Milestones, labels and issue titles are matched with a shared fuzzy comparison at 95% similarity, so small differences in spacing or casing still count as a match. The threshold is a single constant (`SIMILARITY_THRESHOLD` in `task_utils.py`) if you need to tune it.
298
+ Milestones, labels and issue titles are matched with a shared fuzzy comparison at 95% similarity, so small differences in spacing or casing still count as a match. Digit runs must be identical (`java 38` does not match `java 37`). The threshold is a single constant (`SIMILARITY_THRESHOLD` in `task_utils.py`) if you need to tune it.
299
299
 
300
- - **Milestone** — a similar existing milestone is reused; otherwise it is created (with the start/end dates when both are given).
300
+ - **Milestone** — an existing project or parent-group milestone with the same title is reused; otherwise it is created (with the start/end dates when both are given).
301
301
  - **Label** — a similar existing label is linked and logged in white; a new one is created and logged in green. An empty `/label` is never sent to GitLab.
302
- - **Duplicate issues** — before creating, the target project is checked for a similar title *within the same milestone* (or among issues with no milestone). If one exists the issue is not created and a white log line reports it.
302
+ - **Duplicate issues** — before creating, the target project is searched by title. A match in the same milestone, or an unassigned issue with the same title, is treated as a duplicate and skipped.
303
303
 
304
304
  ### Acceptance Criteria and Comments
305
305
 
@@ -266,11 +266,11 @@ With `/repo`, leading and trailing slashes are stripped automatically, and full
266
266
 
267
267
  ### Milestones, Labels and Duplicates
268
268
 
269
- Milestones, labels and issue titles are matched with a shared fuzzy comparison at 95% similarity, so small differences in spacing or casing still count as a match. The threshold is a single constant (`SIMILARITY_THRESHOLD` in `task_utils.py`) if you need to tune it.
269
+ Milestones, labels and issue titles are matched with a shared fuzzy comparison at 95% similarity, so small differences in spacing or casing still count as a match. Digit runs must be identical (`java 38` does not match `java 37`). The threshold is a single constant (`SIMILARITY_THRESHOLD` in `task_utils.py`) if you need to tune it.
270
270
 
271
- - **Milestone** — a similar existing milestone is reused; otherwise it is created (with the start/end dates when both are given).
271
+ - **Milestone** — an existing project or parent-group milestone with the same title is reused; otherwise it is created (with the start/end dates when both are given).
272
272
  - **Label** — a similar existing label is linked and logged in white; a new one is created and logged in green. An empty `/label` is never sent to GitLab.
273
- - **Duplicate issues** — before creating, the target project is checked for a similar title *within the same milestone* (or among issues with no milestone). If one exists the issue is not created and a white log line reports it.
273
+ - **Duplicate issues** — before creating, the target project is searched by title. A match in the same milestone, or an unassigned issue with the same title, is treated as a duplicate and skipped.
274
274
 
275
275
  ### Acceptance Criteria and Comments
276
276
 
@@ -1,9 +1,13 @@
1
1
  import re
2
+ import unicodedata
2
3
  from difflib import SequenceMatcher
4
+ from urllib.parse import quote
3
5
 
4
6
  from itw_python_builder.utils import _gitlab_api_call
5
7
 
6
8
  SIMILARITY_THRESHOLD = 0.95
9
+ GITLAB_PAGE_SIZE = 100
10
+ _RE_DIGITS = re.compile(r'\d+')
7
11
 
8
12
  DATE_PLACEHOLDER = 'YYYY-MM-DD'
9
13
 
@@ -48,7 +52,12 @@ def log_info(msg: str) -> None:
48
52
 
49
53
 
50
54
  def _normalize_title(value: str) -> str:
51
- return ' '.join((value or '').lower().split())
55
+ collapsed = ' '.join((value or '').lower().split())
56
+ return unicodedata.normalize('NFC', collapsed)
57
+
58
+
59
+ def _digit_tokens(value: str) -> list:
60
+ return _RE_DIGITS.findall(value or '')
52
61
 
53
62
 
54
63
  def is_similar(left: str, right: str, threshold: float = SIMILARITY_THRESHOLD) -> bool:
@@ -59,19 +68,64 @@ def is_similar(left: str, right: str, threshold: float = SIMILARITY_THRESHOLD) -
59
68
  return False
60
69
  if a == b:
61
70
  return True
71
+ if _digit_tokens(a) != _digit_tokens(b):
72
+ return False
62
73
  return SequenceMatcher(None, a, b).ratio() >= threshold
63
74
 
64
75
 
65
76
  def find_similar(value: str, items: list, key: str = 'title', threshold: float = SIMILARITY_THRESHOLD):
66
- """Return the first item whose `key` is ~threshold similar to `value`, else None."""
77
+ """Return an exact normalized match if one exists, else the first ~threshold match."""
78
+ target = _normalize_title(value)
79
+ fuzzy = None
67
80
  for item in items:
68
- if is_similar(value, item.get(key, ''), threshold):
81
+ other = item.get(key, '')
82
+ if target and target == _normalize_title(other):
69
83
  return item
70
- return None
84
+ if fuzzy is None and is_similar(value, other, threshold):
85
+ fuzzy = item
86
+ return fuzzy
87
+
88
+
89
+ def _gitlab_api_list(api_host: str, endpoint: str, token: str) -> list:
90
+ """GET a GitLab collection, following pages until a short page is returned."""
91
+ joiner = '&' if '?' in endpoint else '?'
92
+ page = 1
93
+ collected = []
94
+ while True:
95
+ chunk = _gitlab_api_call(
96
+ api_host,
97
+ f'{endpoint}{joiner}per_page={GITLAB_PAGE_SIZE}&page={page}',
98
+ token,
99
+ )
100
+ if not isinstance(chunk, list):
101
+ raise RuntimeError(
102
+ f'GitLab API GET {endpoint} expected a list, got {type(chunk).__name__}'
103
+ )
104
+ collected.extend(chunk)
105
+ if len(chunk) < GITLAB_PAGE_SIZE:
106
+ break
107
+ page += 1
108
+ return collected
109
+
110
+
111
+ def _issue_milestone_title(issue: dict) -> str:
112
+ milestone = issue.get('milestone') or {}
113
+ if isinstance(milestone, dict):
114
+ return (milestone.get('title') or '').strip()
115
+ return ''
116
+
117
+
118
+ def _is_duplicate_issue(issue: dict, title: str, milestone: str = None) -> bool:
119
+ """True when titles match and the issue is in the target milestone or has none."""
120
+ if not is_similar(title, issue.get('title', '')):
121
+ return False
122
+ existing = _issue_milestone_title(issue)
123
+ if milestone:
124
+ return (not existing) or is_similar(milestone, existing)
125
+ return not existing
71
126
 
72
127
 
73
128
  def _list_group_projects(api_host: str, group_path: str, token: str) -> list:
74
- from urllib.parse import quote
75
129
  encoded = quote(group_path, safe='')
76
130
  return _gitlab_api_call(
77
131
  api_host,
@@ -113,27 +167,51 @@ def create_gitlab_issue(api_host: str, project_id: int, payload: dict, token: st
113
167
 
114
168
 
115
169
  def get_project_by_path(api_host: str, project_path: str, token: str) -> dict:
116
- from urllib.parse import quote
117
170
  encoded = quote(project_path, safe='')
118
171
  return _gitlab_api_call(api_host, f'/projects/{encoded}', token)
119
172
 
120
173
 
121
174
  def find_project_milestone(api_host: str, project_id: int, title: str, token: str):
122
- results = _gitlab_api_call(
175
+ """Find a project or ancestor-group milestone by title."""
176
+ encoded = quote(title, safe='')
177
+ base = f'/projects/{project_id}/milestones?include_ancestors=true'
178
+ exact = _gitlab_api_list(api_host, f'{base}&title={encoded}', token)
179
+ match = find_similar(title, exact)
180
+ if match:
181
+ return match
182
+ searched = _gitlab_api_list(api_host, f'{base}&search={encoded}', token)
183
+ return find_similar(title, searched)
184
+
185
+
186
+ def _list_issues_for_milestone(api_host: str, project_id: int, milestone: str, token: str) -> list:
187
+ encoded = quote(milestone, safe='') if milestone else 'None'
188
+ return _gitlab_api_list(
123
189
  api_host,
124
- f'/projects/{project_id}/milestones?per_page=100',
190
+ f'/projects/{project_id}/issues?milestone={encoded}',
125
191
  token,
126
192
  )
127
- return find_similar(title, results)
128
193
 
129
194
 
130
195
  def find_project_issue(api_host: str, project_id: int, title: str, token: str, milestone: str = None):
131
- """Look for a ~matching title within this milestone ('None' = the no-milestone bucket)."""
132
- from urllib.parse import quote
133
- endpoint = f'/projects/{project_id}/issues?per_page=100'
134
- endpoint += f'&milestone={quote(milestone, safe="") if milestone else "None"}'
135
- results = _gitlab_api_call(api_host, endpoint, token)
136
- return find_similar(title, results)
196
+ """Look for a ~matching title in this milestone, or among issues with no milestone."""
197
+ encoded = quote(title, safe='')
198
+ searched = _gitlab_api_list(
199
+ api_host,
200
+ f'/projects/{project_id}/issues?search={encoded}&in=title',
201
+ token,
202
+ )
203
+ for item in searched:
204
+ if _is_duplicate_issue(item, title, milestone):
205
+ return item
206
+
207
+ for item in _list_issues_for_milestone(api_host, project_id, milestone, token):
208
+ if _is_duplicate_issue(item, title, milestone):
209
+ return item
210
+ if milestone:
211
+ for item in _list_issues_for_milestone(api_host, project_id, None, token):
212
+ if _is_duplicate_issue(item, title, milestone):
213
+ return item
214
+ return None
137
215
 
138
216
 
139
217
  def create_project_milestone(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: itw_python_builder
3
- Version: 0.2.14
3
+ Version: 0.2.15
4
4
  Summary: Standardized Django deployment pipeline with Docker, testing, and SonarQube integration
5
5
  Author-email: IT-Works <contact@it-works.io>
6
6
  License: MIT
@@ -295,11 +295,11 @@ With `/repo`, leading and trailing slashes are stripped automatically, and full
295
295
 
296
296
  ### Milestones, Labels and Duplicates
297
297
 
298
- Milestones, labels and issue titles are matched with a shared fuzzy comparison at 95% similarity, so small differences in spacing or casing still count as a match. The threshold is a single constant (`SIMILARITY_THRESHOLD` in `task_utils.py`) if you need to tune it.
298
+ Milestones, labels and issue titles are matched with a shared fuzzy comparison at 95% similarity, so small differences in spacing or casing still count as a match. Digit runs must be identical (`java 38` does not match `java 37`). The threshold is a single constant (`SIMILARITY_THRESHOLD` in `task_utils.py`) if you need to tune it.
299
299
 
300
- - **Milestone** — a similar existing milestone is reused; otherwise it is created (with the start/end dates when both are given).
300
+ - **Milestone** — an existing project or parent-group milestone with the same title is reused; otherwise it is created (with the start/end dates when both are given).
301
301
  - **Label** — a similar existing label is linked and logged in white; a new one is created and logged in green. An empty `/label` is never sent to GitLab.
302
- - **Duplicate issues** — before creating, the target project is checked for a similar title *within the same milestone* (or among issues with no milestone). If one exists the issue is not created and a white log line reports it.
302
+ - **Duplicate issues** — before creating, the target project is searched by title. A match in the same milestone, or an unassigned issue with the same title, is treated as a duplicate and skipped.
303
303
 
304
304
  ### Acceptance Criteria and Comments
305
305
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "itw_python_builder"
7
- version = "0.2.14"
7
+ version = "0.2.15"
8
8
  description = "Standardized Django deployment pipeline with Docker, testing, and SonarQube integration"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"