ultralytics-actions 0.0.62__py3-none-any.whl → 0.0.64__py3-none-any.whl

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.
actions/__init__.py CHANGED
@@ -22,4 +22,4 @@
22
22
  # ├── test_summarize_pr.py
23
23
  # └── ...
24
24
 
25
- __version__ = "0.0.62"
25
+ __version__ = "0.0.64"
actions/utils/__init__.py CHANGED
@@ -1,17 +1,14 @@
1
1
  # Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
2
2
 
3
- from .common_utils import REQUESTS_HEADERS, remove_html_comments
4
- from .github_utils import (
5
- GITHUB_API_URL,
6
- Action,
7
- check_pypi_version,
8
- ultralytics_actions_info,
9
- )
3
+ from .common_utils import REDIRECT_IGNORE_LIST, REQUESTS_HEADERS, URL_IGNORE_LIST, remove_html_comments
4
+ from .github_utils import GITHUB_API_URL, Action, check_pypi_version, ultralytics_actions_info
10
5
  from .openai_utils import get_completion
11
6
 
12
7
  __all__ = (
13
8
  "GITHUB_API_URL",
14
9
  "REQUESTS_HEADERS",
10
+ "URL_IGNORE_LIST",
11
+ "REDIRECT_IGNORE_LIST",
15
12
  "Action",
16
13
  "check_pypi_version",
17
14
  "get_completion",
@@ -58,6 +58,43 @@ URL_IGNORE_LIST = { # use a set (not frozenset) to update with possible private
58
58
  "(", # breaks pattern matches
59
59
  "api", # ignore api endpoints
60
60
  }
61
+ REDIRECT_IGNORE_LIST = {
62
+ "{", # possible f-string
63
+ "}", # possible f-string
64
+ "/es/",
65
+ "/us/",
66
+ "en-us",
67
+ "es-es",
68
+ "/latest/",
69
+ "/2022/",
70
+ "/2023/",
71
+ "/2024/",
72
+ "/2025/",
73
+ "/2026/",
74
+ "/2027/",
75
+ "/2028/",
76
+ "/2029/",
77
+ "/2030/",
78
+ "credential",
79
+ "login",
80
+ "consent",
81
+ "verify",
82
+ "badge",
83
+ "shields.io",
84
+ "bit.ly",
85
+ "ow.ly",
86
+ "https://youtu.be/",
87
+ "latex.codecogs.com",
88
+ "svg.image",
89
+ "?view=azureml",
90
+ "ultralytics.com/actions",
91
+ "ultralytics.com/bilibili",
92
+ "ultralytics.com/images",
93
+ "app.gong.io/call?",
94
+ "https://code.visualstudio.com/", # errors
95
+ "?rdt=", # problems with reddit redirecting to https://www.reddit.com/r/ultralytics/?rdt=48616
96
+ "objects.githubusercontent.com", # Prevent replacement with temporary signed GitHub asset URLs
97
+ }
61
98
  URL_PATTERN = re.compile(
62
99
  r"\[([^]]+)]\(([^)]+)\)" # Matches Markdown links [text](url)
63
100
  r"|"
@@ -83,8 +120,16 @@ def clean_url(url):
83
120
  return url
84
121
 
85
122
 
123
+ def allow_redirect(url):
124
+ """Check if URL should be skipped based on simple rules."""
125
+ url_lower = url.lower()
126
+ return url and url.startswith("https://") and not any(item in url_lower for item in REDIRECT_IGNORE_LIST)
127
+
128
+
86
129
  def brave_search(query, api_key, count=5):
87
130
  """Search for alternative URLs using Brave Search API."""
131
+ if not api_key:
132
+ return
88
133
  headers = {"X-Subscription-Token": api_key, "Accept": "application/json"}
89
134
  if len(query) > 400:
90
135
  print(f"WARNING ⚠️ Brave search query length {len(query)} exceed limit of 400 characters, truncating.")
@@ -95,18 +140,18 @@ def brave_search(query, api_key, count=5):
95
140
  return [result.get("url") for result in results if result.get("url")]
96
141
 
97
142
 
98
- def is_url(url, session=None, check=True, max_attempts=3, timeout=2):
143
+ def is_url(url, session=None, check=True, max_attempts=3, timeout=2, return_url=False, redirect=False):
99
144
  """Check if string is URL and optionally verify it exists, with fallback for GitHub repos."""
100
145
  try:
101
146
  # Check allow list
102
147
  if any(x in url for x in URL_IGNORE_LIST):
103
- return True
148
+ return (True, url) if return_url else True
104
149
 
105
150
  # Check structure
106
151
  result = parse.urlparse(url)
107
152
  partition = result.netloc.partition(".") # i.e. netloc = "github.com" -> ("github", ".", "com")
108
153
  if not result.scheme or not partition[0] or not partition[2]:
109
- return False
154
+ return (False, url) if return_url else False
110
155
 
111
156
  if check:
112
157
  requester = session or requests
@@ -118,9 +163,11 @@ def is_url(url, session=None, check=True, max_attempts=3, timeout=2):
118
163
  try:
119
164
  # Try HEAD first, then GET if needed
120
165
  for method in (requester.head, requester.get):
121
- status_code = method(url, stream=method == requester.get, **kwargs).status_code
122
- if status_code not in BAD_HTTP_CODES:
123
- return True
166
+ response = method(url, stream=method == requester.get, **kwargs)
167
+ if redirect and allow_redirect(response.url):
168
+ url = response.url
169
+ if response.status_code not in BAD_HTTP_CODES:
170
+ return (True, url) if return_url else True
124
171
 
125
172
  # If GitHub and check fails (repo might be private), add the base GitHub URL to ignore list
126
173
  if result.hostname == "github.com":
@@ -129,57 +176,61 @@ def is_url(url, session=None, check=True, max_attempts=3, timeout=2):
129
176
  base_url = f"https://github.com/{parts[0]}/{parts[1]}" # https://github.com/org/repo
130
177
  if requester.head(base_url, **kwargs).status_code == 404:
131
178
  URL_IGNORE_LIST.add(base_url)
132
- return True
179
+ return (True, url) if return_url else True
133
180
 
134
- return False
181
+ return (False, url) if return_url else False
135
182
  except Exception:
136
183
  if attempt == max_attempts - 1: # last attempt
137
- return False
184
+ return (False, url) if return_url else False
138
185
  time.sleep(2**attempt) # exponential backoff
139
- return False
140
- return True
186
+ return (False, url) if return_url else False
187
+ return (True, url) if return_url else True
141
188
  except Exception:
142
- return False
189
+ return (False, url) if return_url else False
143
190
 
144
191
 
145
- def check_links_in_string(text, verbose=True, return_bad=False, replace=False):
146
- """Process a given text, find unique URLs within it, and check for any 404 errors."""
147
- all_urls = []
192
+ def check_links_in_string(text, verbose=True, return_bad=False, replace=False, redirect=True):
193
+ """Process text, find URLs, check for 404s, and handle replacements with redirects or Brave search."""
194
+ urls = []
148
195
  for md_text, md_url, plain_url in URL_PATTERN.findall(text):
149
196
  url = md_url or plain_url
150
197
  if url and parse.urlparse(url).scheme:
151
- all_urls.append((md_text, url, md_url != ""))
152
-
153
- urls = [(t, clean_url(u), is_md) for t, u, is_md in all_urls] # clean URLs
198
+ urls.append((md_text, clean_url(url)))
154
199
 
155
200
  with requests.Session() as session, ThreadPoolExecutor(max_workers=64) as executor:
156
201
  session.headers.update(REQUESTS_HEADERS)
157
- valid_results = list(executor.map(lambda x: is_url(x[1], session), urls))
158
- bad_urls = [url for (_, url, _), valid in zip(urls, valid_results) if not valid]
159
-
160
- if replace and bad_urls:
161
- if brave_api_key := os.getenv("BRAVE_API_KEY"):
162
- replacements = {}
163
- modified_text = text
164
-
165
- for (title, url, is_md), valid in zip(urls, valid_results):
166
- if not valid:
167
- alternative_urls = brave_search(f"{title[:200]} {url[:200]}", brave_api_key, count=3)
168
- if alternative_urls:
169
- # Try each alternative URL until we find one that works
170
- for alt_url in alternative_urls:
171
- if is_url(alt_url, session):
172
- break
173
- replacements[url] = alt_url
174
- modified_text = modified_text.replace(url, alt_url)
175
-
176
- if verbose and replacements:
177
- print(
178
- f"WARNING ⚠️ replaced {len(replacements)} broken links:\n"
179
- + "\n".join(f" {k}: {v}" for k, v in replacements.items())
180
- )
181
- if replacements:
182
- return (True, [], modified_text) if return_bad else modified_text
202
+ results = list(executor.map(lambda x: is_url(x[1], session, return_url=True, redirect=redirect), urls))
203
+ bad_urls = [url for (title, url), (valid, redirect) in zip(urls, results) if not valid]
204
+
205
+ if replace:
206
+ replacements = {}
207
+ modified_text = text
208
+
209
+ # Process all URLs for replacements
210
+ brave_api_key = os.getenv("BRAVE_API_KEY")
211
+ for (title, url), (valid, redirect) in zip(urls, results):
212
+ # Handle invalid URLs with Brave search
213
+ if not valid and brave_api_key:
214
+ alternative_urls = brave_search(f"{title[:200]} {url[:200]}", brave_api_key, count=3)
215
+ if alternative_urls:
216
+ # Try each alternative URL until we find one that works
217
+ for alt_url in alternative_urls:
218
+ if is_url(alt_url, session):
219
+ replacements[url] = alt_url
220
+ modified_text = modified_text.replace(url, alt_url)
221
+ break
222
+ # Handle redirects for valid URLs
223
+ elif valid and redirect and redirect != url:
224
+ replacements[url] = redirect
225
+ modified_text = modified_text.replace(url, redirect)
226
+
227
+ if verbose and replacements:
228
+ print(
229
+ f"WARNING ⚠️ replaced {len(replacements)} links:\n"
230
+ + "\n".join(f" {k}: {v}" for k, v in replacements.items())
231
+ )
232
+ if replacements:
233
+ return (True, bad_urls, modified_text) if return_bad else modified_text
183
234
 
184
235
  passing = not bad_urls
185
236
  if verbose and not passing:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ultralytics-actions
3
- Version: 0.0.62
3
+ Version: 0.0.64
4
4
  Summary: Ultralytics Actions for GitHub automation and PR management.
5
5
  Author-email: Glenn Jocher <glenn.jocher@ultralytics.com>
6
6
  Maintainer-email: Ultralytics <hello@ultralytics.com>
@@ -1,15 +1,15 @@
1
- actions/__init__.py,sha256=L-eJXIjqrGdQJJX03CbSkbk9vx8j0cpT3pVmrO-0WZQ,742
1
+ actions/__init__.py,sha256=_0iCAgCQC2PbO-T1qslfJ9W1VETJy0oGXGI1hGhHklQ,742
2
2
  actions/first_interaction.py,sha256=1_WvQHCi5RWaSfyi49ClF2Zk_3CKGjFnZqz6FlxPRAc,17868
3
3
  actions/summarize_pr.py,sha256=BKttOq-MGaanVaChLU5B1ewKUA8K6S05Cy3FQtyRmxU,11681
4
4
  actions/summarize_release.py,sha256=tov6qsYGC68lfobvkwVyoWZBGtJ598G0m097n4Ydzvo,8472
5
5
  actions/update_markdown_code_blocks.py,sha256=9PL7YIQfApRNAa0que2hYHv7umGZTZoHlblesB0xFj4,8587
6
- actions/utils/__init__.py,sha256=WStdEAYROVnF0nubEOmrFLrejkRiMXIefA5O1ckfcFs,476
7
- actions/utils/common_utils.py,sha256=wN8uQim3-NPTtu_Zg7qYYPY8RQtQSg8ZCrc2d1LaOCY,8394
6
+ actions/utils/__init__.py,sha256=XjFyREuhiA7pHfHHBQQqHYKokHhq_px1P9sezk_f1vA,545
7
+ actions/utils/common_utils.py,sha256=KIwFev9mGdgD7Z5IT0UuHNgZi-phKcZ1A1pggBU8Xbo,10191
8
8
  actions/utils/github_utils.py,sha256=-F--JgxtXE0fSPMFEzakz7iZilp-vonzLiyXfg0b17Y,7117
9
9
  actions/utils/openai_utils.py,sha256=qQbmrJpOUANxSMf7inDSgPIwgf0JHD1fWZuab-y2W6g,2942
10
- ultralytics_actions-0.0.62.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
11
- ultralytics_actions-0.0.62.dist-info/METADATA,sha256=nB1kpNbwXVVic2kKwVsChkMgeySZUUMim4v-TbaBfVQ,10923
12
- ultralytics_actions-0.0.62.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
13
- ultralytics_actions-0.0.62.dist-info/entry_points.txt,sha256=GowvOFplj0C7JmsjbKcbpgLpdf2r921pcaOQkAHWZRA,378
14
- ultralytics_actions-0.0.62.dist-info/top_level.txt,sha256=5apM5x80QlJcGbACn1v3fkmIuL1-XQCKcItJre7w7Tw,8
15
- ultralytics_actions-0.0.62.dist-info/RECORD,,
10
+ ultralytics_actions-0.0.64.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
11
+ ultralytics_actions-0.0.64.dist-info/METADATA,sha256=GpiuJf5WlfPKaSl8hCZsXDH-JuKUJunJzweiokhJzAg,10923
12
+ ultralytics_actions-0.0.64.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
13
+ ultralytics_actions-0.0.64.dist-info/entry_points.txt,sha256=GowvOFplj0C7JmsjbKcbpgLpdf2r921pcaOQkAHWZRA,378
14
+ ultralytics_actions-0.0.64.dist-info/top_level.txt,sha256=5apM5x80QlJcGbACn1v3fkmIuL1-XQCKcItJre7w7Tw,8
15
+ ultralytics_actions-0.0.64.dist-info/RECORD,,