airbyte-source-github 1.9.0rc2__py3-none-any.whl → 1.9.1rc1__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.
Potentially problematic release.
This version of airbyte-source-github might be problematic. Click here for more details.
- {airbyte_source_github-1.9.0rc2.dist-info → airbyte_source_github-1.9.1rc1.dist-info}/METADATA +2 -2
- {airbyte_source_github-1.9.0rc2.dist-info → airbyte_source_github-1.9.1rc1.dist-info}/RECORD +6 -6
- source_github/source.py +5 -1
- source_github/utils.py +41 -10
- {airbyte_source_github-1.9.0rc2.dist-info → airbyte_source_github-1.9.1rc1.dist-info}/WHEEL +0 -0
- {airbyte_source_github-1.9.0rc2.dist-info → airbyte_source_github-1.9.1rc1.dist-info}/entry_points.txt +0 -0
{airbyte_source_github-1.9.0rc2.dist-info → airbyte_source_github-1.9.1rc1.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: airbyte-source-github
|
|
3
|
-
Version: 1.9.
|
|
3
|
+
Version: 1.9.1rc1
|
|
4
4
|
Summary: Source implementation for GitHub.
|
|
5
5
|
Home-page: https://airbyte.com
|
|
6
6
|
License: ELv2
|
|
@@ -11,7 +11,7 @@ Classifier: License :: Other/Proprietary License
|
|
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.10
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
-
Requires-Dist: airbyte-cdk (>=7,<8)
|
|
14
|
+
Requires-Dist: airbyte-cdk (>=7.3.4,<8.0.0)
|
|
15
15
|
Requires-Dist: sgqlc (==16.3)
|
|
16
16
|
Project-URL: Documentation, https://docs.airbyte.com/integrations/sources/github
|
|
17
17
|
Project-URL: Repository, https://github.com/airbytehq/airbyte
|
{airbyte_source_github-1.9.0rc2.dist-info → airbyte_source_github-1.9.1rc1.dist-info}/RECORD
RENAMED
|
@@ -54,11 +54,11 @@ source_github/schemas/users.json,sha256=by1CEx3MW740S5F54KZfUDc2N1_UZ98Bb3fWlgCt
|
|
|
54
54
|
source_github/schemas/workflow_jobs.json,sha256=Kk3N3FcCYBqOdR7FYvz2nAAFtLZBuVfR8Lxg-ORD39U,3934
|
|
55
55
|
source_github/schemas/workflow_runs.json,sha256=XDmIsjtzka-ItEonImD3ZATZjxRNkbFo5-MPTtZDARA,19453
|
|
56
56
|
source_github/schemas/workflows.json,sha256=gSNw8WZaVKbX4AL97PbjZHzvxcOltXqv9Ao1RNQOFXM,1470
|
|
57
|
-
source_github/source.py,sha256=
|
|
57
|
+
source_github/source.py,sha256=rY85d7X_StM0_GYIRYwnvoGQcsbNA_ZtdRnNJdh9-lk,14588
|
|
58
58
|
source_github/spec.json,sha256=7LOQm01fP_RvPF-HifhNPJ7i0AxT2LTNPaLAA3uOfNY,7443
|
|
59
59
|
source_github/streams.py,sha256=uvCHATVyObHCz76tuknNqPETuGlyUdXGsa1YZc7B4JI,78285
|
|
60
|
-
source_github/utils.py,sha256=
|
|
61
|
-
airbyte_source_github-1.9.
|
|
62
|
-
airbyte_source_github-1.9.
|
|
63
|
-
airbyte_source_github-1.9.
|
|
64
|
-
airbyte_source_github-1.9.
|
|
60
|
+
source_github/utils.py,sha256=1mKZzLEaDe0m8BTw8Y-MxCXrJr1bZfaLPeSUbPN3vyQ,7243
|
|
61
|
+
airbyte_source_github-1.9.1rc1.dist-info/METADATA,sha256=AMbb8fqUONHqjtukcD-PcF9ky8ByW4C4IU-zobIUg2E,5199
|
|
62
|
+
airbyte_source_github-1.9.1rc1.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
|
63
|
+
airbyte_source_github-1.9.1rc1.dist-info/entry_points.txt,sha256=gYhqVrTAZvMwuYByg0b_-o115yUFLLcfNxMrLZmiW9k,55
|
|
64
|
+
airbyte_source_github-1.9.1rc1.dist-info/RECORD,,
|
source_github/source.py
CHANGED
|
@@ -9,6 +9,7 @@ from urllib.parse import urlparse
|
|
|
9
9
|
from airbyte_cdk.models import FailureType
|
|
10
10
|
from airbyte_cdk.sources import AbstractSource
|
|
11
11
|
from airbyte_cdk.sources.streams import Stream
|
|
12
|
+
from airbyte_cdk.sources.streams.http.http_client import MessageRepresentationAirbyteTracedErrors
|
|
12
13
|
from airbyte_cdk.sources.streams.http.requests_native_auth import MultipleTokenAuthenticator
|
|
13
14
|
from airbyte_cdk.utils.traced_exception import AirbyteTracedException
|
|
14
15
|
from source_github.utils import MultipleTokenAuthenticatorWithRateLimiter
|
|
@@ -184,7 +185,7 @@ class SourceGithub(AbstractSource):
|
|
|
184
185
|
# 404 Client Error: Not Found for url: https://api.github.com/orgs/airbytehqBLA/repos?per_page=100
|
|
185
186
|
org_name = message.split("https://api.github.com/orgs/")[1].split("/")[0]
|
|
186
187
|
user_message = f'Organization name: "{org_name}" is unknown, "repository" config option should be updated. Please validate your repository config.'
|
|
187
|
-
elif "401 Client Error: Unauthorized for url" in message:
|
|
188
|
+
elif "401 Client Error: Unauthorized for url" in message or ("Error: Unauthorized" in message and "401" in message):
|
|
188
189
|
# 401 Client Error: Unauthorized for url: https://api.github.com/orgs/datarootsio/repos?per_page=100&sort=updated&direction=desc
|
|
189
190
|
user_message = (
|
|
190
191
|
"Github credentials have expired or changed, please review your credentials and re-authenticate or renew your access token."
|
|
@@ -203,6 +204,9 @@ class SourceGithub(AbstractSource):
|
|
|
203
204
|
)
|
|
204
205
|
return True, None
|
|
205
206
|
|
|
207
|
+
except MessageRepresentationAirbyteTracedErrors as e:
|
|
208
|
+
user_message = self.user_friendly_error_message(e.message)
|
|
209
|
+
return False, user_message or e.message
|
|
206
210
|
except Exception as e:
|
|
207
211
|
message = repr(e)
|
|
208
212
|
user_message = self.user_friendly_error_message(message)
|
source_github/utils.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#
|
|
2
2
|
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
|
|
3
3
|
#
|
|
4
|
-
|
|
4
|
+
import logging
|
|
5
5
|
import time
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from datetime import timedelta
|
|
@@ -10,10 +10,12 @@ from typing import Any, List, Mapping
|
|
|
10
10
|
|
|
11
11
|
import requests
|
|
12
12
|
|
|
13
|
-
from airbyte_cdk.models import SyncMode
|
|
13
|
+
from airbyte_cdk.models import FailureType, SyncMode
|
|
14
14
|
from airbyte_cdk.sources.streams import Stream
|
|
15
|
+
from airbyte_cdk.sources.streams.http import HttpClient
|
|
15
16
|
from airbyte_cdk.sources.streams.http.requests_native_auth import TokenAuthenticator
|
|
16
17
|
from airbyte_cdk.sources.streams.http.requests_native_auth.abstract_token import AbstractHeaderAuthenticator
|
|
18
|
+
from airbyte_cdk.utils import AirbyteTracedException
|
|
17
19
|
from airbyte_cdk.utils.datetime_helpers import AirbyteDateTime, ab_datetime_now, ab_datetime_parse
|
|
18
20
|
|
|
19
21
|
|
|
@@ -59,14 +61,30 @@ class MultipleTokenAuthenticatorWithRateLimiter(AbstractHeaderAuthenticator):
|
|
|
59
61
|
DURATION = timedelta(seconds=3600) # Duration at which the current rate limit window resets
|
|
60
62
|
|
|
61
63
|
def __init__(self, tokens: List[str], auth_method: str = "token", auth_header: str = "Authorization"):
|
|
64
|
+
self._logger = logging.getLogger("airbyte")
|
|
62
65
|
self._auth_method = auth_method
|
|
63
66
|
self._auth_header = auth_header
|
|
64
67
|
self._tokens = {t: Token() for t in tokens}
|
|
68
|
+
# It would've been nice to instantiate a single client on this authenticator. However, we are checking
|
|
69
|
+
# the limits of each token which is associated with a TokenAuthenticator. And each HttpClient can only
|
|
70
|
+
# correspond to one authenticator.
|
|
71
|
+
self._token_to_http_client: Mapping[str, HttpClient] = self._initialize_http_clients(tokens)
|
|
65
72
|
self.check_all_tokens()
|
|
66
73
|
self._tokens_iter = cycle(self._tokens)
|
|
67
74
|
self._active_token = next(self._tokens_iter)
|
|
68
75
|
self._max_time = 60 * 10 # 10 minutes as default
|
|
69
76
|
|
|
77
|
+
def _initialize_http_clients(self, tokens: List[str]) -> Mapping[str, HttpClient]:
|
|
78
|
+
return {
|
|
79
|
+
token: HttpClient(
|
|
80
|
+
name="token_validator",
|
|
81
|
+
logger=self._logger,
|
|
82
|
+
authenticator=TokenAuthenticator(token, auth_method=self._auth_method),
|
|
83
|
+
use_cache=False, # We don't want to reuse cached valued because rate limit values change frequently
|
|
84
|
+
)
|
|
85
|
+
for token in tokens
|
|
86
|
+
}
|
|
87
|
+
|
|
70
88
|
@property
|
|
71
89
|
def auth_header(self) -> str:
|
|
72
90
|
return self._auth_header
|
|
@@ -114,14 +132,27 @@ class MultipleTokenAuthenticatorWithRateLimiter(AbstractHeaderAuthenticator):
|
|
|
114
132
|
|
|
115
133
|
def _check_token_limits(self, token: str):
|
|
116
134
|
"""check that token is not limited"""
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
135
|
+
|
|
136
|
+
http_client = self._token_to_http_client.get(token)
|
|
137
|
+
if not http_client:
|
|
138
|
+
raise ValueError("No HttpClient was initialized for this token. This is unexpected. Please contact Airbyte support.")
|
|
139
|
+
|
|
140
|
+
_, response = http_client.send_request(
|
|
141
|
+
http_method="GET",
|
|
142
|
+
url="https://api.github.com/rate_limit",
|
|
143
|
+
headers={"Accept": "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28"},
|
|
144
|
+
request_kwargs={},
|
|
124
145
|
)
|
|
146
|
+
|
|
147
|
+
response_body = response.json()
|
|
148
|
+
if "resources" not in response_body:
|
|
149
|
+
raise AirbyteTracedException(
|
|
150
|
+
failure_type=FailureType.config_error,
|
|
151
|
+
internal_message=f"Token rate limit info response did not contain expected key: resources",
|
|
152
|
+
message="Unable to validate token. Please double check that specified authentication tokens are correct",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
rate_limit_info = response_body.get("resources")
|
|
125
156
|
token_info = self._tokens[token]
|
|
126
157
|
remaining_info_core = rate_limit_info.get("core")
|
|
127
158
|
token_info.count_rest, token_info.reset_at_rest = (
|
|
@@ -144,7 +175,7 @@ class MultipleTokenAuthenticatorWithRateLimiter(AbstractHeaderAuthenticator):
|
|
|
144
175
|
setattr(current_token, count_attr, getattr(current_token, count_attr) - 1)
|
|
145
176
|
return True
|
|
146
177
|
elif all(getattr(x, count_attr) == 0 for x in self._tokens.values()):
|
|
147
|
-
min_time_to_wait = min((getattr(x, reset_attr) - ab_datetime_now()).
|
|
178
|
+
min_time_to_wait = min((getattr(x, reset_attr) - ab_datetime_now()).total_seconds() for x in self._tokens.values())
|
|
148
179
|
if min_time_to_wait < self.max_time:
|
|
149
180
|
time.sleep(min_time_to_wait if min_time_to_wait > 0 else 0)
|
|
150
181
|
self.check_all_tokens()
|
|
File without changes
|
|
File without changes
|