Skip to content

Commit f3c3f8c

Browse files
authored
feat:repo to team collaborator transformer (#26)
* add a repo to team collaborator transformer * chore:lint & poetry
1 parent 3dc674e commit f3c3f8c

File tree

7 files changed

+771
-599
lines changed

7 files changed

+771
-599
lines changed

nodestream_github/client/githubclient.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,3 +599,21 @@ async def fetch_user(self, *, username: str) -> types.GithubUser | None:
599599
except httpx.HTTPError as e:
600600
_fetch_problem(f"full user info for {username}", e)
601601
return None
602+
603+
async def fetch_teams_for_repo(self, *, owner_login: str, repo_name: str):
604+
"""
605+
Lists the teams that have access to the specified repository and that
606+
are also visible to the authenticated user.
607+
608+
For a public repository, a team is listed only if that team added the
609+
public repository explicitly.
610+
611+
https://docs.github.com/en/[email protected]/rest/repos/repos?apiVersion=2022-11-28#list-repository-teams
612+
"""
613+
try:
614+
async for team in self._get_paginated(
615+
f"repos/{owner_login}/{repo_name}/teams"
616+
):
617+
yield team
618+
except httpx.HTTPError as e:
619+
_fetch_problem(f"teams for repo {owner_login}/{repo_name}", e)

nodestream_github/transformer/repo.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
from abc import ABC
23
from collections.abc import AsyncGenerator
34
from typing import Any
45

@@ -13,7 +14,7 @@
1314
logger = get_plugin_logger(__name__)
1415

1516

16-
class RepoToCollaboratorsTransformer(Transformer):
17+
class RepoFullNameTransformer(Transformer, ABC):
1718
def __init__(
1819
self,
1920
*,
@@ -38,6 +39,14 @@ async def transform_record(
3839
else:
3940
logging.info("No full_name key found in record %s", record)
4041

42+
def _transform(self, full_name: str, simplified_repo: types.SimplifiedRepo):
43+
raise NotImplementedError
44+
45+
46+
class RepoToUserCollaboratorsTransformer(RepoFullNameTransformer):
47+
def __init__(self, *, full_name_key: str = "full_name", **kwargs: Any):
48+
super().__init__(full_name_key=full_name_key, **kwargs)
49+
4150
async def _transform(
4251
self,
4352
full_name: str,
@@ -66,3 +75,22 @@ async def _transform(
6675
"repository": simplified_repo,
6776
"affiliation": CollaboratorAffiliation.OUTSIDE,
6877
}
78+
79+
80+
class RepoToTeamCollaboratorsTransformer(RepoFullNameTransformer):
81+
async def _transform(
82+
self,
83+
full_name: str,
84+
simplified_repo: types.SimplifiedRepo,
85+
) -> AsyncGenerator[types.GithubTeam]:
86+
(repo_owner, repo_name) = full_name.split("/")
87+
88+
logging.debug("Transforming repo %s/%s", repo_owner, repo_name)
89+
90+
async for collaborator in self.client.fetch_teams_for_repo(
91+
owner_login=repo_owner, repo_name=repo_name
92+
):
93+
logging.debug("Found team %s", collaborator)
94+
yield collaborator | {
95+
"repository": simplified_repo,
96+
}

poetry.lock

Lines changed: 617 additions & 586 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "nodestream-plugin-github"
3-
version = "0.14.1-beta.6"
3+
version = "0.14.2"
44
description = ""
55
authors = [
66
"Jon Bristow <[email protected]>",
@@ -13,20 +13,20 @@ readme = "README.md"
1313

1414
[tool.poetry.dependencies]
1515
python = "^3.12"
16-
nodestream = "^0.14"
17-
limits = "5.2.0"
16+
nodestream = "^0.14.14"
17+
limits = "^5.5.0"
1818
tenacity = "^9.0.0"
1919
httpx = ">=0.27,<0.28"
20-
freezegun = "^1.5.4"
20+
freezegun = "^1.5.5"
2121

2222
[tool.poetry.group.dev.dependencies]
23-
ruff = "^0.11.0"
23+
ruff = "^0.12.10"
2424
black = "^25.1.0"
2525
isort = "^6.0.0"
26-
pytest = "^8.3.4"
27-
pytest-asyncio = "^0.26.0"
26+
pytest = "^8.4.1"
27+
pytest-asyncio = "^1.1.0"
2828
pytest-httpx = "^0.34.0"
29-
pytest-cov = "^6.0.0"
29+
pytest-cov = "^6.2.1"
3030
pytest-github-actions-annotate-failures = "^0.3.0"
3131

3232
[build-system]

tests/mocks/githubrest.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,3 +198,10 @@ def get_enterprise_audit_logs(self, *, search_phrase: Optional[str], **kwargs: A
198198
if search_phrase:
199199
url += f"&phrase={search_phrase}"
200200
self.add_response(url=url, **kwargs)
201+
202+
def get_teams_for_repo(self, *, owner_login: str, repo_name: str, **kwargs: Any):
203+
path = f"/repos/{owner_login}/{repo_name}/teams?per_page={self.per_page}"
204+
self.add_response(
205+
url=f"{self.base_url}{path}",
206+
**kwargs,
207+
)
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import pytest
2+
3+
from nodestream_github.interpretations.relationship.repository import simplify_repo
4+
from nodestream_github.transformer.repo import RepoToTeamCollaboratorsTransformer
5+
from tests.data.repos import HELLO_WORLD_REPO
6+
from tests.data.teams import JUSTICE_LEAGUE_TEAM_SUMMARY
7+
from tests.mocks.githubrest import DEFAULT_HOSTNAME, DEFAULT_PER_PAGE, GithubHttpxMock
8+
9+
REPO_TEAM_SUMMARY = {
10+
"id": 1,
11+
"node_id": "MDQ6VGVhbTE=",
12+
"url": "https://HOSTNAME/teams/1",
13+
"html_url": "https://github.com/orgs/github/teams/justice-league",
14+
"name": "Justice League",
15+
"slug": "justice-league",
16+
"description": "A great team.",
17+
"privacy": "closed",
18+
"notification_setting": "notifications_enabled",
19+
"permission": "admin",
20+
"members_url": "https://HOSTNAME/teams/1/members{/member}",
21+
"repositories_url": "https://HOSTNAME/teams/1/repos",
22+
"parent": None,
23+
}
24+
25+
26+
@pytest.mark.asyncio
27+
async def test_transform_records(gh_rest_mock: GithubHttpxMock):
28+
transformer = RepoToTeamCollaboratorsTransformer(
29+
auth_token="test-token",
30+
github_hostname=DEFAULT_HOSTNAME,
31+
user_agent="test-agent",
32+
max_retries=0,
33+
per_page=DEFAULT_PER_PAGE,
34+
)
35+
36+
gh_rest_mock.get_teams_for_repo(
37+
owner_login="octocat",
38+
repo_name="Hello-World",
39+
json=[REPO_TEAM_SUMMARY],
40+
)
41+
repo_summary = simplify_repo(HELLO_WORLD_REPO)
42+
43+
response = [r async for r in transformer.transform_record(HELLO_WORLD_REPO)]
44+
45+
assert response == [JUSTICE_LEAGUE_TEAM_SUMMARY | {"repository": repo_summary}]
46+
47+
48+
@pytest.mark.asyncio
49+
async def test_transform_records_alt_key(gh_rest_mock: GithubHttpxMock):
50+
transformer = RepoToTeamCollaboratorsTransformer(
51+
full_name_key="nameWithOwner",
52+
auth_token="test-token",
53+
github_hostname=DEFAULT_HOSTNAME,
54+
user_agent="test-agent",
55+
max_retries=0,
56+
per_page=DEFAULT_PER_PAGE,
57+
)
58+
59+
gh_rest_mock.get_teams_for_repo(
60+
owner_login="octocat",
61+
repo_name="Hello-World",
62+
json=[JUSTICE_LEAGUE_TEAM_SUMMARY],
63+
)
64+
65+
modified_repo = HELLO_WORLD_REPO | {"nameWithOwner": "octocat/Hello-World"}
66+
del modified_repo["full_name"]
67+
repo_summary = simplify_repo(modified_repo)
68+
69+
response = [r async for r in transformer.transform_record(modified_repo)]
70+
71+
assert response == [JUSTICE_LEAGUE_TEAM_SUMMARY | {"repository": repo_summary}]
72+
73+
74+
@pytest.mark.asyncio
75+
async def test_no_full_name_key():
76+
transformer = RepoToTeamCollaboratorsTransformer(
77+
full_name_key="full_name",
78+
auth_token="test-token",
79+
github_hostname=DEFAULT_HOSTNAME,
80+
user_agent="test-agent",
81+
max_retries=0,
82+
per_page=DEFAULT_PER_PAGE,
83+
)
84+
modified_repo = HELLO_WORLD_REPO.copy()
85+
del modified_repo["full_name"]
86+
87+
response = [r async for r in transformer.transform_record(modified_repo)]
88+
assert response == []

tests/transformer/test_repo.py renamed to tests/transformer/test_repo_user_collab.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import pytest
22

33
from nodestream_github.interpretations.relationship.repository import simplify_repo
4-
from nodestream_github.transformer.repo import RepoToCollaboratorsTransformer
4+
from nodestream_github.transformer.repo import RepoToUserCollaboratorsTransformer
55
from nodestream_github.types.enums import CollaboratorAffiliation
66
from tests.data.repos import HELLO_WORLD_REPO
77
from tests.data.users import OCTOCAT_USER_SHORT, TURBO_USER_SHORT
@@ -10,7 +10,7 @@
1010

1111
@pytest.mark.asyncio
1212
async def test_transform_records(gh_rest_mock: GithubHttpxMock):
13-
transformer = RepoToCollaboratorsTransformer(
13+
transformer = RepoToUserCollaboratorsTransformer(
1414
auth_token="test-token",
1515
github_hostname=DEFAULT_HOSTNAME,
1616
user_agent="test-agent",
@@ -43,7 +43,7 @@ async def test_transform_records(gh_rest_mock: GithubHttpxMock):
4343

4444
@pytest.mark.asyncio
4545
async def test_transform_records_alt_key(gh_rest_mock: GithubHttpxMock):
46-
transformer = RepoToCollaboratorsTransformer(
46+
transformer = RepoToUserCollaboratorsTransformer(
4747
full_name_key="nameWithOwner",
4848
auth_token="test-token",
4949
github_hostname=DEFAULT_HOSTNAME,
@@ -79,7 +79,7 @@ async def test_transform_records_alt_key(gh_rest_mock: GithubHttpxMock):
7979

8080
@pytest.mark.asyncio
8181
async def test_no_full_name_key():
82-
transformer = RepoToCollaboratorsTransformer(
82+
transformer = RepoToUserCollaboratorsTransformer(
8383
full_name_key="full_name",
8484
auth_token="test-token",
8585
github_hostname=DEFAULT_HOSTNAME,

0 commit comments

Comments
 (0)