1
0
mirror of synced 2025-12-23 11:57:55 -05:00

Source GitHub: add stream ProjectsV2 (#30731)

Co-authored-by: artem1205 <artem1205@users.noreply.github.com>
This commit is contained in:
Artem Inzhyyants
2023-09-28 08:46:29 +02:00
committed by GitHub
parent 0537dde7d2
commit 61a63ec2ff
11 changed files with 9507 additions and 2518 deletions

View File

@@ -12,5 +12,5 @@ RUN pip install .
ENV AIRBYTE_ENTRYPOINT "python /airbyte/integration_code/main.py"
ENTRYPOINT ["python", "/airbyte/integration_code/main.py"]
LABEL io.airbyte.version=1.2.1
LABEL io.airbyte.version=1.3.0
LABEL io.airbyte.name=airbyte/source-github

View File

@@ -18,6 +18,7 @@
{"stream":"project_cards","data":{"url":"https://api.github.com/projects/columns/cards/77859890","project_url":"https://api.github.com/projects/13167124","id":77859890,"node_id":"PRC_lALOF9hP9c4AyOoUzgSkDDI","note":"note_1","archived":false,"creator":{"login":"grubberr","id":195743,"node_id":"MDQ6VXNlcjE5NTc0Mw==","avatar_url":"https://avatars.githubusercontent.com/u/195743?v=4","gravatar_id":"","url":"https://api.github.com/users/grubberr","html_url":"https://github.com/grubberr","followers_url":"https://api.github.com/users/grubberr/followers","following_url":"https://api.github.com/users/grubberr/following{/other_user}","gists_url":"https://api.github.com/users/grubberr/gists{/gist_id}","starred_url":"https://api.github.com/users/grubberr/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/grubberr/subscriptions","organizations_url":"https://api.github.com/users/grubberr/orgs","repos_url":"https://api.github.com/users/grubberr/repos","events_url":"https://api.github.com/users/grubberr/events{/privacy}","received_events_url":"https://api.github.com/users/grubberr/received_events","type":"User","site_admin":false},"created_at":"2022-02-17T09:56:51Z","updated_at":"2022-02-17T09:56:51Z","column_url":"https://api.github.com/projects/columns/17807006","repository":"airbytehq/integration-test","project_id":13167124,"column_id":17807006},"emitted_at":1677668754200}
{"stream":"project_columns","data":{"url":"https://api.github.com/projects/columns/17807092","project_url":"https://api.github.com/projects/13167124","cards_url":"https://api.github.com/projects/columns/17807092/cards","id":17807092,"node_id":"PC_lATOF9hP9c4AyOoUzgEPtvQ","name":"column_2","created_at":"2022-02-17T09:57:27Z","updated_at":"2022-02-17T09:57:27Z","repository":"airbytehq/integration-test","project_id":13167124},"emitted_at":1677668754456}
{"stream":"projects","data":{"owner_url":"https://api.github.com/repos/airbytehq/integration-test","url":"https://api.github.com/projects/13167124","html_url":"https://github.com/airbytehq/integration-test/projects/3","columns_url":"https://api.github.com/projects/13167124/columns","id":13167124,"node_id":"PRO_kwLOF9hP9c4AyOoU","name":"project_3","body":null,"number":3,"state":"open","creator":{"login":"gaart","id":743901,"node_id":"MDQ6VXNlcjc0MzkwMQ==","avatar_url":"https://avatars.githubusercontent.com/u/743901?v=4","gravatar_id":"","url":"https://api.github.com/users/gaart","html_url":"https://github.com/gaart","followers_url":"https://api.github.com/users/gaart/followers","following_url":"https://api.github.com/users/gaart/following{/other_user}","gists_url":"https://api.github.com/users/gaart/gists{/gist_id}","starred_url":"https://api.github.com/users/gaart/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/gaart/subscriptions","organizations_url":"https://api.github.com/users/gaart/orgs","repos_url":"https://api.github.com/users/gaart/repos","events_url":"https://api.github.com/users/gaart/events{/privacy}","received_events_url":"https://api.github.com/users/gaart/received_events","type":"User","site_admin":false},"created_at":"2021-08-27T15:43:57Z","updated_at":"2022-02-17T12:16:56Z","repository":"airbytehq/integration-test"},"emitted_at":1677668754468}
{"stream":"projects_v2","data":{"closed":false,"created_at":"2023-09-25T18:34:52Z","closed_at":null,"updated_at":"2023-09-25T18:35:45Z","creator":{"avatarUrl":"https://avatars.githubusercontent.com/u/92915184?u=e53c87d81ec6fb0596bc0f75e12e84e8f0df8d83&v=4","login":"airbyteio","resourcePath":"/airbyteio","url":"https://github.com/airbyteio"},"node_id":"PVT_kwDOA4_XW84AV7NS","id":5747538,"number":58,"public":false,"readme":"# Title\nintegration test project","short_description":"integration test project description","template":false,"title":"integration test project","url":"https://github.com/orgs/airbytehq/projects/58","viewerCanClose":true,"viewerCanReopen":true,"viewerCanUpdate":true,"owner_id":"MDEyOk9yZ2FuaXphdGlvbjU5NzU4NDI3","repository":"airbytehq/integration-test"},"emitted_at":1695666959656}
{"stream":"pull_request_comment_reactions","data":{"node_id":"MDMyOlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudFJlYWN0aW9uMTI3MDUxNDM4","id":127051438,"content":"HEART","created_at":"2021-09-06T11:37:25Z","user":{"node_id":"MDQ6VXNlcjM0MTAzMTI1","id":34103125,"login":"yevhenii-ldv","avatar_url":"https://avatars.githubusercontent.com/u/34103125?u=3e49bb73177a9f70896e3d49b34656ab659c70a5&v=4","html_url":"https://github.com/yevhenii-ldv","site_admin":false,"type":"User"},"repository":"airbytehq/integration-test","comment_id":699253726},"emitted_at":1677668755106}
{"stream":"pull_request_commits","data":{"sha":"00a74695eb754865a552196ee158a87f0b9dcff7","node_id":"MDY6Q29tbWl0NDAwMDUyMjEzOjAwYTc0Njk1ZWI3NTQ4NjVhNTUyMTk2ZWUxNThhODdmMGI5ZGNmZjc=","commit":{"author":{"name":"Arthur Galuza","email":"a.galuza@exaft.com","date":"2021-08-27T15:41:11Z"},"committer":{"name":"Arthur Galuza","email":"a.galuza@exaft.com","date":"2021-08-27T15:41:11Z"},"message":"commit number 0","tree":{"sha":"3f2a52f90f9acc30359b00065e5b989267fef1f5","url":"https://api.github.com/repos/airbytehq/integration-test/git/trees/3f2a52f90f9acc30359b00065e5b989267fef1f5"},"url":"https://api.github.com/repos/airbytehq/integration-test/git/commits/00a74695eb754865a552196ee158a87f0b9dcff7","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/airbytehq/integration-test/commits/00a74695eb754865a552196ee158a87f0b9dcff7","html_url":"https://github.com/airbytehq/integration-test/commit/00a74695eb754865a552196ee158a87f0b9dcff7","comments_url":"https://api.github.com/repos/airbytehq/integration-test/commits/00a74695eb754865a552196ee158a87f0b9dcff7/comments","author":{"login":"gaart","id":743901,"node_id":"MDQ6VXNlcjc0MzkwMQ==","avatar_url":"https://avatars.githubusercontent.com/u/743901?v=4","gravatar_id":"","url":"https://api.github.com/users/gaart","html_url":"https://github.com/gaart","followers_url":"https://api.github.com/users/gaart/followers","following_url":"https://api.github.com/users/gaart/following{/other_user}","gists_url":"https://api.github.com/users/gaart/gists{/gist_id}","starred_url":"https://api.github.com/users/gaart/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/gaart/subscriptions","organizations_url":"https://api.github.com/users/gaart/orgs","repos_url":"https://api.github.com/users/gaart/repos","events_url":"https://api.github.com/users/gaart/events{/privacy}","received_events_url":"https://api.github.com/users/gaart/received_events","type":"User","site_admin":false},"committer":{"login":"gaart","id":743901,"node_id":"MDQ6VXNlcjc0MzkwMQ==","avatar_url":"https://avatars.githubusercontent.com/u/743901?v=4","gravatar_id":"","url":"https://api.github.com/users/gaart","html_url":"https://github.com/gaart","followers_url":"https://api.github.com/users/gaart/followers","following_url":"https://api.github.com/users/gaart/following{/other_user}","gists_url":"https://api.github.com/users/gaart/gists{/gist_id}","starred_url":"https://api.github.com/users/gaart/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/gaart/subscriptions","organizations_url":"https://api.github.com/users/gaart/orgs","repos_url":"https://api.github.com/users/gaart/repos","events_url":"https://api.github.com/users/gaart/events{/privacy}","received_events_url":"https://api.github.com/users/gaart/received_events","type":"User","site_admin":false},"parents":[{"sha":"978753aeb56f7b49872279d1b491411a6235aa90","url":"https://api.github.com/repos/airbytehq/integration-test/commits/978753aeb56f7b49872279d1b491411a6235aa90","html_url":"https://github.com/airbytehq/integration-test/commit/978753aeb56f7b49872279d1b491411a6235aa90"}],"repository":"airbytehq/integration-test","pull_number":5},"emitted_at":1677668756160}
{"stream":"pull_request_stats","data":{"node_id":"MDExOlB1bGxSZXF1ZXN0NzIxNDM1NTA2","id":721435506,"number":5,"updated_at":"2021-08-27T15:53:14Z","changed_files":5,"deletions":0,"additions":5,"merged":false,"mergeable":"MERGEABLE","can_be_rebased":true,"maintainer_can_modify":false,"merge_state_status":"BLOCKED","comments":0,"commits":5,"review_comments":0,"merged_by":null,"repository":"airbytehq/integration-test"},"emitted_at":1677668759962}

View File

@@ -5,7 +5,7 @@ data:
connectorSubtype: api
connectorType: source
definitionId: ef69ef6e-aa7f-4af1-a01d-ef775033524e
dockerImageTag: 1.2.1
dockerImageTag: 1.3.0
maxSecondsBetweenMessages: 5400
dockerRepository: airbyte/source-github
githubIssueLabel: source-github

View File

@@ -61,6 +61,40 @@ def get_query_pull_requests(owner, name, first, after, direction):
return str(op)
def get_query_projectsV2(owner, name, first, after, direction):
kwargs = {"first": first, "order_by": {"field": "UPDATED_AT", "direction": direction}}
if after:
kwargs["after"] = after
op = sgqlc.operation.Operation(_schema_root.query_type)
repository = op.repository(owner=owner, name=name)
repository.name()
repository.owner.login()
projects_v2 = repository.projects_v2(**kwargs)
projects_v2.nodes.__fields__(
closed=True,
created_at="created_at",
closed_at="closed_at",
updated_at="updated_at",
creator="creator",
id="node_id",
database_id="id",
number=True,
public=True,
readme="readme",
short_description="short_description",
template=True,
title="title",
url="url",
viewer_can_close=True,
viewer_can_reopen=True,
viewer_can_update=True,
)
projects_v2.nodes.owner.__fields__(id="id")
projects_v2.page_info.__fields__(has_next_page=True, end_cursor=True)
return str(op)
def get_query_reviews(owner, name, first, after, number=None):
op = sgqlc.operation.Operation(_schema_root.query_type)
repository = op.repository(owner=owner, name=name)

View File

@@ -0,0 +1,80 @@
{
"$schema": "https://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"closed": {
"type": ["null", "boolean"]
},
"created_at": {
"type": ["null", "string"],
"format": "date-time"
},
"creator": {
"type": ["null", "object"],
"properties": {
"avatarUrl": {
"type": ["null", "string"]
},
"login": {
"type": ["null", "string"]
},
"resourcePath": {
"type": ["null", "string"]
},
"url": {
"type": ["null", "string"]
}
}
},
"closed_at": {
"type": ["null", "string"],
"format": "date-time"
},
"updated_at": {
"type": ["null", "string"],
"format": "date-time"
},
"node_id": {
"type": ["null", "string"]
},
"id": {
"type": ["null", "integer"]
},
"number": {
"type": ["null", "integer"]
},
"public": {
"type": ["null", "boolean"]
},
"readme": {
"type": ["null", "string"]
},
"short_description": {
"type": ["null", "string"]
},
"template": {
"type": ["null", "boolean"]
},
"title": {
"type": ["null", "string"]
},
"url": {
"type": ["null", "string"]
},
"viewerCanClose": {
"type": ["null", "boolean"]
},
"viewerCanReopen": {
"type": ["null", "boolean"]
},
"viewerCanUpdate": {
"type": ["null", "boolean"]
},
"owner_id": {
"type": ["null", "string"]
},
"repository": {
"type": "string"
}
}
}

