airbyte-source-github 2.2.0.dev202601122124__py3-none-any.whl → 2.2.2__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.
@@ -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
+
@@ -1,11 +1,12 @@
1
1
  source_github/__init__.py,sha256=8hLLjFC1wI1mMU442Y5qCcND9qIwyhaANQwT-GAIK0s,1135
2
- source_github/backoff_strategies.py,sha256=ChkWEmqZL7Qrr1zidOfjZWlMWIVRVO2yqZ7QE_HaZP8,2287
2
+ source_github/backoff_strategies.py,sha256=H1n3xNkkrlto7MqJY-Yp0el2G5S4yuJ7z3kg6aimab0,2343
3
3
  source_github/config_migrations.py,sha256=guUJAdNP-liUciVaJB4ackEEMCN4jd7bNd85QFmDNlU,3932
4
4
  source_github/constants.py,sha256=Hj3Q4y7OoU-Iff4m9gEC2CjwmWJYXhNbHVNjg8EBLmQ,238
5
- source_github/errors_handlers.py,sha256=nKd3iWnrOjFXqVfqBCWzLLWpVxLpBflv676txxSI6x8,7254
5
+ source_github/errors_handlers.py,sha256=y0SjYuxKo3zBRhk2kZe0NZWsKen4W9x-wD84Q0S3sjU,11211
6
6
  source_github/github_schema.py,sha256=2AXmTN_s_VqvZAR1GPqus5HhSXWokS4N7HMQQqefaYw,1600315
7
- source_github/graphql.py,sha256=edd7EoXwD5rRDHrjm4ZBy5VGkruTScL_Xipy4CBmTFc,11625
8
- source_github/run.py,sha256=onA-rP2aVhWHvDquKZdR1381CU66rnzqJ7EFMS5dd4Q,407
7
+ source_github/graphql.py,sha256=fGuYzDynYjJOR7u3GX0t6eBJMzdFxKIp4gHP73MCpos,13518
8
+ source_github/manifest.yaml,sha256=JDINDUIMSofsNdDqjHlEES5s9d7k83mVa9mUvwOKcA0,57776
9
+ source_github/run.py,sha256=gfWy8TcMHa0XRwaVUyq4KmslooST38fvfCLuP5D-5Fo,2391
9
10
  source_github/schemas/assignees.json,sha256=_WmGIwYmTmEi9WWkvXdFF3CLsMzG2usoIjaqw-tb4NA,2335
10
11
  source_github/schemas/branches.json,sha256=3CIkdEi2XVFoVZrpO_U04mMI7-jSb9i-CT3wbIEgPik,2734
11
12
  source_github/schemas/collaborators.json,sha256=ZeRsZvj9lUr43hnjX0l84rl2TTeKKbFKk-tmzBYdOmo,3505
@@ -32,8 +33,7 @@ source_github/schemas/pull_request_comment_reactions.json,sha256=shrzajYnEHg_UlU
32
33
  source_github/schemas/pull_request_commits.json,sha256=WICcuxUsExJQe1qlXvXVN-VvkqbjWxR_E__WKO-Ta7E,4800
33
34
  source_github/schemas/pull_request_stats.json,sha256=harNH5ha6su-2OhxszHOqDKJ34OZzUsL_4nBnaOD16o,3497
34
35
  source_github/schemas/pull_requests.json,sha256=Yp9Sq3jgpfBn0YFbcZC_sy06rlJ-AMkCbHNq90KXT_Y,12196
35
- source_github/schemas/releases.json,sha256=MnoP0oSioKLmXvo6JyuqCoesIOFhtcRli7j6umlkn8k,5140
36
- source_github/schemas/repositories.json,sha256=WpC5ZjoycnHUP5DNz8ImEXROYAel8DGRkBN10W1mQ2w,13319
36
+ source_github/schemas/releases.json,sha256=3ITMlMFML3ZSfo_EAkVajJdaS_Xxz_8V4LhL9GRwWrY,5121
37
37
  source_github/schemas/review_comments.json,sha256=HVH3OE-x_wYVvg0Qxn6rzXOwAGEk0y3De8_VLCgE3mA,4905
38
38
  source_github/schemas/reviews.json,sha256=OPqo9Dz6slRGojamq8pBMKKRZgz96kDRkYiUJjJQH_E,2731
