ghstack 0.16.0__tar.gz → 0.17.0__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 (38) hide show
  1. {ghstack-0.16.0 → ghstack-0.17.0}/PKG-INFO +1 -1
  2. {ghstack-0.16.0 → ghstack-0.17.0}/pyproject.toml +1 -1
  3. {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/cli.py +2 -0
  4. {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/config.py +17 -0
  5. {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/github_real.py +27 -24
  6. {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/github_utils.py +7 -2
  7. {ghstack-0.16.0 → ghstack-0.17.0}/LICENSE +0 -0
  8. {ghstack-0.16.0 → ghstack-0.17.0}/README.md +0 -0
  9. {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/__init__.py +0 -0
  10. {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/__main__.py +0 -0
  11. {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/action.py +0 -0
  12. {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/async_script.py +0 -0
  13. {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/cache.py +0 -0
  14. {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/checkout.py +0 -0
  15. {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/cherry_pick.py +0 -0
  16. {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/circleci.py +0 -0
  17. {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/circleci_real.py +0 -0
  18. {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/diff.py +0 -0
  19. {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/forensics.py +0 -0
  20. {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/git.py +0 -0
  21. {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/github.py +0 -0
  22. {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/github_fake.py +0 -0
  23. {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/github_schema.graphql +0 -0
  24. {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/gpg_sign.py +0 -0
  25. {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/land.py +0 -0
  26. {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/log.py +0 -0
  27. {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/logs.py +0 -0
  28. {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/pull.py +0 -0
  29. {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/py.typed +0 -0
  30. {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/rage.py +0 -0
  31. {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/shell.py +0 -0
  32. {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/status.py +0 -0
  33. {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/submit.py +0 -0
  34. {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/sync.py +0 -0
  35. {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/test_prelude.py +0 -0
  36. {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/trailers.py +0 -0
  37. {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/types.py +0 -0
  38. {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/unlink.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ghstack
3
- Version: 0.16.0
3
+ Version: 0.17.0
4
4
  Summary: Stack diff support for GitHub
5
5
  Author: Edward Z. Yang
6
6
  Author-email: Edward Z. Yang <ezyang@mit.edu>
@@ -16,7 +16,7 @@ dependencies = [
16
16
  "flake8<8.0.0,>=7.0.0",
17
17
  ]
18
18
  name = "ghstack"
19
- version = "0.16.0"
19
+ version = "0.17.0"
20
20
  description = "Stack diff support for GitHub"
21
21
  readme = "README.md"
22
22
  classifiers = [
@@ -61,6 +61,8 @@ def cli_context(
61
61
  oauth_token=config.github_oauth,
62
62
  proxy=config.proxy,
63
63
  github_url=config.github_url,
64
+ max_retries=config.max_retries,
65
+ initial_backoff_seconds=config.initial_backoff_seconds,
64
66
  )
65
67
  yield shell, config, github
66
68
 
@@ -111,6 +111,11 @@ Config = NamedTuple(
111
111
  ("reviewer", Optional[str]),
112
112
  # Default labels to add to new pull requests (comma-separated labels)
113
113
  ("label", Optional[str]),
114
+ # Retry/backoff tuning
115
+ ("max_retries", int),
116
+ # The initial backoff time to use, in seconds. We will double this
117
+ # time for each retry.
118
+ ("initial_backoff_seconds", int),
114
119
  # Command to generate a per-PR update description from diff contents
115
120
  ("automsg", Optional[str]),
116
121
  ],
@@ -334,6 +339,16 @@ def read_config(
334
339
  else:
335
340
  automsg = None
336
341
 
342
+ if config.has_option("ghstack", "max_retries"):
343
+ max_retries = config.getint("ghstack", "max_retries")
344
+ else:
345
+ max_retries = 5
346
+
347
+ if config.has_option("ghstack", "initial_backoff_seconds"):
348
+ initial_backoff_seconds = config.getint("ghstack", "initial_backoff_seconds")
349
+ else:
350
+ initial_backoff_seconds = 60
351
+
337
352
  if write_back:
338
353
  with open(config_path, "w") as f:
339
354
  config.write(f)
@@ -351,6 +366,8 @@ def read_config(
351
366
  remote_name=remote_name,
352
367
  reviewer=reviewer,
353
368
  label=label,
369
+ max_retries=max_retries,
370
+ initial_backoff_seconds=initial_backoff_seconds,
354
371
  automsg=automsg,
355
372
  )
356
373
  logging.debug(f"conf = {conf}")
@@ -13,9 +13,6 @@ import aiohttp
13
13
 
14
14
  import ghstack.github
15
15
 
16
- MAX_RETRIES = 5
17
- INITIAL_BACKOFF_SECONDS = 60
18
-
19
16
 
20
17
  class RealGitHubEndpoint(ghstack.github.GitHubEndpoint):
21
18
  """
@@ -61,6 +58,9 @@ class RealGitHubEndpoint(ghstack.github.GitHubEndpoint):
61
58
  cert: Optional[Union[str, Tuple[str, str]]]
62
59
  _session: Optional[aiohttp.ClientSession]
63
60
 
61
+ max_retries: int
62
+ initial_backoff_seconds: int
63
+
64
64
  def __init__(
65
65
  self,
66
66
  oauth_token: Optional[str],
@@ -68,12 +68,16 @@ class RealGitHubEndpoint(ghstack.github.GitHubEndpoint):
68
68
  proxy: Optional[str] = None,
69
69
  verify: Optional[Union[str, bool]] = None,
70
70
  cert: Optional[Union[str, Tuple[str, str]]] = None,
71
+ max_retries: int = 5,
72
+ initial_backoff_seconds: int = 60,
71
73
  ):
72
74
  self.oauth_token = oauth_token
73
75
  self.proxy = proxy
74
76
  self.github_url = github_url
75
77
  self.verify = verify
76
78
  self.cert = cert
79
+ self.max_retries = max_retries
80
+ self.initial_backoff_seconds = initial_backoff_seconds
77
81
  self._rest_request_ids = itertools.count(1)
78
82
  self._session = None
79
83
 
@@ -212,11 +216,11 @@ class RealGitHubEndpoint(ghstack.github.GitHubEndpoint):
212
216
  if aiohttp_ssl is not None:
213
217
  request_kwargs["ssl"] = aiohttp_ssl
214
218
 
215
- backoff_seconds = INITIAL_BACKOFF_SECONDS
219
+ backoff_seconds = self.initial_backoff_seconds
216
220
  request_id = next(self._rest_request_ids)
217
221
  log_prefix = f"rest[{request_id}]"
218
222
  session = self._get_session()
219
- for attempt in range(0, MAX_RETRIES):
223
+ for attempt in range(0, self.max_retries):
220
224
  logging.debug("# %s %s %s", log_prefix, method, url)
221
225
  logging.debug(
222
226
  "%s request body:\n%s", log_prefix, json.dumps(kwargs, indent=1)
@@ -224,12 +228,11 @@ class RealGitHubEndpoint(ghstack.github.GitHubEndpoint):
224
228
 
225
229
  async with getattr(session, method)(url, **request_kwargs) as resp:
226
230
  logging.debug("%s response status: %s", log_prefix, resp.status)
231
+ resp_text = await resp.text()
227
232
  try:
228
- r = await resp.json()
229
- except (aiohttp.ContentTypeError, ValueError):
230
- logging.debug(
231
- "%s response body:\n%s", log_prefix, await resp.text()
232
- )
233
+ r = json.loads(resp_text)
234
+ except ValueError:
235
+ logging.debug("%s response body:\n%s", log_prefix, resp_text)
233
236
  raise
234
237
  else:
235
238
  pretty_json = json.dumps(r, indent=1)
@@ -240,32 +243,32 @@ class RealGitHubEndpoint(ghstack.github.GitHubEndpoint):
240
243
  if resp.status in (403, 429):
241
244
  remaining_count = resp.headers.get("x-ratelimit-remaining")
242
245
  reset_time = resp.headers.get("x-ratelimit-reset")
246
+ more_attempts = attempt < self.max_retries - 1
243
247
 
244
248
  if remaining_count == "0" and reset_time:
245
- sleep_time = int(reset_time) - int(time.time())
246
- logging.warning(
247
- f"Rate limit exceeded. Sleeping until reset in {sleep_time} seconds."
248
- )
249
- await asyncio.sleep(sleep_time)
250
- continue
249
+ sleep_time = max(0, int(reset_time) - int(time.time()))
250
+ if more_attempts:
251
+ logging.warning(
252
+ f"Rate limit exceeded. Sleeping until reset in {sleep_time} seconds."
253
+ )
254
+ await asyncio.sleep(sleep_time)
255
+ continue
251
256
  # GitHub doesn't document the content of these messages, but this
252
257
  # seems to be an accurate way to find secondary rate limits. Any
253
258
  # other reason for 403 or 429 will fall through to the error below.
254
- elif b"rate limit" in resp.content.lower():
259
+ elif "rate limit" in resp_text.lower():
255
260
  retry_after_seconds = resp.headers.get("retry-after")
256
261
  if retry_after_seconds:
257
262
  sleep_time = int(retry_after_seconds)
258
- logging.warning(
259
- f"Secondary rate limit hit. Sleeping for {sleep_time} seconds."
260
- )
261
263
  else:
262
264
  sleep_time = backoff_seconds
265
+ backoff_seconds *= 2
266
+ if more_attempts:
263
267
  logging.warning(
264
- f"Secondary rate limit hit. Sleeping for {sleep_time} seconds (exponential backoff)."
268
+ f"Secondary rate limit hit. Sleeping for {sleep_time} seconds."
265
269
  )
266
- backoff_seconds *= 2
267
- await asyncio.sleep(sleep_time)
268
- continue
270
+ await asyncio.sleep(max(0, sleep_time))
271
+ continue
269
272
 
270
273
  if resp.status == 404:
271
274
  raise ghstack.github.NotFoundError(
@@ -32,7 +32,10 @@ async def get_github_repo_name_with_owner(
32
32
  # where commits will actually be pushed to
33
33
  remote_url = await sh.agit("remote", "get-url", "--push", remote_name)
34
34
  while True:
35
- match = r"^git@{github_url}:/?([^/]+)/(.+?)(?:\.git)?$".format(
35
+ # The SSH user can be "git" (e.g. git@github.com:owner/repo.git) or an
36
+ # arbitrary identity such as the org-scoped deploy user GitHub now hands
37
+ # out (e.g. org-21003710@github.com:owner/repo.git), so match any user.
38
+ match = r"^[^@]+@{github_url}:/?([^/]+)/(.+?)(?:\.git)?$".format(
36
39
  github_url=github_url
37
40
  )
38
41
  m = re.match(match, remote_url)
@@ -175,7 +178,9 @@ GitHubPullRequestParams = TypedDict(
175
178
  def _normalize_remote_url(remote_url: str) -> str:
176
179
  """Convert SSH remote URL to HTTPS format, strip .git suffix."""
177
180
  # git@github.com:owner/repo.git -> https://github.com/owner/repo
178
- m = re.match(r"^git@([^:]+):/?(.+?)(?:\.git)?$", remote_url)
181
+ # The SSH user may be "git" or an org-scoped identity such as
182
+ # org-21003710@github.com:owner/repo.git, so match any user.
183
+ m = re.match(r"^[^@]+@([^:]+):/?(.+?)(?:\.git)?$", remote_url)
179
184
  if m:
180
185
  return f"https://{m.group(1)}/{m.group(2)}"
181
186
  return re.sub(r"\.git$", "", remote_url)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes