airbyte-source-github 2.2.0.dev202601122124__tar.gz → 2.2.2__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 (72) hide show
  1. airbyte_source_github-2.2.2/.airbyte-pypi-readme.md +7 -0
  2. airbyte_source_github-2.2.2/PKG-INFO +29 -0
  3. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/pyproject.toml +3 -3
  4. airbyte_source_github-2.2.2/source_github/backoff_strategies.py +61 -0
  5. airbyte_source_github-2.2.2/source_github/errors_handlers.py +244 -0
  6. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/graphql.py +48 -0
  7. airbyte_source_github-2.2.2/source_github/manifest.yaml +1363 -0
  8. airbyte_source_github-2.2.2/source_github/run.py +63 -0
  9. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/releases.json +13 -24
  10. airbyte_source_github-2.2.2/source_github/source.py +442 -0
  11. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/streams.py +306 -167
  12. airbyte_source_github-2.2.2/source_github/utils.py +24 -0
  13. airbyte_source_github-2.2.0.dev202601122124/PKG-INFO +0 -126
  14. airbyte_source_github-2.2.0.dev202601122124/README.md +0 -104
  15. airbyte_source_github-2.2.0.dev202601122124/source_github/backoff_strategies.py +0 -52
  16. airbyte_source_github-2.2.0.dev202601122124/source_github/errors_handlers.py +0 -152
  17. airbyte_source_github-2.2.0.dev202601122124/source_github/run.py +0 -17
  18. airbyte_source_github-2.2.0.dev202601122124/source_github/schemas/repositories.json +0 -427
  19. airbyte_source_github-2.2.0.dev202601122124/source_github/schemas/workflow_run_attempts.json +0 -585
  20. airbyte_source_github-2.2.0.dev202601122124/source_github/source.py +0 -315
  21. airbyte_source_github-2.2.0.dev202601122124/source_github/spec.json +0 -190
  22. airbyte_source_github-2.2.0.dev202601122124/source_github/utils.py +0 -186
  23. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/__init__.py +0 -0
  24. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/config_migrations.py +0 -0
  25. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/constants.py +0 -0
  26. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/github_schema.py +0 -0
  27. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/assignees.json +0 -0
  28. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/branches.json +0 -0
  29. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/collaborators.json +0 -0
  30. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/comments.json +0 -0
  31. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/commit_comment_reactions.json +0 -0
  32. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/commit_comments.json +0 -0
  33. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/commits.json +0 -0
  34. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/contributor_activity.json +0 -0
  35. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/deployments.json +0 -0
  36. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/events.json +0 -0
  37. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/issue_comment_reactions.json +0 -0
  38. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/issue_events.json +0 -0
  39. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/issue_labels.json +0 -0
  40. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/issue_milestones.json +0 -0
  41. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/issue_reactions.json +0 -0
  42. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/issue_timeline_events.json +0 -0
  43. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/issues.json +0 -0
  44. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/organizations.json +0 -0
  45. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/project_cards.json +0 -0
  46. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/project_columns.json +0 -0
  47. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/projects.json +0 -0
  48. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/projects_v2.json +0 -0
  49. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/pull_request_comment_reactions.json +0 -0
  50. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/pull_request_commits.json +0 -0
  51. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/pull_request_stats.json +0 -0
  52. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/pull_requests.json +0 -0
  53. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/review_comments.json +0 -0
  54. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/reviews.json +0 -0
  55. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/shared/events/comment.json +0 -0
  56. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/shared/events/commented.json +0 -0
  57. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/shared/events/committed.json +0 -0
  58. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/shared/events/cross_referenced.json +0 -0
  59. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/shared/events/reviewed.json +0 -0
  60. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/shared/reaction.json +0 -0
  61. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/shared/reactions.json +0 -0
  62. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/shared/user.json +0 -0
  63. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/shared/user_graphql.json +0 -0
  64. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/stargazers.json +0 -0
  65. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/tags.json +0 -0
  66. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/team_members.json +0 -0
  67. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/team_memberships.json +0 -0
  68. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/teams.json +0 -0
  69. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/users.json +0 -0
  70. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/workflow_jobs.json +0 -0
  71. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/source_github/schemas/workflow_runs.json +0 -0
  72. {airbyte_source_github-2.2.0.dev202601122124 → airbyte_source_github-2.2.2}/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.2
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.0.dev.202601122124"
6
+ version = "2.2.2"
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 = "README.md"
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.4.1"
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)
@@ -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.