39
39
  source_github/schemas/shared/events/comment.json,sha256=6dcatpi1gitgcR_JscPllyGGoSb8Pgce5OS9vh1HKP4,7400
@@ -52,14 +52,12 @@ source_github/schemas/team_memberships.json,sha256=ECr0ICzCwcUVp-btBicm0EemVxiOP
52
52
  source_github/schemas/teams.json,sha256=_GQOy0IOO9d8BcpnvqIxQLjtPJusfU7fC7IUJmSi6R4,1796
53
53
  source_github/schemas/users.json,sha256=by1CEx3MW740S5F54KZfUDc2N1_UZ98Bb3fWlgCtgxc,2530
54
54
  source_github/schemas/workflow_jobs.json,sha256=Kk3N3FcCYBqOdR7FYvz2nAAFtLZBuVfR8Lxg-ORD39U,3934
55
- source_github/schemas/workflow_run_attempts.json,sha256=XDmIsjtzka-ItEonImD3ZATZjxRNkbFo5-MPTtZDARA,19453
56
55
  source_github/schemas/workflow_runs.json,sha256=XDmIsjtzka-ItEonImD3ZATZjxRNkbFo5-MPTtZDARA,19453
57
56
  source_github/schemas/workflows.json,sha256=gSNw8WZaVKbX4AL97PbjZHzvxcOltXqv9Ao1RNQOFXM,1470
58
- source_github/source.py,sha256=MGJ5alBYUUsauouTBEv2svoLk8LcI0LmSkqBblAe9fE,14710
59
- source_github/spec.json,sha256=7LOQm01fP_RvPF-HifhNPJ7i0AxT2LTNPaLAA3uOfNY,7443
60
- source_github/streams.py,sha256=PyTTBjmB37ClebdIue36zu7qgz1mUO7FZUV53noedRw,82103
61
- source_github/utils.py,sha256=1mKZzLEaDe0m8BTw8Y-MxCXrJr1bZfaLPeSUbPN3vyQ,7243
62
- airbyte_source_github-2.2.0.dev202601122124.dist-info/METADATA,sha256=4D0R1s1DRhGA2AvAVDN5o6GU7y-IkemTkHDqx8h_knY,5314
63
- airbyte_source_github-2.2.0.dev202601122124.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
64
- airbyte_source_github-2.2.0.dev202601122124.dist-info/entry_points.txt,sha256=gYhqVrTAZvMwuYByg0b_-o115yUFLLcfNxMrLZmiW9k,55
65
- airbyte_source_github-2.2.0.dev202601122124.dist-info/RECORD,,
57
+ source_github/source.py,sha256=oAwpfXCMwZf8_lbxr49rSm5qzGffhSq0qIbvIFEy2xs,22559
58
+ source_github/streams.py,sha256=MYPvzAEbGk9nyN0mYamFXEAxepbf7O7aN5Hepf-Bj80,88297
59
+ source_github/utils.py,sha256=rkOk8VG6ey1xDl-9t4B99N6HrvTTPCTGhvifrKwYG9Y,706
60
+ airbyte_source_github-2.2.2.dist-info/METADATA,sha256=1YJ8kLVAVSrdY-KZwq86eicGzaX0EwIbKSYIzL0avn4,1248
61
+ airbyte_source_github-2.2.2.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
62
+ airbyte_source_github-2.2.2.dist-info/entry_points.txt,sha256=gYhqVrTAZvMwuYByg0b_-o115yUFLLcfNxMrLZmiW9k,55
63
+ airbyte_source_github-2.2.2.dist-info/RECORD,,
@@ -10,38 +10,47 @@ import requests
10
10
  from airbyte_cdk import BackoffStrategy
11
11
  from airbyte_cdk.sources.streams.http import HttpStream
12
12
 
13
+ from .errors_handlers import is_rate_limited_response
14
+
13
15
 
14
16
  class GithubStreamABCBackoffStrategy(BackoffStrategy):
15
- def __init__(self, stream: HttpStream, **kwargs): # type: ignore # noqa
17
+ def __init__(self, stream: HttpStream, max_wait_time_seconds: float = 120 * 60, **kwargs): # type: ignore # noqa
16
18
  self.stream = stream