View File

@@ -36,6 +36,7 @@ from .streams import (
ProjectCards,
ProjectColumns,
Projects,
ProjectsV2,
PullRequestCommentReactions,
PullRequestCommits,
PullRequests,
@@ -311,6 +312,7 @@ class SourceGithub(AbstractSource):
PullRequestCommentReactions(**repository_args_with_start_date),
PullRequestCommits(parent=pull_requests_stream, **repository_args),
PullRequestStats(**repository_args_with_start_date),
ProjectsV2(**repository_args_with_start_date),
pull_requests_stream,
Releases(**repository_args_with_start_date),
Repositories(**organization_args_with_start_date),

View File

@@ -16,7 +16,14 @@ from airbyte_cdk.sources.streams.http.exceptions import DefaultBackoffException
from requests.exceptions import HTTPError
from . import constants
from .graphql import CursorStorage, QueryReactions, get_query_issue_reactions, get_query_pull_requests, get_query_reviews
from .graphql import (
CursorStorage,
QueryReactions,
get_query_issue_reactions,
get_query_projectsV2,
get_query_pull_requests,
get_query_reviews,
)
from .utils import getter
@@ -127,6 +134,14 @@ class GithubStreamABC(HttpStream, ABC):
if reset_time:
return max(float(reset_time) - time.time(), min_backoff_time)
def check_graphql_rate_limited(self, response_json) -> bool:
errors = response_json.get("errors")
if errors:
for error in errors:
if error.get("type") == "RATE_LIMITED":
return True
return False
def read_records(self, stream_slice: Mapping[str, Any] = None, **kwargs) -> Iterable[Mapping[str, Any]]:
# get out the stream_slice parts for later use.
organisation = stream_slice.get("organization", "")
@@ -204,14 +219,6 @@ class GithubStream(GithubStreamABC):
for repository in self.repositories:
yield {"repository": repository}
def check_graphql_rate_limited(self, response_json) -> bool:
errors = response_json.get("errors")
if errors:
for error in errors:
if error.get("type") == "RATE_LIMITED":
return True
return False
def get_error_display_message(self, exception: BaseException) -> Optional[str]:
if (
isinstance(exception, DefaultBackoffException)
@@ -716,12 +723,8 @@ class ReviewComments(IncrementalMixin, GithubStream):
return f"repos/{stream_slice['repository']}/pulls/comments"
class PullRequestStats(SemiIncrementalMixin, GithubStream):
"""
API docs: https://docs.github.com/en/graphql/reference/objects#pullrequest
"""
class GitHubGraphQLStream(GithubStream, ABC):
is_sorted = "asc"
http_method = "POST"
def path(
@@ -729,15 +732,27 @@ class PullRequestStats(SemiIncrementalMixin, GithubStream):
) -> str:
return "graphql"
def raise_error_from_response(self, response_json):
if "errors" in response_json:
raise Exception(str(response_json["errors"]))
def should_retry(self, response: requests.Response) -> bool:
return True if response.json().get("errors") else super().should_retry(response)
def _get_name(self, repository):
def _get_repository_name(self, repository: Mapping[str, Any]) -> str:
return repository["owner"]["login"] + "/" + repository["name"]
def request_params(
self, stream_state: Mapping[str, Any], stream_slice: Mapping[str, Any] = None, next_page_token: Mapping[str, Any] = None
) -> MutableMapping[str, Any]:
return {}
class PullRequestStats(SemiIncrementalMixin, GitHubGraphQLStream):
"""
API docs: https://docs.github.com/en/graphql/reference/objects#pullrequest
"""
large_stream = True
is_sorted = "asc"
def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]:
self.raise_error_from_response(response_json=response.json())
repository = response.json()["data"]["repository"]
if repository:
nodes = repository["pullRequests"]["nodes"]
@@ -745,7 +760,7 @@ class PullRequestStats(SemiIncrementalMixin, GithubStream):
record["review_comments"] = sum([node["comments"]["totalCount"] for node in record["review_comments"]["nodes"]])
record["comments"] = record["comments"]["totalCount"]
record["commits"] = record["commits"]["totalCount"]
record["repository"] = self._get_name(repository)
record["repository"] = self._get_repository_name(repository)
if record["merged_by"]:
record["merged_by"]["type"] = record["merged_by"].pop("__typename")
yield record
@@ -757,11 +772,6 @@ class PullRequestStats(SemiIncrementalMixin, GithubStream):
if pageInfo["hasNextPage"]:
return {"after": pageInfo["endCursor"]}
def request_params(
self, stream_state: Mapping[str, Any], stream_slice: Mapping[str, Any] = None, next_page_token: Mapping[str, Any] = None
) -> MutableMapping[str, Any]:
return {}
def request_body_json(
self,
stream_state: Mapping[str, Any],
@@ -783,13 +793,12 @@ class PullRequestStats(SemiIncrementalMixin, GithubStream):
return {**base_headers, **headers}
class Reviews(SemiIncrementalMixin, GithubStream):
class Reviews(SemiIncrementalMixin, GitHubGraphQLStream):
"""
API docs: https://docs.github.com/en/graphql/reference/objects#pullrequestreview
"""
is_sorted = False
http_method = "POST"
cursor_field = "updated_at"
def __init__(self, **kwargs):
@@ -797,20 +806,6 @@ class Reviews(SemiIncrementalMixin, GithubStream):
self.pull_requests_cursor = {}
self.reviews_cursors = {}
def path(
self, *, stream_state: Mapping[str, Any] = None, stream_slice: Mapping[str, Any] = None, next_page_token: Mapping[str, Any] = None
) -> str:
return "graphql"
def request_params(
self, stream_state: Mapping[str, Any], stream_slice: Mapping[str, Any] = None, next_page_token: Mapping[str, Any] = None
) -> MutableMapping[str, Any]:
return {}
def raise_error_from_response(self, response_json):
if "errors" in response_json:
raise Exception(str(response_json["errors"]))
def _get_records(self, pull_request, repository_name):
"yield review records from pull_request"
for record in pull_request["reviews"]["nodes"]:
@@ -827,14 +822,10 @@ class Reviews(SemiIncrementalMixin, GithubStream):
}
yield record
def _get_name(self, repository):
return repository["owner"]["login"] + "/" + repository["name"]
def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]:
self.raise_error_from_response(response_json=response.json())
repository = response.json()["data"]["repository"]
if repository:
repository_name = self._get_name(repository)
repository_name = self._get_repository_name(repository)
if "pullRequests" in repository:
for pull_request in repository["pullRequests"]["nodes"]:
yield from self._get_records(pull_request, repository_name)
@@ -844,7 +835,7 @@ class Reviews(SemiIncrementalMixin, GithubStream):
def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, Any]]:
repository = response.json()["data"]["repository"]
if repository:
repository_name = self._get_name(repository)
repository_name = self._get_repository_name(repository)
reviews_cursors = self.reviews_cursors.setdefault(repository_name, {})
if "pullRequests" in repository:
if repository["pullRequests"]["pageInfo"]["hasNextPage"]:
@@ -909,6 +900,44 @@ class PullRequestCommits(GithubStream):
return record
class ProjectsV2(SemiIncrementalMixin, GitHubGraphQLStream):
"""
API docs: https://docs.github.com/en/graphql/reference/objects#pullrequest
"""
is_sorted = "asc"
def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]:
repository = response.json()["data"]["repository"]
if repository:
nodes = repository["projectsV2"]["nodes"]
for record in nodes:
record["owner_id"] = record.pop("owner").get("id")
record["repository"] = self._get_repository_name(repository)
yield record
def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, Any]]:
repository = response.json()["data"]["repository"]
if repository:
page_info = repository["projectsV2"]["pageInfo"]
if page_info["hasNextPage"]:
return {"after": page_info["endCursor"]}
def request_body_json(
self,
stream_state: Mapping[str, Any],
stream_slice: Mapping[str, Any] = None,
next_page_token: Mapping[str, Any] = None,
) -> Optional[Mapping]:
organization, name = stream_slice["repository"].split("/")
if next_page_token:
next_page_token = next_page_token["after"]
query = get_query_projectsV2(
owner=organization, name=name, first=self.page_size, after=next_page_token, direction=self.is_sorted.upper()
)
return {"query": query}
# Reactions streams
@@ -995,13 +1024,12 @@ class IssueCommentReactions(ReactionStream):
parent_entity = Comments
class IssueReactions(SemiIncrementalMixin, GithubStream):
class IssueReactions(SemiIncrementalMixin, GitHubGraphQLStream):
"""
https://docs.github.com/en/graphql/reference/objects#issue
https://docs.github.com/en/graphql/reference/objects#reaction
"""
http_method = "POST"
cursor_field = "created_at"
def __init__(self, **kwargs):
@@ -1009,18 +1037,6 @@ class IssueReactions(SemiIncrementalMixin, GithubStream):
self.issues_cursor = {}
self.reactions_cursors = {}
def path(
self, *, stream_state: Mapping[str, Any] = None, stream_slice: Mapping[str, Any] = None, next_page_token: Mapping[str, Any] = None
) -> str:
return "graphql"
def raise_error_from_response(self, response_json):
if "errors" in response_json:
raise Exception(str(response_json["errors"]))
def _get_name(self, repository):
return repository["owner"]["login"] + "/" + repository["name"]
def _get_reactions_from_issue(self, issue, repository_name):
for reaction in issue["reactions"]["nodes"]:
reaction["repository"] = repository_name
@@ -1029,10 +1045,9 @@ class IssueReactions(SemiIncrementalMixin, GithubStream):
yield reaction
def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]:
self.raise_error_from_response(response_json=response.json())
repository = response.json()["data"]["repository"]
if repository:
repository_name = self._get_name(repository)
repository_name = self._get_repository_name(repository)
if "issues" in repository:
for issue in repository["issues"]["nodes"]:
yield from self._get_reactions_from_issue(issue, repository_name)
@@ -1042,7 +1057,7 @@ class IssueReactions(SemiIncrementalMixin, GithubStream):
def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, Any]]:
repository = response.json()["data"]["repository"]
if repository:
repository_name = self._get_name(repository)
repository_name = self._get_repository_name(repository)
reactions_cursors = self.reactions_cursors.setdefault(repository_name, {})
if "issues" in repository:
if repository["issues"]["pageInfo"]["hasNextPage"]:
@@ -1074,14 +1089,13 @@ class IssueReactions(SemiIncrementalMixin, GithubStream):
return {"query": query}
class PullRequestCommentReactions(SemiIncrementalMixin, GithubStream):
class PullRequestCommentReactions(SemiIncrementalMixin, GitHubGraphQLStream):
"""
API docs:
https://docs.github.com/en/graphql/reference/objects#pullrequestreviewcomment
https://docs.github.com/en/graphql/reference/objects#reaction
"""
http_method = "POST"
cursor_field = "created_at"
def __init__(self, **kwargs):
@@ -1089,21 +1103,9 @@ class PullRequestCommentReactions(SemiIncrementalMixin, GithubStream):
self.cursor_storage = CursorStorage(["PullRequest", "PullRequestReview", "PullRequestReviewComment", "Reaction"])
self.query_reactions = QueryReactions()
def path(
self, *, stream_state: Mapping[str, Any] = None, stream_slice: Mapping[str, Any] = None, next_page_token: Mapping[str, Any] = None
) -> str:
return "graphql"
def raise_error_from_response(self, response_json):
if "errors" in response_json:
raise Exception(str(response_json["errors"]))
def _get_name(self, repository):
return repository["owner"]["login"] + "/" + repository["name"]
def _get_reactions_from_comment(self, comment, repository):
for reaction in comment["reactions"]["nodes"]:
reaction["repository"] = self._get_name(repository)
reaction["repository"] = self._get_repository_name(repository)
reaction["comment_id"] = comment["id"]
if reaction["user"]:
reaction["user"]["type"] = "User"
@@ -1122,7 +1124,6 @@ class PullRequestCommentReactions(SemiIncrementalMixin, GithubStream):
yield from self._get_reactions_from_pull_request(pull_request, repository)
def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]:
self.raise_error_from_response(response_json=response.json())
data = response.json()["data"]
repository = data.get("repository")
if repository:
@@ -1180,11 +1181,6 @@ class PullRequestCommentReactions(SemiIncrementalMixin, GithubStream):
link_to_object[link], pageInfo["endCursor"], node[link]["totalCount"], parent_id=node.get("node_id")
)
def request_params(
self, stream_state: Mapping[str, Any], stream_slice: Mapping[str, Any] = None, next_page_token: Mapping[str, Any] = None
) -> MutableMapping[str, Any]:
return {}
def request_body_json(
self,
stream_state: Mapping[str, Any],

View File

@@ -0,0 +1,3 @@
{
"query": "query {\n repository(owner: \"airbytehq\", name: \"airbyte\") {\n name\n owner {\n login\n }\n projectsV2(first: 100, orderBy: {field: UPDATED_AT, direction: ASC}) {\n nodes {\n closed\n created_at: createdAt\n closed_at: closedAt\n updated_at: updatedAt\n creator: creator {\n avatarUrl\n login\n resourcePath\n url\n }\n node_id: id\n id: databaseId\n number\n public\n readme: readme\n short_description: shortDescription\n template\n title: title\n url: url\n viewerCanClose\n viewerCanReopen\n viewerCanUpdate\n owner {\n id: id\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n}"
}

View File

@@ -10,7 +10,7 @@ from unittest.mock import MagicMock, patch
import pytest
import requests
import responses
from airbyte_cdk.sources.streams.http.exceptions import BaseBackoffException
from airbyte_cdk.sources.streams.http.exceptions import BaseBackoffException, UserDefinedBackoffException
from requests import HTTPError
from responses import matchers
from source_github import constants
@@ -29,6 +29,7 @@ from source_github.streams import (
ProjectCards,
ProjectColumns,
Projects,
ProjectsV2,
PullRequestCommentReactions,
PullRequestCommits,
PullRequests,
@@ -1288,3 +1289,33 @@ def test_stream_pull_request_comment_reactions_read():
]
assert stream_state == {"airbytehq/airbyte": {"created_at": "2022-01-02T00:00:01Z"}}
@responses.activate
def test_stream_projects_v2_graphql_retry():
repository_args_with_start_date = {
"start_date": "2022-01-01T00:00:00Z",
"page_size_for_large_streams": 20,
"repositories": ["airbytehq/airbyte"],
}
stream = ProjectsV2(**repository_args_with_start_date)
resp = responses.add(responses.POST, "https://api.github.com/graphql", json={"errors": "not found"}, status=200, )
with patch.object(stream, "backoff_time", return_value=0.01), pytest.raises(UserDefinedBackoffException):
read_incremental(stream, stream_state={})
assert resp.call_count == stream.max_retries + 1
def test_stream_projects_v2_graphql_query():
repository_args_with_start_date = {
"start_date": "2022-01-01T00:00:00Z",
"page_size_for_large_streams": 20,
"repositories": ["airbytehq/airbyte"],
}
stream = ProjectsV2(**repository_args_with_start_date)
query = stream.request_body_json(stream_state={}, stream_slice={'repository': 'airbytehq/airbyte'})
f = Path(__file__).parent / "projects_v2_pull_requests_query.json"
expected_query = json.load(open(f))
assert query == expected_query

View File

@@ -101,6 +101,7 @@ This connector outputs the following incremental streams:
- [Project cards](https://docs.github.com/en/rest/reference/projects#list-project-cards)
- [Project columns](https://docs.github.com/en/rest/reference/projects#list-project-columns)
- [Projects](https://docs.github.com/en/rest/reference/projects#list-repository-projects)
- [ProjectsV2](https://docs.github.com/en/graphql/reference/objects#projectv2)
- [Pull request comment reactions](https://docs.github.com/en/rest/reference/reactions#list-reactions-for-a-pull-request-review-comment)
- [Pull request stats](https://docs.github.com/en/rest/reference/pulls#get-a-pull-request)
- [Pull requests](https://docs.github.com/en/rest/reference/pulls#list-pull-requests)
@@ -164,6 +165,7 @@ The GitHub connector should not run into GitHub API limitations under normal usa
| Version | Date | Pull Request | Subject |
|:--------|:-----------|:------------------------------------------------------------------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1.3.0 | 2023-08-25 | [30731](https://github.com/airbytehq/airbyte/pull/30731) | Add new stream `ProjectsV2` |
| 1.2.1 | 2023-08-22 | [30693](https://github.com/airbytehq/airbyte/pull/30693) | Handle 404 error in `TeamMemberShips` |
| 1.2.0 | 2023-08-22 | [30647](https://github.com/airbytehq/airbyte/pull/30647) | Add support for self-hosted GitHub instances |
| 1.1.1 | 2023-09-21 | [30654](https://github.com/airbytehq/airbyte/pull/30654) | Rewrite source connection error messages |