def test_missing_items(): config = Configuration(os.path.join(MOCK_FOLDER_PATH, 'conf.ini')) config.load_properties() project = Project( name="test project", config=config, columns={ "Queue": ProjectColumn(id="id", name="Queue", cards=[ItemCard(id="sdf", item=Issue(id="sdf", title="title", number=1))]), "Review in progress": ProjectColumn(id="id", name="Review in progress", cards=[]) } ) assert len(project.get_all_item_ids()) == 1 assert len(project.columns.keys()) == 2 issue = Issue( id="2", number=2, title="issue title" ) issues = { "2": issue } assert project.find_missing_item_ids(issues) == {"2"}
def test_matching_issue_filter(): config = Configuration(os.path.join(MOCK_FOLDER_PATH, 'conf.ini')) config.load_properties() assert is_matching_issue(['test', 'bug'], config.must_have_labels, config.cant_have_labels, config.filter_labels) is True assert is_matching_issue(['not test', 'bug'], config.must_have_labels, config.cant_have_labels, config.filter_labels) is False assert is_matching_issue(['not test', 'test', 'bug'], config.must_have_labels, config.cant_have_labels, config.filter_labels) is False config.filter_labels = ['not bug'] assert is_matching_issue(['bug', 'test'], config.must_have_labels, config.cant_have_labels, config.filter_labels) is False assert is_matching_issue(['not bug', 'test'], config.must_have_labels, config.cant_have_labels, config.filter_labels) is True config.must_have_labels = ['test||something'] assert is_matching_issue(['not bug', 'test'], config.must_have_labels, config.cant_have_labels, config.filter_labels) is True assert is_matching_issue(['not bug', 'something'], config.must_have_labels, config.cant_have_labels, config.filter_labels) is True assert is_matching_issue(['not bug', 'else'], config.must_have_labels, config.cant_have_labels, config.filter_labels) is False
def test_removing_items(): config = Configuration(os.path.join(MOCK_FOLDER_PATH, 'conf.ini')) config.load_properties() project = Project( name="test project", config=config, columns={ "Queue": ProjectColumn(id="id", name="Queue", cards=[ItemCard(id="sdf", item=Issue(id="sdf", title="title", number=1)), ItemCard(id="sdff", item=PullRequest(id="sdff", title="title", number=2))]), "Review in progress": ProjectColumn(id="id", name="Review in progress", cards=[]) } ) assert len(project.get_all_item_ids()) == 2 assert len(project.columns.keys()) == 2 class ClientMock(object): def delete_project_card(self, **kwargs): return project.remove_items(ClientMock, config) assert project.get_all_item_ids() == set()
def test_sort_column(): config = Configuration(os.path.join(MOCK_FOLDER_PATH, 'conf.ini')) config.load_properties() class MockClient(object): def add_to_column(*args, **kwargs): return def move_to_specific_place_in_column(*args, **kwargs): return mock_client = MockClient() # Sorting config.priority_list = DEFAULT_PRIORITY_LIST column_object = ProjectColumn( id="id", name="Review in progress", cards=[ ItemCard(id="sdf", item=Issue(id="sdf", title="issue 2", number=2, labels=["Low"])), ItemCard(id="sdf3", item=Issue(id="sdf", title="issue 4", number=4, labels=["Medium"])), ItemCard(id="sdf2", item=PullRequest(id="sdf2", title="pull request 3", number=3, labels=["High"])), ]) column_object.sort_cards(mock_client, config) card_titles_in_column = [card.item.title for card in column_object.cards] assert card_titles_in_column == ['pull request 3', "issue 4", 'issue 2']
def test_loading_configuration(): configuration = Configuration(os.path.join(MOCK_FOLDER_PATH, 'conf.ini'), quiet=True, log_path="/tmp/") assert 'General' in configuration.config.sections() configuration.load_properties() assert configuration.closed_issues_column == 'Done' assert configuration.project_owner == 'ronykoz' assert configuration.repository_name == 'test' assert configuration.project_number == 1 assert configuration.priority_list == [ 'Critical', 'High', 'Medium', 'Low', 'Customer|||zendesk' ] assert configuration.filter_labels == ['bug'] assert configuration.filter_milestone == '' assert configuration.must_have_labels == ['test'] assert configuration.cant_have_labels == ['not test'] assert configuration.column_names == [ 'Queue', 'In progress', 'Review in progress', 'Waiting for Docs' ] assert configuration.column_rule_desc_order == [ 'Queue', 'Waiting for Docs', 'Review in progress', 'In progress' ] assert configuration.remove is True assert configuration.add is True assert configuration.move is True assert configuration.sort is False assert configuration.column_to_rules['Waiting for Docs'][ 'issue.pull_request.review_requested'] is True assert configuration.column_to_rules['Waiting for Docs'][ 'issue.pull_request.assignees'] == ['ronykoz||not rony']
def test_adding_issue(): config = Configuration(os.path.join(MOCK_FOLDER_PATH, 'conf.ini')) config.load_properties() project = Project(name="test project", config=config, columns={ "Queue": ProjectColumn(id="id", name="Queue", cards=[ IssueCard(id="sdf", issue=Issue( id="sdf", title="title", number=2)) ]), "Review in progress": ProjectColumn(id="id", name="Review in progress", cards=[]) }) assert len(project.get_all_issue_ids()) == 1 assert len(project.columns.keys()) == 2 issue = Issue(id="1", number=1, title="Rony") assert issue.priority_rank == 0 issues = {"1": issue} class ClientMock(object): def add_issues_to_project(*args, **kwargs): return {'addProjectCard': {'cardEdge': {'node': {'id': "1"}}}} def add_to_column(*args, **kwargs): return def move_to_specific_place_in_column(*args, **kwargs): return project.add_issues(ClientMock, issues, {"1"}, config) assert project.columns['Queue'].cards[0].issue.title == "Rony" # testing non existent column with pytest.raises(Exception) as err: project.add_issue(ClientMock, issue, "non existent", config) assert "Did not found a matching column" in err issue2 = Issue(id="1", number=1, title="Rony", card_id_to_project={"1": { "project_number": 1 }}) issues2 = {"2": issue2} project.add_issues(ClientMock, issues2, {"2"}, config) assert project.columns['Queue'].cards[0].issue.title == "Rony"
def test_get_matching_column(): config = Configuration(os.path.join(MOCK_FOLDER_PATH, 'conf.ini')) config.load_properties() issue_queue = Issue(id="1", title="issue 1", number=1) assert Project.get_matching_column(issue_queue, config) == 'Queue' issue_in_progress = Issue(id="1", title="issue 1", number=1) issue_in_progress.add_assignee("Rony") assert Project.get_matching_column(issue_in_progress, config) == '' issue_in_progress.add_label("Testing") assert Project.get_matching_column(issue_in_progress, config) == 'In progress' issue_review_in_progress = Issue(id="1", title="issue 1", number=1) issue_review_in_progress.add_assignee("Rony") class MockPullRequest(object): review_requested = True review_completed = False issue_review_in_progress.pull_request = MockPullRequest() assert Project.get_matching_column(issue_review_in_progress, config) == 'Review in progress' issue_docs = Issue(id="1", title="issue 1", number=1) issue_docs.add_assignee("Rony") class MockPullRequest2(object): review_requested = True review_completed = True assignees = "ronykoz" issue_docs.pull_request = MockPullRequest2() assert Project.get_matching_column(issue_docs, config) == 'Waiting for Docs' class MockPullRequest3(object): review_requested = True review_completed = True assignees = "someone" issue_docs.pull_request = MockPullRequest3() assert Project.get_matching_column(issue_docs, config) == 'Review in progress' # faulty field from issues config.column_to_rules["Waiting for Docs"] = { "issue.not_existent": "field" } assert Project.get_matching_column(issue_docs, config) == 'Review in progress'
def manage(**kwargs): """Manage a GitHub project board""" for conf_path in kwargs['conf'].split(','): configuration = Configuration(conf_file_path=conf_path, verbose=kwargs['verbose'], quiet=kwargs['quiet'], log_path=kwargs['log_path']) configuration.load_properties() configuration.logger.info(f'Starting going over the board {conf_path}') manager = ProjectManager(configuration=configuration) manager.manage()
def test_loading_illegal_configuration(): configuration = Configuration( os.path.join(MOCK_FOLDER_PATH, 'illegal_conf.ini')) with pytest.raises(ValueError) as exception: configuration.load_properties() assert 'You have either added a section which is not in the column_names key' in exception configuration = Configuration( os.path.join(MOCK_FOLDER_PATH, 'illegal_query_conf.ini')) with pytest.raises(ValueError) as exception: configuration.load_properties() assert 'You have entered an illegal query' in exception configuration = Configuration( os.path.join(MOCK_FOLDER_PATH, 'illegal_action_conf.ini')) with pytest.raises(ValueError) as exception: configuration.load_properties() assert 'Provided illegal key' in exception assert 'in Actions section' in exception configuration = Configuration( os.path.join(MOCK_FOLDER_PATH, 'illegal_general_conf.ini')) with pytest.raises(ValueError) as exception: configuration.load_properties() assert 'Provided illegal key' in exception assert 'in General section' in exception
def test_event_manager_flow(mocker): config = Configuration(os.path.join(MOCK_FOLDER_PATH, 'conf.ini')) config.load_properties() project_object = Project(name="project_name", columns={ "In progress": ProjectColumn(id="some id", name='In progress', cards=[]) }, config=config) mocker.patch.object(EventManager, "get_issue_object", return_value=Issue(id="1", title="this is a test title", number=1, assignees=["ronykoz"], labels=['test', 'Testing', 'bug'])) mocker.patch.object(EventManager, "load_project_column", return_value=project_object) class MockClient(object): def add_issues_to_project(*args, **kwargs): return {"addProjectCard": {"cardEdge": {"node": {"id": "1"}}}} def add_to_column(self, **kwargs): return def move_to_specific_place_in_column(self, **kwargs): return client = MockClient() manager = EventManager(os.path.join(MOCK_FOLDER_PATH, 'conf.ini'), client=client, event=json.dumps({"text": "text"})) manager.run() assert len(project_object.get_all_issue_ids()) == 1
def test_move_issues(): config = Configuration(os.path.join(MOCK_FOLDER_PATH, 'conf.ini')) config.load_properties() issue = Issue(id="1", title="title", number=1, assignees=["Rony"], labels=["Testing"], card_id_to_project={"sdf": { "project_number": 1 }}) project = Project(name="test project", config=config, columns={ "Queue": ProjectColumn( id="id", name="Queue", cards=[IssueCard(id="sdf", issue=issue)]), "In progress": ProjectColumn(id="id", name="In progress", cards=[]) }) class MockClient(object): def add_to_column(*args, **kwargs): return def move_to_specific_place_in_column(*args, **kwargs): return project.move_issues(MockClient(), config) assert project.is_in_column("Queue", "1") is False assert project.is_in_column("In progress", "1") is True # Move within the same column project.move_issues(MockClient(), config) assert project.is_in_column("Queue", "1") is False assert project.is_in_column("In progress", "1") is True
def test_get_filters(): config = Configuration(os.path.join(MOCK_FOLDER_PATH, 'conf.ini')) config.load_properties() filters = ProjectManager.get_filters(config) assert len(filters) == 1 assert ' label:bug' in filters[0] assert ' label:test' in filters[0] assert '-label:not test' in filters[0] config.filter_labels = ['one', 'two'] config.must_have_labels = ['three', 'four||five'] config.cant_have_labels = ['six', 'seven'] filters = ProjectManager.get_filters(config) assert len(filters) == 4 for filter_str in filters: assert ('label:one' in filter_str and 'label:two' not in filter_str) or ('label:one' not in filter_str and 'label:two' in filter_str) assert ('label:four' in filter_str and 'label:five' not in filter_str) or ('label:four' not in filter_str and 'label:five' in filter_str)
class EventManager(object): DEFAULT_PRIORITY_LIST = ['Critical', 'High', 'Medium', 'Low'] def __init__(self, conf: str, event: str, quiet: bool = False, log_path: str = '', verbose: int = 2, client=None, api_key=None): self.config = None self.conf_paths = conf.split(',') self.quiet = quiet self.log_path = log_path self.verbose = verbose config = Configuration(self.conf_paths[0]) config.load_general_properties() self.project_owner = config.project_owner self.repository_name = config.repository_name self.event = json.loads(event) self.client = client if client else GraphQLClient(api_key) def get_prev_column_cursor(self, column_name): layout = self.client.get_project_layout( owner=self.config.project_owner, repository_name=self.config.repository_name, project_number=self.config.project_number, is_org_project=self.config.is_org_project) prev_cursor = '' project = get_project_from_response(layout, self.config.is_org_project) column_edges = project.get('columns', {}).get('edges', {}) if project else {} for index, column in enumerate(column_edges): if column_name == column['node']['name']: if index != 0: prev_cursor = column_edges[index - 1]['cursor'] return prev_cursor def load_project_column(self, column_name): prev_cursor = self.get_prev_column_cursor(column_name) if prev_cursor: response = get_column_items_with_prev_column( self.client, self.config, prev_cursor) else: response = get_first_column_items(self.client, self.config) project = get_project_from_response(response, self.config.is_org_project) return Project(**parse_project(project, config=self.config)) def manage_item_in_project(self, item): if (self.config.remove and self.config.project_number in item.get_associated_project() and not is_matching_project_item( item.labels, self.config.must_have_labels, self.config.cant_have_labels, self.config.filter_labels)): card_id = [ _id for _id, value in item.card_id_project.items() if value['project_number'] == self.config.project_number ][0] Project.remove_item(self.client, item.title, card_id, self.config, item) return matching_column_name = Project.get_matching_column(item, self.config) if self.config.add and self.config.project_number not in item.get_associated_project( ): project = self.load_project_column(matching_column_name) project.add_item(self.client, item, matching_column_name, self.config) return column_name_before = [ value['project_column'] for _id, value in item.card_id_project.items() if value['project_number'] == self.config.project_number ][0] if (self.config.add and not column_name_before) or \ (self.config.move and matching_column_name != column_name_before): print(f'Moving {item.title} from {column_name_before}') project = self.load_project_column(matching_column_name) project.move_item(self.client, item, matching_column_name, self.config) return if self.config.sort and column_name_before == matching_column_name: project = self.load_project_column(matching_column_name) project.columns[matching_column_name].sort_cards( self.client, self.config) return def get_project_item_object(self): """Get the issue or pull request full representation from the API""" if 'issue' in self.event: issue_number = self.event['issue']['number'] issue_response = self.client.get_issue( self.project_owner, self.repository_name, issue_number) # need to address the remove here issue = Issue(**parse_issue(issue_response['repository']['issue'])) return issue elif 'pull_request' in self.event: pr_number = self.event['pull_request']['number'] pr_response = self.client.get_pull_request(self.project_owner, self.repository_name, pr_number) pr = PullRequest( **parse_pull_request(pr_response['repository']['pullRequest'])) return pr else: print("This is not an issue or a pull request.") return def run(self): item = self.get_project_item_object() if item is None: return # In case the event is not for an issue / pull request if item.state and item.state.upper() in ('CLOSED', 'MERGED'): print(f"The item is {item.state.lower()}, not taking an action.") return for conf_path in self.conf_paths: self.config = Configuration(conf_path, self.verbose, self.quiet, self.log_path) self.config.load_properties() if (self.config.project_number in item.get_associated_project() or is_matching_project_item(item.labels, self.config.must_have_labels, self.config.cant_have_labels, self.config.filter_labels)): item.set_priority(self.config.priority_list) self.manage_item_in_project(item) else: self.config.logger.debug( f"The issue does not match the filter provided in the configuration " f"file {conf_path}.")
def test_add_card_to_column(): config = Configuration(os.path.join(MOCK_FOLDER_PATH, 'conf.ini')) config.load_properties() config.priority_list = DEFAULT_PRIORITY_LIST column_object = ProjectColumn(id="id", name="Review in progress", cards=[ ItemCard(id="sdf", item=Issue(id="sdf", title="issue 2", number=2, labels=["Low"])), ItemCard(id="sdf2", item=Issue(id="sdf2", title="issue 3", number=3, labels=["High"])) ]) issue_to_inject = Issue( id="4", title="issue 4", number=4, labels=['Medium'] ) issue_to_inject.set_priority(DEFAULT_PRIORITY_LIST) class MockClient(object): def add_to_column(*args, **kwargs): return def move_to_specific_place_in_column(*args, **kwargs): return mock_client = MockClient() # Sorting column_object.sort_cards(mock_client, config) card_titles_in_column = [card.item.title for card in column_object.cards] assert card_titles_in_column == ['issue 3', 'issue 2'] # Adding in the middle column_object.add_card("id", issue_to_inject, mock_client) card_titles_in_column = [card.item.title for card in column_object.cards] assert card_titles_in_column == ['issue 3', 'issue 4', 'issue 2'] # Higher priority addition issue_to_inject2 = Issue( id="5", title="issue 5", number=5, labels=['Critical'] ) issue_to_inject2.set_priority(DEFAULT_PRIORITY_LIST) column_object.add_card("id", issue_to_inject2, mock_client) card_titles_in_column = [card.item.title for card in column_object.cards] assert card_titles_in_column == ['issue 5', 'issue 3', 'issue 4', 'issue 2'] # Lower priority addition pr_to_inject3 = PullRequest( id="6", title="pull request 6", number=6 ) column_object.add_card("id", pr_to_inject3, mock_client) card_titles_in_column = [card.item.title for card in column_object.cards] assert card_titles_in_column == ['issue 5', 'issue 3', 'issue 4', 'issue 2', "pull request 6"] # Same priority different number pr_to_inject4 = PullRequest( id="7", title="pull request 7", number=-1, ) column_object.add_card("id", pr_to_inject4, mock_client) card_titles_in_column = [card.item.title for card in column_object.cards] assert card_titles_in_column == ['issue 5', 'issue 3', 'issue 4', 'issue 2', "pull request 7", "pull request 6"]
def test_move_items(): config = Configuration(os.path.join(MOCK_FOLDER_PATH, 'conf.ini')) config.load_properties() issue = Issue(id="1", title="title", number=1, assignees=["Rony"], labels=["Testing"], card_id_to_project={ "sdf": { "project_number": 1 } } ) pull_request = PullRequest(id="10", title="title", number=10, assignees=["Daud"], labels=["Testing"], card_id_to_project={ "dsf": { "project_number": 1 } }) project = Project( name="test project", config=config, columns={ "Queue": ProjectColumn(id="id", name="Queue", cards=[ItemCard(id="sdf", item=issue), ItemCard(id="dsf", item=pull_request) ]), "In progress": ProjectColumn(id="id", name="In progress", cards=[]) } ) class MockClient(object): def add_to_column(*args, **kwargs): return def move_to_specific_place_in_column(*args, **kwargs): return project.move_items(MockClient(), config, {'1': issue}) assert project.is_in_column("Queue", "1") is False assert project.is_in_column("In progress", "1") is True project.move_items(MockClient(), config, {'10': pull_request}) assert project.is_in_column("Queue", "10") is False assert project.is_in_column("In progress", "10") is True # Move within the same column project.move_items(MockClient(), config, {'1': issue}) assert project.is_in_column("Queue", "1") is False assert project.is_in_column("In progress", "1") is True issue.state = 'closed' project.move_item(MockClient(), issue, 'In progress', config) assert project.is_in_column("In progress", "1") is True
def test_loading(): project_layout = { "repository": { "project": { "columns": { "edges": [{ "cursor": 1, "node": { "name": "Queue" } }, { "cursor": 2, "node": { "name": "Review in progress" } }] } } } } column1 = { "repository": { "project": { "name": "test", "columns": { "nodes": [{ "name": "Queue", "id": "1234", "cards": { "pageInfo": { "hasNextPage": False, "endCursor": "MQ" }, "edges": [{ "cursor": "MQ", "node": { "note": None, "state": "CONTENT_ONLY", "id": "3434=", "content": { "id": "1234=", "number": 1, "title": "issue 1", "labels": { "edges": [{ "node": { "name": "High" } }, { "node": { "name": "bug" } }] }, "assignees": { "edges": [] } } } }] } }] } } } } column2 = { "repository": { "project": { "name": "test", "columns": { "nodes": [{ "name": "In progress", "id": "5656", "cards": { "pageInfo": { "hasNextPage": False, "endCursor": "MQ" }, "edges": [{ "cursor": "MQ", "node": { "note": None, "state": "CONTENT_ONLY", "id": "56565=", "content": { "id": "56567=", "number": 15, "title": "issue 2", "labels": { "edges": [{ "node": { "name": "Medium" } }, { "node": { "name": "bug" } }] }, "assignees": { "edges": [{ "node": { "id": "234", "login": "******" } }] } } } }, { "cursor": "MB", "node": { "note": None, "state": "CONTENT_ONLY", "id": "123=", "content": { "id": "1234=", "number": 3, "title": "issue 3", "labels": { "edges": [{ "node": { "name": "High" } }, { "node": { "name": "bug" } }] }, "assignees": { "edges": [{ "node": { "id": "234", "login": "******" } }] } } } }] } }] } } } } issue_id = "=asdf=sdf=" title = "issue name" labels = ["HighEffort", "Critical", "bug", "test", "Testing"] issue = { "projectCards": { "nodes": [{ "id": "id=", "project": { "number": 1, "columns": { "nodes": [{ "name": "testing" }] } } }, { "id": "id2=", "project": { "number": 2, "columns": { "nodes": [{ "name": "Queue" }] } } }] }, "comments": { "nodes": [{ "author": { "login": "******" }, "body": "comment 1", "createdAt": "2019-03-19T12:24:27Z" }, { "author": { "login": "******" }, "body": "second comment", "createdAt": "2019-03-19T12:27:53Z" }, { "author": { "login": "******" }, "body": "third comment", "createdAt": "2019-03-19T12:52:08Z" }] }, "timelineItems": { "__typename": "IssueTimelineItemsConnection", "nodes": [{ "__typename": "LabeledEvent", "label": { "name": labels[0] }, "createdAt": "2019-03-15T12:40:22Z" }, { "__typename": "LabeledEvent", "label": { "name": labels[1] }, "createdAt": "2019-03-17T13:59:27Z" }, { "__typename": "LabeledEvent", "label": { "name": labels[2] }, "createdAt": "2019-04-08T10:48:02Z" }] }, "title": title, "id": issue_id, "number": 1, "milestone": { "title": "test" }, "labels": { "edges": [{ "node": { "name": labels[0] } }, { "node": { "name": labels[1] } }, { "node": { "name": labels[2] } }, { "node": { "name": labels[3] } }, { "node": { "name": labels[4] } }] }, "assignees": { "edges": [{ "node": { "login": "******" } }] } } issues = { "search": { "pageInfo": { "hasNextPage": True, "endCursor": "cursor" }, "edges": [{ "node": issue }] } } issues_with_no_after = { "search": { "pageInfo": { "hasNextPage": False, "endCursor": "cursor" }, "edges": [{ "node": issue }] } } config = Configuration(os.path.join(MOCK_FOLDER_PATH, 'conf.ini')) config.load_properties() config.sort = True class MockClient(object): def delete_project_card(*args, **kwargs): return def add_issues_to_project(self, **kwargs): return {"addProjectCard": {"cardEdge": {"node": {"id": "id="}}}} def get_project_layout(self, **kwargs): return project_layout def get_first_column_issues(self, **kwargs): return column1 def get_column_issues(self, **kwargs): return column2 def search_issues_by_query(self, **kwargs): if 'start_cursor' not in kwargs: return issues return issues_with_no_after def add_to_column(self, **kwargs): return def move_to_specific_place_in_column(self, **kwargs): return client = MockClient() manager = ProjectManager(configuration=config, client=client) assert len(manager.matching_issues) == 1 assert manager.project.name == 'test' assert manager.project.get_current_location("56567=") == ("In progress", "56565=") assert manager.project.get_current_location("Random text") == (None, None) assert manager.project.is_in_column("In progress", "56567=") is True assert manager.project.is_in_column("In progress", "Random text") is False manager.project.sort_issues_in_columns(client, config) issues = [ card.issue.title for card in manager.project.columns['In progress'].cards ] assert issues == ['issue 3', 'issue 2'] manager.project.columns['In progress'].remove_card("56565=") assert manager.project.is_in_column("In progress", "56567=") is False manager.manage() assert manager.project.is_in_column("In progress", issue_id) is True assert manager.project.columns["In progress"].cards[0].issue.id == issue_id
class EventManager(object): DEFAULT_PRIORITY_LIST = ['Critical', 'High', 'Medium', 'Low'] def __init__(self, conf: str, event: str, quiet: bool = False, log_path: str = '', verbose: int = 2, client=None, api_key=None): self.config = None self.conf_paths = conf.split(',') self.quiet = quiet self.log_path = log_path self.verbose = verbose config = Configuration(self.conf_paths[0]) config.load_general_properties() self.project_owner = config.project_owner self.repository_name = config.repository_name self.event = json.loads(event) self.client = client if client else GraphQLClient(api_key) @staticmethod def get_issue_number(event): return event['issue']['number'] def get_prev_column_cursor(self, column_name): layout = self.client.get_project_layout(owner=self.config.project_owner, repository_name=self.config.repository_name, project_number=self.config.project_number) prev_cursor = '' column_edges = layout['repository']['project']['columns']['edges'] for index, column in enumerate(column_edges): if column_name == column['node']['name']: if index != 0: prev_cursor = column_edges[index - 1]['cursor'] return prev_cursor def load_project_column(self, column_name): prev_cursor = self.get_prev_column_cursor(column_name) if prev_cursor: response = get_column_issues_with_prev_column(self.client, self.config, prev_cursor) else: response = get_first_column_issues(self.client, self.config) return Project(**parse_project(response.get("repository", {}).get('project', {}), config=self.config)) def manage_issue_in_project(self, issue): if (self.config.remove and self.config.project_number in issue.get_associated_project() and not is_matching_issue(issue.labels, self.config.must_have_labels, self.config.cant_have_labels, self.config.filter_labels)): card_id = [_id for _id, value in issue.card_id_project.items() if value['project_number'] == self.config.project_number][0] Project.remove_issue(self.client, issue.title, card_id, self.config) return matching_column_name = Project.get_matching_column(issue, self.config) if self.config.add and self.config.project_number not in issue.get_associated_project(): project = self.load_project_column(matching_column_name) project.add_issue(self.client, issue, matching_column_name, self.config) return column_name_before = [value['project_column'] for _id, value in issue.card_id_project.items() if value['project_number'] == self.config.project_number][0] if (self.config.add and not column_name_before) or \ (self.config.move and matching_column_name != column_name_before): project = self.load_project_column(matching_column_name) project.move_issue(self.client, issue, matching_column_name, self.config) return if self.config.sort and column_name_before == matching_column_name: project = self.load_project_column(matching_column_name) project.columns[matching_column_name].sort_cards(self.client, self.config) return def get_issue_object(self): issue_number = self.get_issue_number(self.event) issue_response = self.client.get_issue( self.project_owner, self.repository_name, issue_number) # need to address the remove here issue = Issue(**parse_issue(issue_response['repository']['issue'])) return issue def run(self): issue = self.get_issue_object() for conf_path in self.conf_paths: self.config = Configuration(conf_path, self.verbose, self.quiet, self.log_path) self.config.load_properties() if (self.config.project_number in issue.get_associated_project() or is_matching_issue(issue.labels, self.config.must_have_labels, self.config.cant_have_labels, self.config.filter_labels)): issue.set_priority(self.config.priority_list) self.manage_issue_in_project(issue) else: self.config.logger.debug(f"The issue does not match the filter provided in the configuration " f"file {conf_path}.")
def test_loading_repo_project(): project_layout = { "repository": { "project": { "columns": { "edges": [{ "cursor": 1, "node": { "name": "Queue" } }, { "cursor": 2, "node": { "name": "Review in progress" } }] } } } } column1 = { "repository": { "project": { "name": "test", "columns": { "nodes": [{ "name": "Queue", "id": "1234", "cards": { "pageInfo": { "hasNextPage": False, "endCursor": "MQ" }, "edges": [{ "cursor": "MQ", "node": { "note": None, "state": "CONTENT_ONLY", "id": "3434=", "content": { "__typename": "Issue", "id": "1234=", "number": 1, "title": "issue 1", "labels": { "edges": [{ "node": { "name": "High" } }, { "node": { "name": "bug" } }] }, "assignees": { "edges": [] } } } }] } }] } } } } column2 = { "repository": { "project": { "name": "test", "columns": { "nodes": [{ "name": "In progress", "id": "5656", "cards": { "pageInfo": { "hasNextPage": False, "endCursor": "MQ" }, "edges": [{ "cursor": "MQ", "node": { "note": None, "state": "CONTENT_ONLY", "id": "56565=", "content": { "__typename": "Issue", "id": "56567=", "number": 15, "title": "issue 2", "labels": { "edges": [{ "node": { "name": "Medium" } }, { "node": { "name": "bug" } }] }, "assignees": { "edges": [{ "node": { "id": "234", "login": "******" } }] } } } }, { "cursor": "MB", "node": { "note": None, "state": "CONTENT_ONLY", "id": "123=", "content": { "__typename": "Issue", "id": "1234=", "number": 3, "title": "issue 3", "labels": { "edges": [{ "node": { "name": "High" } }, { "node": { "name": "bug" } }] }, "assignees": { "edges": [{ "node": { "id": "234", "login": "******" } }] } } } }, { "cursor": "HZ", "node": { "note": None, "state": "CONTENT_ONLY", "id": "76565=", "content": { "__typename": "PullRequest", "title": "pull_request 1", "id": "76566=", "state": "OPEN", "number": 20, "mergedAt": None, "merged": False, "reviewDecision": None, "reviews": { "totalCount": 0 }, "reviewRequests": { "totalCount": 0 }, "labels": { "edges": [{ "node": { "name": "Medium" } }, { "node": { "name": "bug" } }] }, "assignees": { "edges": [{ "node": { "id": "334", "login": "******" } }] } } } }] } }] } } } } pr_id = "=zxcx=sads=" pr_title = "pr name" labels = ["HighEffort", "Critical", "bug", "test", "Testing"] assignee = "daud" pull_request = { "projectCards": { "nodes": [{ "id": "id=", "column": { "name": "testing" }, "project": { "number": 1 } }, { "id": "id2=", "column": { "name": "Queue" }, "project": { "number": 2 } }] }, "title": pr_title, "id": pr_id, "number": 1, "state": "MERGED", "mergedAt": "2021-01-25T15:27:08Z", "merged": True, "reviewDecision": None, "reviews": { "totalCount": 0 }, "reviewRequests": { "totalCount": 1 }, "labels": { "edges": [{ "node": { "name": labels[0] } }, { "node": { "name": labels[1] } }, { "node": { "name": labels[2] } }, { "node": { "name": labels[3] } }] }, "assignees": { "edges": [{ "node": { "login": assignee } }] } } issue_id = "=asdf=sdf=" issue_title = "issue name" issue = { "projectCards": { "nodes": [{ "id": "id=", "column": { "name": "Queue" }, "project": { "number": 1 } }, { "id": "id2=", "column": { "name": "In progress" }, "project": { "number": 2 } }] }, "comments": { "nodes": [{ "author": { "login": "******" }, "body": "comment 1", "createdAt": "2019-03-19T12:24:27Z" }, { "author": { "login": "******" }, "body": "second comment", "createdAt": "2019-03-19T12:27:53Z" }, { "author": { "login": "******" }, "body": "third comment", "createdAt": "2019-03-19T12:52:08Z" }] }, "timelineItems": { "__typename": "IssueTimelineItemsConnection", "nodes": [{ "__typename": "LabeledEvent", "label": { "name": labels[0] }, "createdAt": "2019-03-15T12:40:22Z" }, { "__typename": "LabeledEvent", "label": { "name": labels[1] }, "createdAt": "2019-03-17T13:59:27Z" }, { "__typename": "LabeledEvent", "label": { "name": labels[2] }, "createdAt": "2019-04-08T10:48:02Z" }] }, "title": issue_title, "id": issue_id, "number": 1, "milestone": { "title": "test" }, "labels": { "edges": [{ "node": { "name": labels[0] } }, { "node": { "name": labels[1] } }, { "node": { "name": labels[2] } }, { "node": { "name": labels[3] } }, { "node": { "name": labels[4] } }] }, "assignees": { "edges": [{ "node": { "login": "******" } }] } } items = { "repository": { "issues": { "pageInfo": { "hasNextPage": True, "endCursor": "cursor" }, "edges": [{ "node": issue }] } } } issues_with_no_after = { "repository": { "issues": { "pageInfo": { "hasNextPage": False, "endCursor": "cursor" }, "edges": [{ "node": issue }] } } } config = Configuration(os.path.join(MOCK_FOLDER_PATH, 'conf.ini')) config.load_properties() config.sort = True class MockClient(object): def delete_project_card(*args, **kwargs): return def add_issues_to_project(self, **kwargs): return {"addProjectCard": {"cardEdge": {"node": {"id": "id="}}}} def get_project_layout(self, **kwargs): return project_layout def get_first_column_items(self, **kwargs): return column1 def get_column_items(self, **kwargs): return column2 def get_github_issues(self, **kwargs): if not kwargs['after']: return items return issues_with_no_after def add_to_column(self, **kwargs): return def move_to_specific_place_in_column(self, **kwargs): return def get_github_pull_requests(self, **kwargs): return { "repository": { "pullRequests": { "pageInfo": { "endCursor": "cursor", "hasNextPage": False }, "edges": [{ "node": pull_request, }] } } } client = MockClient() manager = ProjectManager(configuration=config, client=client) assert len(manager.matching_issues) == 1 assert len(manager.matching_pull_requests) == 1 assert manager.project.name == 'test' assert manager.project.get_current_location("56567=") == ("In progress", "56565=") assert manager.project.get_current_location("Random text") == (None, None) assert manager.project.is_in_column("In progress", "56567=") is True assert manager.project.is_in_column("In progress", "Random text") is False # assert there're 3 items 'In Progress' column manager.project.sort_items_in_columns(client, config) items = [ card.item.title for card in manager.project.columns['In progress'].cards ] assert items == ['issue 3', 'issue 2', "pull_request 1"] # remove a single issue from the column manager.project.columns['In progress'].remove_card("56565=") items = [ card.item.title for card in manager.project.columns['In progress'].cards ] assert len(items) == 2 assert manager.project.is_in_column("In progress", "1234=") is True assert manager.project.is_in_column("In progress", "76566=") is True assert manager.project.is_in_column("In progress", "56565=") is False # remove a single pull request from the column manager.project.columns['In progress'].remove_card("76565=") items = [ card.item.title for card in manager.project.columns['In progress'].cards ] assert len(items) == 1 assert manager.project.is_in_column("In progress", "1234=") is True assert manager.project.is_in_column("In progress", "76566=") is False manager.manage() assert manager.project.is_in_column("In progress", "56565=") is False