19
+ self.max_wait_time_seconds = max_wait_time_seconds
17
20
  super().__init__(**kwargs)
18
21
 
19
22
  def backoff_time(
20
23
  self, response_or_exception: Optional[Union[requests.Response, requests.RequestException]], **kwargs: Any
21
24
  ) -> Optional[float]:
22
- # This method is called if we run into the rate limit. GitHub limits requests to 5000 per hour and provides
23
- # `X-RateLimit-Reset` header which contains time when this hour will be finished and limits will be reset so
24
- # we again could have 5000 per another hour.
25
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
+
26
33
  min_backoff_time = 60.0
27
34
  retry_after = response_or_exception.headers.get("Retry-After")
28
35
  if retry_after is not None:
29
- backoff_time_in_seconds = max(float(retry_after), min_backoff_time)
30
- return self.get_waiting_time(backoff_time_in_seconds)
31
-
32
- reset_time = response_or_exception.headers.get("X-RateLimit-Reset")
33
- if reset_time:
34
- backoff_time_in_seconds = max(float(reset_time) - time.time(), min_backoff_time)
35
- return self.get_waiting_time(backoff_time_in_seconds)
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
36
52
  return None
37
53
 
38
- def get_waiting_time(self, backoff_time_in_seconds: Optional[float]) -> Optional[float]:
39
- if backoff_time_in_seconds < 60 * 10: # type: ignore[operator]
40
- return backoff_time_in_seconds
41
- else:
42
- self.stream._http_client._session.auth.update_token() # New token will be used in next request
43
- return 1
44
-
45
54
 
46
55
  class ContributorActivityBackoffStrategy(BackoffStrategy):
