airbyte-source-github 2.2.0.dev202601122028__tar.gz → 2.2.1__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.
- airbyte_source_github-2.2.1/.airbyte-pypi-readme.md +7 -0
- airbyte_source_github-2.2.1/PKG-INFO +29 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/pyproject.toml +3 -3
- airbyte_source_github-2.2.1/source_github/backoff_strategies.py +61 -0
- airbyte_source_github-2.2.1/source_github/errors_handlers.py +244 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/graphql.py +48 -0
- airbyte_source_github-2.2.1/source_github/manifest.yaml +1331 -0
- airbyte_source_github-2.2.1/source_github/run.py +63 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/releases.json +13 -24
- airbyte_source_github-2.2.1/source_github/source.py +437 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/streams.py +282 -164
- airbyte_source_github-2.2.1/source_github/utils.py +24 -0
- airbyte_source_github-2.2.0.dev202601122028/PKG-INFO +0 -126
- airbyte_source_github-2.2.0.dev202601122028/README.md +0 -104
- airbyte_source_github-2.2.0.dev202601122028/source_github/backoff_strategies.py +0 -52
- airbyte_source_github-2.2.0.dev202601122028/source_github/errors_handlers.py +0 -152
- airbyte_source_github-2.2.0.dev202601122028/source_github/run.py +0 -17
- airbyte_source_github-2.2.0.dev202601122028/source_github/schemas/repositories.json +0 -427
- airbyte_source_github-2.2.0.dev202601122028/source_github/source.py +0 -315
- airbyte_source_github-2.2.0.dev202601122028/source_github/spec.json +0 -190
- airbyte_source_github-2.2.0.dev202601122028/source_github/utils.py +0 -186
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/__init__.py +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/config_migrations.py +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/constants.py +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/github_schema.py +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/assignees.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/branches.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/collaborators.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/comments.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/commit_comment_reactions.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/commit_comments.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/commits.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/contributor_activity.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/deployments.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/events.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/issue_comment_reactions.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/issue_events.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/issue_labels.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/issue_milestones.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/issue_reactions.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/issue_timeline_events.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/issues.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/organizations.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/project_cards.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/project_columns.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/projects.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/projects_v2.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/pull_request_comment_reactions.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/pull_request_commits.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/pull_request_stats.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/pull_requests.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/review_comments.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/reviews.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/shared/events/comment.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/shared/events/commented.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/shared/events/committed.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/shared/events/cross_referenced.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/shared/events/reviewed.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/shared/reaction.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/shared/reactions.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/shared/user.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/shared/user_graphql.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/stargazers.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/tags.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/team_members.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/team_memberships.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/teams.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/users.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/workflow_jobs.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/workflow_runs.json +0 -0
- {airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/schemas/workflows.json +0 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# GitHub
|
|
2
|
+
|
|
3
|
+
This package contains the Airbyte GitHub connector.
|
|
4
|
+
|
|
5
|
+
Airbyte is open-source data movement for ELT pipelines and AI agents. It moves data from APIs, databases, and files to warehouses, lakes, and AI applications.
|
|
6
|
+
|
|
7
|
+
Airbyte provides a catalog of 600+ connectors for APIs, databases, data warehouses, data lakes, and AI applications. Use this package with [PyAirbyte](https://docs.airbyte.com/developers/pyairbyte) to run the connector from Python.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: airbyte-source-github
|
|
3
|
+
Version: 2.2.1
|
|
4
|
+
Summary: Source implementation for GitHub.
|
|
5
|
+
Home-page: https://airbyte.com
|
|
6
|
+
License: ELv2
|
|
7
|
+
Author: Airbyte
|
|
8
|
+
Author-email: contact@airbyte.io
|
|
9
|
+
Requires-Python: >=3.10,<3.14
|
|
10
|
+
Classifier: License :: Other/Proprietary License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Requires-Dist: airbyte-cdk (>=7.28.2,<8.0.0)
|
|
17
|
+
Requires-Dist: sgqlc (==16.3)
|
|
18
|
+
Project-URL: Documentation, https://docs.airbyte.com/integrations/sources/github
|
|
19
|
+
Project-URL: Repository, https://github.com/airbytehq/airbyte
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# GitHub
|
|
23
|
+
|
|
24
|
+
This package contains the Airbyte GitHub connector.
|
|
25
|
+
|
|
26
|
+
Airbyte is open-source data movement for ELT pipelines and AI agents. It moves data from APIs, databases, and files to warehouses, lakes, and AI applications.
|
|
27
|
+
|
|
28
|
+
Airbyte provides a catalog of 600+ connectors for APIs, databases, data warehouses, data lakes, and AI applications. Use this package with [PyAirbyte](https://docs.airbyte.com/developers/pyairbyte) to run the connector from Python.
|
|
29
|
+
|
|
@@ -3,12 +3,12 @@ requires = [ "poetry-core>=1.0.0",]
|
|
|
3
3
|
build-backend = "poetry.core.masonry.api"
|
|
4
4
|
|
|
5
5
|
[tool.poetry]
|
|
6
|
-
version = "2.2.
|
|
6
|
+
version = "2.2.1"
|
|
7
7
|
name = "airbyte-source-github"
|
|
8
8
|
description = "Source implementation for GitHub."
|
|
9
9
|
authors = [ "Airbyte <contact@airbyte.io>",]
|
|
10
10
|
license = "ELv2"
|
|
11
|
-
readme = "
|
|
11
|
+
readme = ".airbyte-pypi-readme.md"
|
|
12
12
|
documentation = "https://docs.airbyte.com/integrations/sources/github"
|
|
13
13
|
homepage = "https://airbyte.com"
|
|
14
14
|
repository = "https://github.com/airbytehq/airbyte"
|
|
@@ -17,7 +17,7 @@ include = "source_github"
|
|
|
17
17
|
|
|
18
18
|
[tool.poetry.dependencies]
|
|
19
19
|
python = "^3.10,<3.14"
|
|
20
|
-
airbyte-cdk = "^7.
|
|
20
|
+
airbyte-cdk = "^7.28.2"
|
|
21
21
|
sgqlc = "==16.3"
|
|
22
22
|
|
|
23
23
|
[tool.poetry.scripts]
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
|
|
3
|
+
#
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any, Optional, Union
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
from airbyte_cdk import BackoffStrategy
|
|
11
|
+
from airbyte_cdk.sources.streams.http import HttpStream
|
|
12
|
+
|
|
13
|
+
from .errors_handlers import is_rate_limited_response
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GithubStreamABCBackoffStrategy(BackoffStrategy):
|
|
17
|
+
def __init__(self, stream: HttpStream, max_wait_time_seconds: float = 120 * 60, **kwargs): # type: ignore # noqa
|
|
18
|
+
self.stream = stream
|
|
19
|
+
self.max_wait_time_seconds = max_wait_time_seconds
|
|
20
|
+
super().__init__(**kwargs)
|
|
21
|
+
|
|
22
|
+
def backoff_time(
|
|
23
|
+
self, response_or_exception: Optional[Union[requests.Response, requests.RequestException]], **kwargs: Any
|
|
24
|
+
) -> Optional[float]:
|
|
25
|
+
if isinstance(response_or_exception, requests.Response):
|
|
26
|
+
if not is_rate_limited_response(
|
|
27
|
+
response_or_exception,
|
|
28
|
+
self.stream.check_graphql_rate_limited,
|
|
29
|
+
self.stream.logger,
|
|
30
|
+
):
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
min_backoff_time = 60.0
|
|
34
|
+
retry_after = response_or_exception.headers.get("Retry-After")
|
|
35
|
+
if retry_after is not None:
|
|
36
|
+
wait_time = max(float(retry_after), min_backoff_time)
|
|
37
|
+
else:
|
|
38
|
+
reset_time = response_or_exception.headers.get("X-RateLimit-Reset")
|
|
39
|
+
if not reset_time:
|
|
40
|
+
return None
|
|
41
|
+
wait_time = max(float(reset_time) - time.time(), min_backoff_time)
|
|
42
|
+
|
|
43
|
+
if wait_time > self.max_wait_time_seconds:
|
|
44
|
+
self.stream.logger.warning(
|
|
45
|
+
"Rate limit wait for stream `%s` is %.2f seconds, exceeding the maximum of %.2f seconds; using default backoff.",
|
|
46
|
+
self.stream.name,
|
|
47
|
+
wait_time,
|
|
48
|
+
self.max_wait_time_seconds,
|
|
49
|
+
)
|
|
50
|
+
return None
|
|
51
|
+
return wait_time
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ContributorActivityBackoffStrategy(BackoffStrategy):
|
|
56
|
+
def backoff_time(
|
|
57
|
+
self, response_or_exception: Optional[Union[requests.Response, requests.RequestException]], **kwargs: Any
|
|
58
|
+
) -> Optional[float]:
|
|
59
|
+
if isinstance(response_or_exception, requests.Response) and response_or_exception.status_code == requests.codes.ACCEPTED:
|
|
60
|
+
return 90
|
|
61
|
+
return None
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
|
|
3
|
+
#
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Callable, Optional, Union
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
from airbyte_cdk.models import FailureType
|
|
11
|
+
from airbyte_cdk.sources.streams.http import HttpStream
|
|
12
|
+
from airbyte_cdk.sources.streams.http.error_handlers import ErrorHandler, ErrorResolution, HttpStatusErrorHandler, ResponseAction
|
|
13
|
+
from airbyte_cdk.sources.streams.http.error_handlers.default_error_mapping import DEFAULT_ERROR_MAPPING
|
|
14
|
+
|
|
15
|
+
from . import constants
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger("airbyte")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
GITHUB_DEFAULT_ERROR_MAPPING = DEFAULT_ERROR_MAPPING | {
|
|
22
|
+
401: ErrorResolution(
|
|
23
|
+
response_action=ResponseAction.RETRY,
|
|
24
|
+
failure_type=FailureType.config_error,
|
|
25
|
+
error_message="Conflict.",
|
|
26
|
+
),
|
|
27
|
+
403: ErrorResolution(
|
|
28
|
+
response_action=ResponseAction.FAIL,
|
|
29
|
+
failure_type=FailureType.config_error,
|
|
30
|
+
error_message=(
|
|
31
|
+
"GitHub denied access (HTTP 403). Your token may be missing required scopes "
|
|
32
|
+
"(this connector typically needs: repo, read:org, read:user, read:project, workflow), "
|
|
33
|
+
"or this organization may require SAML SSO authorization. "
|
|
34
|
+
"See https://docs.github.com/en/rest/using-the-rest-api/troubleshooting-the-rest-api"
|
|
35
|
+
),
|
|
36
|
+
),
|
|
37
|
+
404: ErrorResolution(
|
|
38
|
+
response_action=ResponseAction.RETRY,
|
|
39
|
+
failure_type=FailureType.config_error,
|
|
40
|
+
error_message="Conflict.",
|
|
41
|
+
),
|
|
42
|
+
409: ErrorResolution(
|
|
43
|
+
response_action=ResponseAction.RETRY,
|
|
44
|
+
failure_type=FailureType.config_error,
|
|
45
|
+
error_message="Conflict.",
|
|
46
|
+
),
|
|
47
|
+
410: ErrorResolution(
|
|
48
|
+
response_action=ResponseAction.FAIL,
|
|
49
|
+
failure_type=FailureType.config_error,
|
|
50
|
+
error_message=(
|
|
51
|
+
"GitHub returned 410 Gone for an unexpected reason. "
|
|
52
|
+
"The endpoint or API version may be deprecated. "
|
|
53
|
+
"Verify the connector version is current and the endpoint is still supported."
|
|
54
|
+
),
|
|
55
|
+
),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def is_conflict_with_empty_repository(response_or_exception: Optional[Union[requests.Response, Exception]] = None) -> bool:
|
|
60
|
+
if isinstance(response_or_exception, requests.Response) and response_or_exception.status_code == requests.codes.CONFLICT:
|
|
61
|
+
try:
|
|
62
|
+
response_data = response_or_exception.json()
|
|
63
|
+
except ValueError:
|
|
64
|
+
logger.warning(
|
|
65
|
+
"is_conflict_with_empty_repository received non-JSON 409 response (first 50 chars: %r).",
|
|
66
|
+
response_or_exception.text[:50],
|
|
67
|
+
)
|
|
68
|
+
return False
|
|
69
|
+
return response_data.get("message") == "Git Repository is empty."
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def is_gone_with_feature_disabled(response_or_exception: Optional[Union[requests.Response, Exception]] = None) -> bool:
|
|
74
|
+
if isinstance(response_or_exception, requests.Response) and response_or_exception.status_code == requests.codes.GONE:
|
|
75
|
+
try:
|
|
76
|
+
message = (response_or_exception.json().get("message") or "").lower()
|
|
77
|
+
except ValueError:
|
|
78
|
+
logger.warning(
|
|
79
|
+
"is_gone_with_feature_disabled received non-JSON 410 response (first 50 chars: %r).",
|
|
80
|
+
response_or_exception.text[:50],
|
|
81
|
+
)
|
|
82
|
+
return False
|
|
83
|
+
return "are disabled" in message or "is disabled" in message
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def is_rate_limited_response(
|
|
88
|
+
response: requests.Response, graphql_rate_limit_checker: Callable[[dict], bool], logger: logging.Logger
|
|
89
|
+
) -> bool:
|
|
90
|
+
# GitHub GraphQL resource limitations:
|
|
91
|
+
# https://docs.github.com/en/graphql/overview/resource-limitations
|
|
92
|
+
if response.headers.get("X-RateLimit-Resource") == "graphql":
|
|
93
|
+
try:
|
|
94
|
+
body = response.json()
|
|
95
|
+
except ValueError:
|
|
96
|
+
logger.warning(
|
|
97
|
+
"GraphQL rate-limit check received non-JSON response (HTTP %s, first 50 chars: %r).",
|
|
98
|
+
response.status_code,
|
|
99
|
+
response.text[:50],
|
|
100
|
+
)
|
|
101
|
+
graphql_rate_limited = False
|
|
102
|
+
else:
|
|
103
|
+
graphql_rate_limited = graphql_rate_limit_checker(body or {})
|
|
104
|
+
|
|
105
|
+
if graphql_rate_limited:
|
|
106
|
+
return True
|
|
107
|
+
|
|
108
|
+
# GitHub REST rate-limit HTTP headers:
|
|
109
|
+
# https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limit-http-headers
|
|
110
|
+
# GitHub secondary rate limits:
|
|
111
|
+
# https://docs.github.com/en/rest/overview/resources-in-the-rest-api#secondary-rate-limits
|
|
112
|
+
return (response.status_code != 200 and response.headers.get("X-RateLimit-Remaining") == "0") or "Retry-After" in response.headers
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class GithubStreamABCErrorHandler(HttpStatusErrorHandler):
|
|
116
|
+
def __init__(self, stream: HttpStream, **kwargs): # type: ignore # noqa
|
|
117
|
+
self.stream = stream
|
|
118
|
+
super().__init__(**kwargs)
|
|
119
|
+
|
|
120
|
+
def interpret_response(self, response_or_exception: Optional[Union[requests.Response, Exception]] = None) -> ErrorResolution:
|
|
121
|
+
if isinstance(response_or_exception, requests.Response):
|
|
122
|
+
retry_flag = is_rate_limited_response(
|
|
123
|
+
response_or_exception,
|
|
124
|
+
self.stream.check_graphql_rate_limited,
|
|
125
|
+
self._logger,
|
|
126
|
+
)
|
|
127
|
+
if retry_flag:
|
|
128
|
+
headers = [
|
|
129
|
+
"X-RateLimit-Resource",
|
|
130
|
+
"X-RateLimit-Remaining",
|
|
131
|
+
"X-RateLimit-Reset",
|
|
132
|
+
"X-RateLimit-Limit",
|
|
133
|
+
"X-RateLimit-Used",
|
|
134
|
+
"Retry-After",
|
|
135
|
+
]
|
|
136
|
+
string_headers = ", ".join(
|
|
137
|
+
[f"{h}: {response_or_exception.headers[h]}" for h in headers if h in response_or_exception.headers]
|
|
138
|
+
)
|
|
139
|
+
if string_headers:
|
|
140
|
+
string_headers = f"HTTP headers: {string_headers},"
|
|
141
|
+
|
|
142
|
+
self._logger.info(
|
|
143
|
+
f"Rate limit handling for stream `{self.stream.name}` for the response with {response_or_exception.status_code} status code, {string_headers} with message: {response_or_exception.text}"
|
|
144
|
+
)
|
|
145
|
+
return ErrorResolution(
|
|
146
|
+
response_action=ResponseAction.RATE_LIMITED,
|
|
147
|
+
failure_type=FailureType.transient_error,
|
|
148
|
+
error_message=(
|
|
149
|
+
f"GitHub rate limit hit for stream `{self.stream.name}` "
|
|
150
|
+
f"(HTTP {response_or_exception.status_code}). "
|
|
151
|
+
f"Waiting for the rate limit window to reset before retrying."
|
|
152
|
+
),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if is_conflict_with_empty_repository(response_or_exception=response_or_exception):
|
|
156
|
+
log_message = (
|
|
157
|
+
f"Skipping `{self.stream.name}` for this repository: GitHub returned 409 Conflict "
|
|
158
|
+
f"with message 'Git Repository is empty.' This means the repository has no commits."
|
|
159
|
+
)
|
|
160
|
+
return ErrorResolution(
|
|
161
|
+
response_action=ResponseAction.IGNORE,
|
|
162
|
+
failure_type=FailureType.config_error,
|
|
163
|
+
error_message=log_message,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
if is_gone_with_feature_disabled(response_or_exception=response_or_exception):
|
|
167
|
+
log_message = f"Skipping stream slice for '{response_or_exception.url}': {response_or_exception.json().get('message', 'Feature disabled')}."
|
|
168
|
+
return ErrorResolution(
|
|
169
|
+
response_action=ResponseAction.IGNORE,
|
|
170
|
+
failure_type=FailureType.config_error,
|
|
171
|
+
error_message=log_message,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
return super().interpret_response(response_or_exception)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class ContributorActivityErrorHandler(GithubStreamABCErrorHandler):
|
|
178
|
+
"""
|
|
179
|
+
This custom error handler is needed for streams based on repository statistics endpoints like ContributorActivity because
|
|
180
|
+
when requesting data that hasn't been cached yet when the request is made, you'll receive a 202 response. And these requests
|
|
181
|
+
need to retried to get the actual results.
|
|
182
|
+
|
|
183
|
+
See the docs for more info:
|
|
184
|
+
https://docs.github.com/en/rest/metrics/statistics?apiVersion=2022-11-28#a-word-about-caching
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
def interpret_response(self, response_or_exception: Optional[Union[requests.Response, Exception]] = None) -> ErrorResolution:
|
|
188
|
+
if isinstance(response_or_exception, requests.Response) and response_or_exception.status_code == requests.codes.ACCEPTED:
|
|
189
|
+
return ErrorResolution(
|
|
190
|
+
response_action=ResponseAction.RETRY,
|
|
191
|
+
failure_type=FailureType.transient_error,
|
|
192
|
+
error_message=f"Response status code: {response_or_exception.status_code}. Retrying...",
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
return super().interpret_response(response_or_exception)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class GitHubGraphQLErrorHandler(GithubStreamABCErrorHandler):
|
|
199
|
+
def _safe_json_get_errors(self, response: requests.Response) -> bool:
|
|
200
|
+
try:
|
|
201
|
+
body = response.json()
|
|
202
|
+
except ValueError:
|
|
203
|
+
return False
|
|
204
|
+
return bool((body or {}).get("errors"))
|
|
205
|
+
|
|
206
|
+
def interpret_response(self, response_or_exception: Optional[Union[requests.Response, Exception]] = None) -> ErrorResolution:
|
|
207
|
+
if isinstance(response_or_exception, requests.Response):
|
|
208
|
+
if response_or_exception.status_code in (requests.codes.BAD_GATEWAY, requests.codes.GATEWAY_TIMEOUT):
|
|
209
|
+
# Halve the page size on every 502/504 to reduce GraphQL query cost,
|
|
210
|
+
# but never let it drop below 1 — a page_size of 0 would request no
|
|
211
|
+
# records and cause infinite paging.
|
|
212
|
+
previous_page_size = self.stream.page_size
|
|
213
|
+
self.stream.page_size = max(1, int(self.stream.page_size / 2))
|
|
214
|
+
self._logger.info(
|
|
215
|
+
"GitHub GraphQL endpoint returned HTTP %s for stream `%s`; reducing GraphQL page_size from %s to %s and retrying.",
|
|
216
|
+
response_or_exception.status_code,
|
|
217
|
+
self.stream.name,
|
|
218
|
+
previous_page_size,
|
|
219
|
+
self.stream.page_size,
|
|
220
|
+
)
|
|
221
|
+
return ErrorResolution(
|
|
222
|
+
response_action=ResponseAction.RETRY,
|
|
223
|
+
failure_type=FailureType.transient_error,
|
|
224
|
+
error_message=(
|
|
225
|
+
f"GitHub GraphQL endpoint returned HTTP {response_or_exception.status_code} "
|
|
226
|
+
f"for stream `{self.stream.name}`. Reducing GraphQL page size and retrying."
|
|
227
|
+
),
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
self.stream.page_size = (
|
|
231
|
+
constants.DEFAULT_PAGE_SIZE_FOR_LARGE_STREAM if self.stream.large_stream else constants.DEFAULT_PAGE_SIZE
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
if self._safe_json_get_errors(response_or_exception):
|
|
235
|
+
return ErrorResolution(
|
|
236
|
+
response_action=ResponseAction.RETRY,
|
|
237
|
+
failure_type=FailureType.transient_error,
|
|
238
|
+
error_message=(
|
|
239
|
+
f"GitHub GraphQL endpoint returned errors in the response body "
|
|
240
|
+
f"for stream `{self.stream.name}` (HTTP {response_or_exception.status_code}). Retrying."
|
|
241
|
+
),
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
return super().interpret_response(response_or_exception)
|
{airbyte_source_github-2.2.0.dev202601122028 → airbyte_source_github-2.2.1}/source_github/graphql.py
RENAMED
|
@@ -165,6 +165,54 @@ def get_query_issue_reactions(owner, name, first, after, number=None):
|
|
|
165
165
|
return str(op)
|
|
166
166
|
|
|
167
167
|
|
|
168
|
+
def get_query_releases(owner, name, first, after):
|
|
169
|
+
kwargs = {"first": first, "order_by": {"field": "CREATED_AT", "direction": "ASC"}}
|
|
170
|
+
if after:
|
|
171
|
+
kwargs["after"] = after
|
|
172
|
+
|
|
173
|
+
op = sgqlc.operation.Operation(_schema_root.query_type)
|
|
174
|
+
repository = op.repository(owner=owner, name=name)
|
|
175
|
+
repository.name()
|
|
176
|
+
repository.owner.login()
|
|
177
|
+
releases = repository.releases(**kwargs)
|
|
178
|
+
releases.nodes.__fields__(
|
|
179
|
+
id="node_id",
|
|
180
|
+
database_id="id",
|
|
181
|
+
name=True,
|
|
182
|
+
tag_name="tag_name",
|
|
183
|
+
created_at="created_at",
|
|
184
|
+
published_at="published_at",
|
|
185
|
+
updated_at="updated_at",
|
|
186
|
+
is_draft="draft",
|
|
187
|
+
is_prerelease="prerelease",
|
|
188
|
+
description="body",
|
|
189
|
+
description_html="body_html",
|
|
190
|
+
url="html_url",
|
|
191
|
+
)
|
|
192
|
+
releases.nodes.tag_commit.oid(__alias__="target_commitish")
|
|
193
|
+
author = releases.nodes.author(__alias__="author").__as__(_schema_root.User)
|
|
194
|
+
select_user_fields(author)
|
|
195
|
+
release_assets = releases.nodes.release_assets(first=100, __alias__="assets")
|
|
196
|
+
release_assets.nodes.__fields__(
|
|
197
|
+
id="node_id",
|
|
198
|
+
name=True,
|
|
199
|
+
content_type="content_type",
|
|
200
|
+
size=True,
|
|
201
|
+
download_count="download_count",
|
|
202
|
+
created_at="created_at",
|
|
203
|
+
updated_at="updated_at",
|
|
204
|
+
download_url="browser_download_url",
|
|
205
|
+
url=True,
|
|
206
|
+
)
|
|
207
|
+
release_assets.nodes.uploaded_by(__alias__="uploader").__as__(_schema_root.User).__fields__(database_id="id")
|
|
208
|
+
release_assets.page_info.__fields__(has_next_page=True)
|
|
209
|
+
releases.nodes.reaction_groups(__alias__="reaction_groups").__fields__(content=True)
|
|
210
|
+
releases.nodes.reaction_groups(__alias__="reaction_groups").reactors.__fields__(total_count=True)
|
|
211
|
+
releases.nodes.mentions(first=0, __alias__="mentions_connection").total_count()
|
|
212
|
+
releases.page_info.__fields__(has_next_page=True, end_cursor=True)
|
|
213
|
+
return str(op)
|
|
214
|
+
|
|
215
|
+
|
|
168
216
|
class QueryReactions:
|
|
169
217
|
# AVERAGE_REVIEWS - optimal number of reviews to fetch inside every pull request.
|
|
170
218
|
# If we try to fetch too many (up to 100) we will spend too many scores of query cost.
|