✨ Source GitHub: add stream ProjectsV2 (#30731)
Co-authored-by: artem1205 <artem1205@users.noreply.github.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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}"
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user