47
56
  def backoff_time(
@@ -2,7 +2,8 @@
2
2
  # Copyright (c) 2023 Airbyte, Inc., all rights reserved.
3
3
  #
4
4
 
5
- from typing import Optional, Union
5
+ import logging
6
+ from typing import Callable, Optional, Union
6
7
 
7
8
  import requests
8
9
 
@@ -14,6 +15,9 @@ from airbyte_cdk.sources.streams.http.error_handlers.default_error_mapping impor
14
15
  from . import constants
15
16
 
16
17
 
18
+ logger = logging.getLogger("airbyte")
19
+
20
+
17
21
  GITHUB_DEFAULT_ERROR_MAPPING = DEFAULT_ERROR_MAPPING | {
18
22
  401: ErrorResolution(
19
23
  response_action=ResponseAction.RETRY,
@@ -21,9 +25,14 @@ GITHUB_DEFAULT_ERROR_MAPPING = DEFAULT_ERROR_MAPPING | {
21
25
  error_message="Conflict.",
22
26
  ),
23
27
  403: ErrorResolution(
24
- response_action=ResponseAction.RETRY,
28
+ response_action=ResponseAction.FAIL,
25
29
  failure_type=FailureType.config_error,
26
- error_message="Conflict.",
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
+ ),
27
36
  ),
28
37
  404: ErrorResolution(
29
38
  response_action=ResponseAction.RETRY,
@@ -36,20 +45,73 @@ GITHUB_DEFAULT_ERROR_MAPPING = DEFAULT_ERROR_MAPPING | {
36
45
  error_message="Conflict.",
37
46
  ),
38
47
  410: ErrorResolution(
39
- response_action=ResponseAction.RETRY,
48
+ response_action=ResponseAction.FAIL,
40
49
  failure_type=FailureType.config_error,
41
- error_message="Gone. Please ensure the url is valid.",
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
+ ),
42
55
  ),
43
56
  }
44
57
 
45
58
 
46
59
  def is_conflict_with_empty_repository(response_or_exception: Optional[Union[requests.Response, Exception]] = None) -> bool:
47
60
  if isinstance(response_or_exception, requests.Response) and response_or_exception.status_code == requests.codes.CONFLICT:
48
- response_data = response_or_exception.json()
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
49
69
  return response_data.get("message") == "Git Repository is empty."
50
70
  return False
51
71
 
52
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
+
53
115
  class GithubStreamABCErrorHandler(HttpStatusErrorHandler):
54
116
  def __init__(self, stream: HttpStream, **kwargs): # type: ignore # noqa
55
117
  self.stream = stream
@@ -57,19 +119,10 @@ class GithubStreamABCErrorHandler(HttpStatusErrorHandler):
57
119
 
58
120
  def interpret_response(self, response_or_exception: Optional[Union[requests.Response, Exception]] = None) -> ErrorResolution:
59
121
  if isinstance(response_or_exception, requests.Response):
60
- retry_flag = (
61
- # The GitHub GraphQL API has limitations
62
- # https://docs.github.com/en/graphql/overview/resource-limitations
63
- (
64
- response_or_exception.headers.get("X-RateLimit-Resource") == "graphql"
65
- and self.stream.check_graphql_rate_limited(response_or_exception.json())
66
- )
67
- # Rate limit HTTP headers
68
- # https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limit-http-headers
69
- or (response_or_exception.status_code != 200 and response_or_exception.headers.get("X-RateLimit-Remaining") == "0")
70
- # Secondary rate limits
71
- # https://docs.github.com/en/rest/overview/resources-in-the-rest-api#secondary-rate-limits
72
- or "Retry-After" in response_or_exception.headers
122
+ retry_flag = is_rate_limited_response(
123
+ response_or_exception,
124
+ self.stream.check_graphql_rate_limited,
125
+ self._logger,
73
126
  )
74
127
  if retry_flag:
75
128
  headers = [
@@ -92,11 +145,26 @@ class GithubStreamABCErrorHandler(HttpStatusErrorHandler):
92
145
  return ErrorResolution(
93
146
  response_action=ResponseAction.RATE_LIMITED,
94
147
  failure_type=FailureType.transient_error,
95
- error_message=f"Response status code: {response_or_exception.status_code}. Retrying...",
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
+ ),
96
153
  )
97
154
 
98
155
  if is_conflict_with_empty_repository(response_or_exception=response_or_exception):
99
- log_message = f"Ignoring response for '{response_or_exception.request.method}' request to '{response_or_exception.url}' with response code '{response_or_exception.status_code}' as the repository is empty."
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')}."
100
168
  return ErrorResolution(
101
169
  response_action=ResponseAction.IGNORE,
102
170
  failure_type=FailureType.config_error,
@@ -106,7 +174,7 @@ class GithubStreamABCErrorHandler(HttpStatusErrorHandler):
106
174
  return super().interpret_response(response_or_exception)
107
175
 
108
176
 
109
- class ContributorActivityErrorHandler(HttpStatusErrorHandler):
177
+ class ContributorActivityErrorHandler(GithubStreamABCErrorHandler):
110
178
  """
111
179
  This custom error handler is needed for streams based on repository statistics endpoints like ContributorActivity because
112
180
  when requesting data that hasn't been cached yet when the request is made, you'll receive a 202 response. And these requests
@@ -128,25 +196,49 @@ class ContributorActivityErrorHandler(HttpStatusErrorHandler):
128
196
 
129
197
 
130
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
+
131
206
  def interpret_response(self, response_or_exception: Optional[Union[requests.Response, Exception]] = None) -> ErrorResolution:
132
207
  if isinstance(response_or_exception, requests.Response):
133
208
  if response_or_exception.status_code in (requests.codes.BAD_GATEWAY, requests.codes.GATEWAY_TIMEOUT):
134
- self.stream.page_size = int(self.stream.page_size / 2)
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
+ )
135
221
  return ErrorResolution(
136
222
  response_action=ResponseAction.RETRY,
137
223
  failure_type=FailureType.transient_error,
138
- error_message=f"Response status code: {response_or_exception.status_code}. Retrying...",
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
+ ),
139
228
  )
140
229
 
141
230
  self.stream.page_size = (
142
231
  constants.DEFAULT_PAGE_SIZE_FOR_LARGE_STREAM if self.stream.large_stream else constants.DEFAULT_PAGE_SIZE
143
232
  )
144
233
 
145
- if response_or_exception.json().get("errors"):
234
+ if self._safe_json_get_errors(response_or_exception):
146
235
  return ErrorResolution(
147
236
  response_action=ResponseAction.RETRY,
148
237
  failure_type=FailureType.transient_error,
149
- error_message=f"Response status code: {response_or_exception.status_code}. Retrying...",
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
+ ),
150
242
  )
151
243
 
152
244
  return super().interpret_response(response_or_exception)
source_github/graphql.py CHANGED
@@ -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.