diff --git a/Dockerfile b/Dockerfile index 5608295..f91ed8b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Use the latest stable Python image -FROM python:3.11-slim +FROM python:3.14-slim # Set environment variables ENV PYTHONDONTWRITEBYTECODE=1 \ @@ -34,4 +34,4 @@ RUN chown -R app:app /app USER app # Set the default command -CMD ["python", "main.py"] \ No newline at end of file +CMD ["python", "main.py"] diff --git a/README.md b/README.md index 80a3afe..58780dc 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ runs in a Docker container for easy deployment and isolation. ### Prerequisites -1. **GitHub Personal Access Token**: Create a [token](https://github.com/settings/tokens) +1. **GitHub App** *(recommended, required for authenticated runs)*: Create a GitHub App with read access to the target repositories, then note the numeric **App ID** and download a **private key** (PEM format). Without these the ETL runs unauthenticated (low rate-limit quota — suitable for testing only). 2. **Google Cloud Project**: Set up a GCP project with BigQuery enabled 3. **BigQuery Dataset**: Create a dataset in your GCP project 4. **Authentication**: Configure GCP credentials (see Authentication section below) @@ -35,23 +35,40 @@ docker build -t github-etl . ### Running the Container +Create an env file (do **not** commit it): + +```bash +# github-etl.env +GITHUB_REPOS=mozilla-firefox/firefox +GITHUB_APP_ID=your_github_app_id +GITHUB_PRIVATE_KEY= +BIGQUERY_PROJECT=your-gcp-project +BIGQUERY_DATASET=your_dataset +GOOGLE_APPLICATION_CREDENTIALS=/path/to/credentials.json +``` + +Then run the container using `--env-file` to avoid exposing secrets in shell history +or via `/proc//environ`: + ```bash docker run --rm \ - -e GITHUB_REPOS="mozilla/firefox" \ - -e GITHUB_TOKEN="your_github_token" \ - -e BIGQUERY_PROJECT="your-gcp-project" \ - -e BIGQUERY_DATASET="your_dataset" \ - -e GOOGLE_APPLICATION_CREDENTIALS="/path/to/credentials.json" \ + --env-file github-etl.env \ -v /local/path/to/credentials.json:/path/to/credentials.json \ github-etl ``` +> **Note**: Never pass the private key inline with `-e GITHUB_PRIVATE_KEY="$(cat ...)"` — +> that leaks the key into your shell history and makes it visible to other processes via +> `ps`/`/proc`. Use `--env-file`, Docker secrets, or a secret manager that injects +> `GITHUB_PRIVATE_KEY` as an environment variable instead. + ### Environment Variables | Variable | Required | Default | Description | |----------|----------|---------|-------------| | `GITHUB_REPOS` | Yes | - | Comma separated repositories in format "owner/repo" (e.g., "mozilla/firefox") | -| `GITHUB_TOKEN` | No | - | GitHub Personal Access Token (recommended to avoid rate limits) | +| `GITHUB_APP_ID` | No* | - | GitHub App numeric ID (found on the App's settings page). Required for authenticated access. | +| `GITHUB_PRIVATE_KEY` | No* | - | RSA private key in PEM format for the GitHub App. Required for authenticated access. | | `BIGQUERY_PROJECT` | Yes | - | Google Cloud Project ID | | `BIGQUERY_DATASET` | Yes | - | BigQuery dataset ID | | `GOOGLE_APPLICATION_CREDENTIALS` | Yes* | - | Path to GCP service account JSON file (*or use Workload Identity) | @@ -66,7 +83,7 @@ docker run --rm \ ### Container Specifications -- **Base Image**: `python:3.11-slim` (latest stable Python) +- **Base Image**: `python:3.14-slim` (latest stable Python) - **User**: `app` (uid: 1000, gid: 1000) - **Working Directory**: `/app` - **Ownership**: All files in `/app` are owned by the `app` user @@ -128,7 +145,9 @@ Set up environment variables and run the script: ```bash export GITHUB_REPOS="mozilla/firefox" -export GITHUB_TOKEN="your_github_token" +export GITHUB_APP_ID="your_github_app_id" +# Load the PEM from a file to avoid the key appearing in shell history +export GITHUB_PRIVATE_KEY="$(< your_private_key.pem)" export BIGQUERY_PROJECT="your-gcp-project" export BIGQUERY_DATASET="your_dataset" diff --git a/main.py b/main.py index 2bc4f49..3ab1ca0 100755 --- a/main.py +++ b/main.py @@ -13,9 +13,10 @@ import time from dataclasses import dataclass from datetime import datetime, timedelta, timezone -from typing import Iterator, Optional +from typing import Callable, Iterator, Optional from urllib.parse import parse_qs, urlparse +import jwt import requests from google.api_core import exceptions as api_exceptions from google.api_core.client_options import ClientOptions @@ -37,22 +38,46 @@ class AccessToken: repo_installation_cache: dict[str, int] = {} +def generate_github_jwt(app_id: str, private_key_pem: str) -> str: + """ + Generate a short-lived GitHub App JWT signed with the app's private key. + + GitHub App JWTs are valid for a maximum of 10 minutes. We use a 9-minute + expiry and backdate iat by 60 seconds to absorb clock skew between the + local machine and GitHub's servers. + + Args: + app_id: GitHub App ID (numeric, found on the App's settings page) + private_key_pem: RSA private key in PEM format + + Returns: + Signed JWT string + """ + now = int(time.time()) + payload = { + "iat": now - 60, # backdate 60s to absorb clock skew + "exp": now + 540, # 9 minutes (GitHub maximum is 10) + "iss": app_id, + } + return jwt.encode(payload, private_key_pem, algorithm="RS256") + + def get_installation_access_token( - jwt: str, + app_jwt: str, repo: str, github_api_url: str, ) -> str: """ Get a GitHub App installation access token, returning a cached one if still valid. - Uses the JWT to look up the installation for the given repo, then exchanges - it for an installation access token (valid for 1 hour). Tokens are cached - per installation ID so that repos sharing an installation reuse the same token, - while repos on different installations each get their own. The repo->installation - ID mapping is also cached since it never changes. + Uses the JWT (generated by ``generate_github_jwt()``) to look up the installation + for the given repo, then exchanges it for an installation access token (valid for + 1 hour). Tokens are cached per installation ID so that repos sharing an installation + reuse the same token, while repos on different installations each get their own. + The repo->installation ID mapping is also cached since it never changes. Args: - jwt: GitHub App JWT (stored in GITHUB_TOKEN env var) + app_jwt: Short-lived GitHub App JWT produced by ``generate_github_jwt()`` repo: Repository in "owner/repo" format, used to look up the installation github_api_url: GitHub API base URL @@ -63,7 +88,7 @@ def get_installation_access_token( session = requests.Session() session.headers.update( { - "Authorization": f"Bearer {jwt}", + "Authorization": f"Bearer {app_jwt}", "Accept": "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28", } @@ -154,6 +179,7 @@ def extract_pull_requests( repo: str, chunk_size: int = 100, github_api_url: str = "https://api.github.com", + refresh_auth: Optional[Callable[[], None]] = None, ) -> Iterator[list[dict]]: """ Extract data from GitHub repositories in chunks. @@ -165,6 +191,9 @@ def extract_pull_requests( repo: GitHub repository name chunk_size: Number of PRs to yield per chunk (default: 100) github_api_url: GitHub API base URL + refresh_auth: Optional callable invoked before each page fetch to refresh + the session's Authorization header. Use this to prevent installation + tokens (1-hour TTL) from expiring mid-extraction on large repos. Yields: List of pull request dictionaries (up to chunk_size items) @@ -183,6 +212,8 @@ def extract_pull_requests( pages = 0 while True: + if refresh_auth: + refresh_auth() resp = github_get(session, base_url, params=params) batch = resp.json() @@ -680,11 +711,12 @@ def main() -> int: def _main() -> int: logger.info("Starting GitHub ETL process with chunked processing") - github_jwt = os.environ.get("GITHUB_TOKEN") or None - if not github_jwt: + github_app_id = os.environ.get("GITHUB_APP_ID") or None + github_private_key = os.environ.get("GITHUB_PRIVATE_KEY") or None + if not github_app_id or not github_private_key: logger.warning( - "GITHUB_TOKEN (expected to be a GitHub App JWT, not a personal access token) " - "is not set; proceeding without authentication (suitable for testing only)" + "GITHUB_APP_ID and GITHUB_PRIVATE_KEY are not set; " + "proceeding without authentication (suitable for testing only)" ) # Read BigQuery configuration @@ -748,16 +780,43 @@ def _main() -> int: bigquery_client, bigquery_dataset, repo, snapshot_date ) - # Get (or refresh) the installation access token before processing each repo - if github_jwt: - access_token = get_installation_access_token( - github_jwt, repo, github_api_url - ) - session.headers["Authorization"] = f"Bearer {access_token}" + # Build a per-repo token refresh callable. It is called by the generator + # before each page fetch, so every API request (PRs + commits + reviewers + + # comments) uses a valid token. The access_token_cache means this only hits + # the GitHub API when the cached token has <60 seconds remaining. + refresh_auth: Optional[Callable[[], None]] = None + if github_app_id and github_private_key: + + def _make_refresh( + _repo: str = repo, + ) -> Callable[[], None]: + def _refresh() -> None: + try: + app_jwt = generate_github_jwt(github_app_id, github_private_key) + access_token = get_installation_access_token( + app_jwt, _repo, github_api_url + ) + except Exception as e: + raise RuntimeError( + f"Failed to obtain GitHub App access token for {_repo}: {e}. " + "Check that GITHUB_APP_ID is correct and GITHUB_PRIVATE_KEY " + "is a valid PEM-encoded RSA private key." + ) from e + session.headers["Authorization"] = f"Bearer {access_token}" + + return _refresh + + refresh_auth = _make_refresh() + # Set the token immediately so the first generator page is authenticated. + refresh_auth() for chunk_count, chunk in enumerate( extract_pull_requests( - session, repo, chunk_size=100, github_api_url=github_api_url + session, + repo, + chunk_size=100, + github_api_url=github_api_url, + refresh_auth=refresh_auth, ), start=1, ): diff --git a/pyproject.toml b/pyproject.toml index f4aac49..5119f06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ classifiers = [ dependencies = [ "requests>=2.25.0", "google-cloud-bigquery==3.25.0", + "PyJWT[crypto]>=2.0.0", ] [project.optional-dependencies] diff --git a/requirements.txt b/requirements.txt index fd521f6..64dbf53 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,6 +8,92 @@ certifi==2026.1.4 \ --hash=sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c \ --hash=sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120 # via requests +cffi==2.0.0 \ + --hash=sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb \ + --hash=sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b \ + --hash=sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f \ + --hash=sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9 \ + --hash=sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44 \ + --hash=sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2 \ + --hash=sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c \ + --hash=sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75 \ + --hash=sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65 \ + --hash=sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e \ + --hash=sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a \ + --hash=sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e \ + --hash=sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25 \ + --hash=sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a \ + --hash=sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe \ + --hash=sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b \ + --hash=sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91 \ + --hash=sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592 \ + --hash=sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187 \ + --hash=sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c \ + --hash=sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1 \ + --hash=sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94 \ + --hash=sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba \ + --hash=sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb \ + --hash=sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165 \ + --hash=sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529 \ + --hash=sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca \ + --hash=sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c \ + --hash=sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6 \ + --hash=sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c \ + --hash=sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0 \ + --hash=sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743 \ + --hash=sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63 \ + --hash=sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5 \ + --hash=sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5 \ + --hash=sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4 \ + --hash=sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d \ + --hash=sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b \ + --hash=sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93 \ + --hash=sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205 \ + --hash=sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27 \ + --hash=sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512 \ + --hash=sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d \ + --hash=sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c \ + --hash=sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037 \ + --hash=sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26 \ + --hash=sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322 \ + --hash=sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb \ + --hash=sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c \ + --hash=sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8 \ + --hash=sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4 \ + --hash=sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414 \ + --hash=sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9 \ + --hash=sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664 \ + --hash=sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9 \ + --hash=sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775 \ + --hash=sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739 \ + --hash=sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc \ + --hash=sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062 \ + --hash=sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe \ + --hash=sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9 \ + --hash=sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92 \ + --hash=sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5 \ + --hash=sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13 \ + --hash=sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d \ + --hash=sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26 \ + --hash=sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f \ + --hash=sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495 \ + --hash=sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b \ + --hash=sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6 \ + --hash=sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c \ + --hash=sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef \ + --hash=sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5 \ + --hash=sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18 \ + --hash=sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad \ + --hash=sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3 \ + --hash=sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7 \ + --hash=sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5 \ + --hash=sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534 \ + --hash=sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49 \ + --hash=sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2 \ + --hash=sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5 \ + --hash=sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453 \ + --hash=sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf + # via cryptography charset-normalizer==3.4.4 \ --hash=sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad \ --hash=sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93 \ @@ -123,6 +209,57 @@ charset-normalizer==3.4.4 \ --hash=sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e \ --hash=sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608 # via requests +cryptography==46.0.5 \ + --hash=sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72 \ + --hash=sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235 \ + --hash=sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9 \ + --hash=sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356 \ + --hash=sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257 \ + --hash=sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad \ + --hash=sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4 \ + --hash=sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c \ + --hash=sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614 \ + --hash=sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed \ + --hash=sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31 \ + --hash=sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229 \ + --hash=sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0 \ + --hash=sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731 \ + --hash=sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b \ + --hash=sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4 \ + --hash=sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4 \ + --hash=sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263 \ + --hash=sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595 \ + --hash=sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1 \ + --hash=sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678 \ + --hash=sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48 \ + --hash=sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76 \ + --hash=sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0 \ + --hash=sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18 \ + --hash=sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d \ + --hash=sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d \ + --hash=sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1 \ + --hash=sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981 \ + --hash=sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7 \ + --hash=sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82 \ + --hash=sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2 \ + --hash=sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4 \ + --hash=sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663 \ + --hash=sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c \ + --hash=sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d \ + --hash=sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a \ + --hash=sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a \ + --hash=sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d \ + --hash=sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b \ + --hash=sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a \ + --hash=sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826 \ + --hash=sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee \ + --hash=sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9 \ + --hash=sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648 \ + --hash=sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da \ + --hash=sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2 \ + --hash=sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2 \ + --hash=sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87 + # via pyjwt google-api-core[grpc]==2.29.0 \ --hash=sha256:84181be0f8e6b04006df75ddfe728f24489f0af57c96a529ff7cf45bc28797f7 \ --hash=sha256:d30bc60980daa36e314b5d5a3e5958b0200cb44ca8fa1be2b614e932b75a3ea9 @@ -296,6 +433,16 @@ pyasn1-modules==0.4.2 \ --hash=sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a \ --hash=sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6 # via google-auth +pycparser==3.0 \ + --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ + --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 + # via cffi +pyjwt[crypto]==2.12.1 \ + --hash=sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c \ + --hash=sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b + # via + # github-etl (pyproject.toml) + # pyjwt python-dateutil==2.9.0.post0 \ --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427