diff --git a/poetry.lock b/poetry.lock index e0039e5..6c8729d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -403,6 +403,46 @@ dev = ["PyYAML (>=5.4.1)", "coloredlogs (>=15.0.1)", "fire (>=0.4.0)"] diskcache = ["diskcache (>=5.2.1)", "multiprocess (>=0.70.12)", "psutil (>=5.8.0)"] testing = ["beautifulsoup4 (>=4.8.2)", "cryptography", "dash-testing-stub (>=0.0.2)", "lxml (>=4.6.2)", "multiprocess (>=0.70.12)", "percy (>=2.0.2)", "psutil (>=5.8.0)", "pytest (>=6.0.2)", "requests[security] (>=2.21.0)", "selenium (>=3.141.0,<=4.2.0)", "waitress (>=1.4.4)"] +[[package]] +name = "debugpy" +version = "1.8.17" +description = "An implementation of the Debug Adapter Protocol for Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "debugpy-1.8.17-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:c41d2ce8bbaddcc0009cc73f65318eedfa3dbc88a8298081deb05389f1ab5542"}, + {file = "debugpy-1.8.17-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:1440fd514e1b815edd5861ca394786f90eb24960eb26d6f7200994333b1d79e3"}, + {file = "debugpy-1.8.17-cp310-cp310-win32.whl", hash = "sha256:3a32c0af575749083d7492dc79f6ab69f21b2d2ad4cd977a958a07d5865316e4"}, + {file = "debugpy-1.8.17-cp310-cp310-win_amd64.whl", hash = "sha256:a3aad0537cf4d9c1996434be68c6c9a6d233ac6f76c2a482c7803295b4e4f99a"}, + {file = "debugpy-1.8.17-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:d3fce3f0e3de262a3b67e69916d001f3e767661c6e1ee42553009d445d1cd840"}, + {file = "debugpy-1.8.17-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:c6bdf134457ae0cac6fb68205776be635d31174eeac9541e1d0c062165c6461f"}, + {file = "debugpy-1.8.17-cp311-cp311-win32.whl", hash = "sha256:e79a195f9e059edfe5d8bf6f3749b2599452d3e9380484cd261f6b7cd2c7c4da"}, + {file = "debugpy-1.8.17-cp311-cp311-win_amd64.whl", hash = "sha256:b532282ad4eca958b1b2d7dbcb2b7218e02cb934165859b918e3b6ba7772d3f4"}, + {file = "debugpy-1.8.17-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:f14467edef672195c6f6b8e27ce5005313cb5d03c9239059bc7182b60c176e2d"}, + {file = "debugpy-1.8.17-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:24693179ef9dfa20dca8605905a42b392be56d410c333af82f1c5dff807a64cc"}, + {file = "debugpy-1.8.17-cp312-cp312-win32.whl", hash = "sha256:6a4e9dacf2cbb60d2514ff7b04b4534b0139facbf2abdffe0639ddb6088e59cf"}, + {file = "debugpy-1.8.17-cp312-cp312-win_amd64.whl", hash = "sha256:e8f8f61c518952fb15f74a302e068b48d9c4691768ade433e4adeea961993464"}, + {file = "debugpy-1.8.17-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:857c1dd5d70042502aef1c6d1c2801211f3ea7e56f75e9c335f434afb403e464"}, + {file = "debugpy-1.8.17-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:3bea3b0b12f3946e098cce9b43c3c46e317b567f79570c3f43f0b96d00788088"}, + {file = "debugpy-1.8.17-cp313-cp313-win32.whl", hash = "sha256:e34ee844c2f17b18556b5bbe59e1e2ff4e86a00282d2a46edab73fd7f18f4a83"}, + {file = "debugpy-1.8.17-cp313-cp313-win_amd64.whl", hash = "sha256:6c5cd6f009ad4fca8e33e5238210dc1e5f42db07d4b6ab21ac7ffa904a196420"}, + {file = "debugpy-1.8.17-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:045290c010bcd2d82bc97aa2daf6837443cd52f6328592698809b4549babcee1"}, + {file = "debugpy-1.8.17-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:b69b6bd9dba6a03632534cdf67c760625760a215ae289f7489a452af1031fe1f"}, + {file = "debugpy-1.8.17-cp314-cp314-win32.whl", hash = "sha256:5c59b74aa5630f3a5194467100c3b3d1c77898f9ab27e3f7dc5d40fc2f122670"}, + {file = "debugpy-1.8.17-cp314-cp314-win_amd64.whl", hash = "sha256:893cba7bb0f55161de4365584b025f7064e1f88913551bcd23be3260b231429c"}, + {file = "debugpy-1.8.17-cp38-cp38-macosx_15_0_x86_64.whl", hash = "sha256:8deb4e31cd575c9f9370042876e078ca118117c1b5e1f22c32befcfbb6955f0c"}, + {file = "debugpy-1.8.17-cp38-cp38-manylinux_2_34_x86_64.whl", hash = "sha256:b75868b675949a96ab51abc114c7163f40ff0d8f7d6d5fd63f8932fd38e9c6d7"}, + {file = "debugpy-1.8.17-cp38-cp38-win32.whl", hash = "sha256:17e456da14848d618662354e1dccfd5e5fb75deec3d1d48dc0aa0baacda55860"}, + {file = "debugpy-1.8.17-cp38-cp38-win_amd64.whl", hash = "sha256:e851beb536a427b5df8aa7d0c7835b29a13812f41e46292ff80b2ef77327355a"}, + {file = "debugpy-1.8.17-cp39-cp39-macosx_15_0_x86_64.whl", hash = "sha256:f2ac8055a0c4a09b30b931100996ba49ef334c6947e7ae365cdd870416d7513e"}, + {file = "debugpy-1.8.17-cp39-cp39-manylinux_2_34_x86_64.whl", hash = "sha256:eaa85bce251feca8e4c87ce3b954aba84b8c645b90f0e6a515c00394a9f5c0e7"}, + {file = "debugpy-1.8.17-cp39-cp39-win32.whl", hash = "sha256:b13eea5587e44f27f6c48588b5ad56dcb74a4f3a5f89250443c94587f3eb2ea1"}, + {file = "debugpy-1.8.17-cp39-cp39-win_amd64.whl", hash = "sha256:bb1bbf92317e1f35afcf3ef0450219efb3afe00be79d8664b250ac0933b9015f"}, + {file = "debugpy-1.8.17-py2.py3-none-any.whl", hash = "sha256:60c7dca6571efe660ccb7a9508d73ca14b8796c4ed484c2002abba714226cfef"}, + {file = "debugpy-1.8.17.tar.gz", hash = "sha256:fd723b47a8c08892b1a16b2c6239a8b96637c62a59b94bb5dab4bac592a58a8e"}, +] + [[package]] name = "distlib" version = "0.4.0" @@ -1717,4 +1757,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = "^3.11" -content-hash = "b2d90bacf4735caaaaae6b09a2b148a40e2fcc81ca6a2175e80b9a57c154d16f" +content-hash = "be493ece87263513a822717b26db5cffee5e818a75f7d1c5ab001cb362f30876" diff --git a/pyproject.toml b/pyproject.toml index 6f00608..af3a2ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ isort = ">=5.13.2,<8.0.0" pytest-aiohttp = "^1.0.5" pytest-asyncio = ">=0.25,<1.3" pre-commit = "^4.0.0" +debugpy = "^1.8.17" [build-system] requires = ["poetry-core"] diff --git a/reviewtally/main.py b/reviewtally/main.py index b9ff8f0..c1a6da1 100644 --- a/reviewtally/main.py +++ b/reviewtally/main.py @@ -4,6 +4,7 @@ import time from typing import Any +import requests from tqdm import tqdm from reviewtally.analysis.sprint_periods import calculate_sprint_periods @@ -20,7 +21,12 @@ from reviewtally.exporters.sprint_export import export_sprint_csv from reviewtally.metrics_calculation import calculate_reviewer_metrics from reviewtally.output_formatting import generate_results_table -from reviewtally.queries import set_github_host +from reviewtally.queries import ( + GENERAL_TIMEOUT, + build_github_rest_api_url, + require_github_token, + set_github_host, +) from reviewtally.queries.get_repos_gql import get_repos from reviewtally.visualization.individual_plot import ( SUPPORTED_INDIVIDUAL_METRICS, @@ -61,6 +67,33 @@ def main() -> None: f"{time.time() - start_time:.2f} seconds" ) timestamped_print(configured_message) + token = require_github_token() + headers = { + "Authorization": f"Bearer {token}", + "Accept": "application/vnd.github.v3+json", + } + for target in repo_targets: + url = build_github_rest_api_url( + f"repos/{target.owner}/{target.name}", + ) + response = requests.get( + url, + headers=headers, + timeout=GENERAL_TIMEOUT, + ) + try: + response.raise_for_status() + except requests.exceptions.HTTPError as e: + status = ( + getattr(e.response, "status_code", None) + or response.status_code + ) + print( # noqa: T201 + f"Error: repository {target.owner}/{target.name}" + " not found ", + f"(HTTP {status})", + ) + sys.exit(1) else: request_message = ( "Calling get_repos_by_language " @@ -94,7 +127,8 @@ def _handle_sprint_analysis( ) -> None: """Handle sprint analysis mode.""" sprint_periods = calculate_sprint_periods( - args["start_date"], args["end_date"], + args["start_date"], + args["end_date"], ) sprint_stats: dict[str, dict[str, Any]] = {} diff --git a/reviewtally/queries/__init__.py b/reviewtally/queries/__init__.py index b45dfa2..f04e23a 100644 --- a/reviewtally/queries/__init__.py +++ b/reviewtally/queries/__init__.py @@ -34,7 +34,6 @@ # HTTP status codes that should trigger retries RETRYABLE_STATUS_CODES = { - 429, # Too Many Requests (rate limiting) 500, # Internal Server Error 502, # Bad Gateway 503, # Service Unavailable diff --git a/reviewtally/queries/get_prs.py b/reviewtally/queries/get_prs.py index 9d60789..59d0736 100644 --- a/reviewtally/queries/get_prs.py +++ b/reviewtally/queries/get_prs.py @@ -95,13 +95,28 @@ def _make_pr_request_with_retry( # Handle rate limiting (existing logic) backoff_if_ratelimited(response.headers) - response.raise_for_status() + try: + response.raise_for_status() + except requests.exceptions.HTTPError as e: + status = ( + getattr(e.response, "status_code", None) + or response.status_code + ) + # Fail fast on non-retryable HTTP errors (e.g. 404/422) + if status not in RETRYABLE_STATUS_CODES: + raise + # Retry on retryable HTTP errors if attempts remain + if attempt < MAX_RETRIES: + _backoff_delay(attempt) + continue + # No attempts left; re-raise + raise return response.json() except ( - requests.exceptions.RequestException, requests.exceptions.Timeout, + requests.exceptions.ConnectionError, ): if attempt < MAX_RETRIES: _backoff_delay(attempt) @@ -240,8 +255,7 @@ def get_pull_requests_between_dates( # Fetch forward if needed and end is after cached data if needs_forward and end_date > cached_max: print( # noqa: T201 - f"Forward fetch: {cached_max.date()} to " - f"{end_date.date()}", + f"Forward fetch: {cached_max.date()} to {end_date.date()}", ) forward_prs, boundary = fetch_pull_requests_from_github( owner,