tp/tools/libgithub/project.py

546 lines
18 KiB
Python

import yaml
from .issue import *
from .field import *
from typing import Optional
class Project:
def __eq__(self, other):
if isinstance(other, Label):
return self.title == other.name and self.id == other.id
return False
def __hash__(self):
return hash((self.title, self.id))
def __init__(self, id=None, title=None, number=None, items=None, items_to_attach=None, status_field=None, data=None):
if data is not None:
self.id = None
self.title = data.get("project").get('title', 'MISSING_TITLE')
self.number = None
self.items = None
self.items_to_attach = None
self.status_field = None
else:
self.id = id
self.title = title
self.number = number
self.items = items
self.items_to_attach = items_to_attach
self.status_field = status_field
@staticmethod
def get_all_from_yaml(data) -> list['Project']:
ret_projects = []
issues_dict = {issue['file_path']: issue['id'] for issue in StateFile.data["issues"]}
for d in data:
items = []
for tu, _, file_path in get_translation_units(d):
if file_path in issues_dict:
LOG.debug(f'Issue {tu} found in state file.')
LOG.debug(f'Adding ID {issues_dict[file_path]} to items.')
items.append({
"issue_id": issues_dict[file_path]
})
else:
LOG.error(f'Issue {tu} not found in state file. Please run ./tp github-sync-issues first.')
sys.exit(1)
project = Project(
id=None,
title=d['project']['title'],
number=None,
items=[],
items_to_attach=items
)
ret_projects.append(project)
return ret_projects
@staticmethod
def get_all_from_github() -> list['Project']:
LOG.debug(f'Getting projects on {RepoInfo.owner.name}/{RepoInfo.name}')
query = '''
query ($owner: String!, $repo: String!, $cursor: String) {
repository(owner: $owner, name: $repo) {
projectsV2(first: 20) {
nodes {
id
title
number
items(first: 100, after: $cursor) {
pageInfo {
endCursor
hasNextPage
}
edges {
cursor
node {
id
content {
... on Issue {
id
title
}
}
}
}
}
}
}
}
}
'''
variables = {
"owner": RepoInfo.owner.name,
"repo": RepoInfo.name,
"cursor": None,
}
ret_projects = []
while True:
data = GraphQLClient.get_instance().make_request(query, variables)
if data:
projects = data['data']['repository']['projectsV2']['nodes']
LOG.debug(f'Projects retrieved: {projects}')
for project in projects:
items = []
for edge in project['items']['edges']:
LOG.debug(f'Item: {edge}')
item_id = edge['node']['id']
issue_id = edge['node']['content']['id']
item = ProjectItem(
id=item_id,
issue_id = issue_id,
)
items.append(item)
ret_project = Project(
id=project['id'],
title=project['title'],
number=project['number'],
items=items
)
ret_projects.append(ret_project)
# Check if there are more items to fetch
if len(projects) == 0 or not data['data']['repository']['projectsV2']['nodes'][0]['items']['pageInfo']['hasNextPage']:
break
# Update the cursor to the last item's cursor for the next fetch
variables['cursor'] = data['data']['repository']['projectsV2']['nodes'][0]['items']['pageInfo']['endCursor']
else:
LOG.warning("No projects found!")
break
return ret_projects
@staticmethod
def get_project_by_name(project_name: str) -> Optional['Project']:
all_projects = Project.get_all_from_github()
if all_projects:
for project in all_projects:
if project.title == project_name:
return project
else:
LOG.warning(f'No projects found in {RepoInfo.owner.name}/{RepoInfo.name}')
return None
@staticmethod
def delete_all():
LOG.debug(f'Deleting all projects in {RepoInfo.owner.name}/{RepoInfo.name}')
project_state = StateFile.data["projects"]
if project_state is not None and len(project_state) > 0:
for project in project_state.copy():
Project(
id=project["id"],
title=project["title"]
).delete()
else:
LOG.warning(f'No projects found in state file. Nothing to delete.')
return
def create(self) -> None:
owner_id = RepoInfo.owner.id
repo_id = RepoInfo.id
if not owner_id or not repo_id:
return
LOG.debug(f'Creating Github project {self.title}')
mutation = '''
mutation ($ownerId: ID!, $repoId: ID!, $projectName: String!) {
createProjectV2(input: { ownerId: $ownerId, repositoryId: $repoId, title: $projectName }) {
projectV2 {
id
number
title
}
}
}
'''
variables = {
"ownerId": owner_id,
"repoId": repo_id,
"projectName": self.title
}
data = GraphQLClient.get_instance().make_request(mutation, variables)
if data:
self.id = data['data']['createProjectV2']['projectV2']['id']
self.number = data['data']['createProjectV2']['projectV2']['number']
self.status_field = Field.get_status_field(self.id)
self.write_state_to_file()
self.set_public()
LOG.info(f"Successfully created project '{self.title}' with ID {self.id} and number {self.number}")
else:
LOG.error(f'Failed to create project {self.title}')
sys.exit(1)
def check_and_create(self) -> None:
projects = StateFile.data.get('projects')
if projects is None:
project_dict = {}
else:
project_dict = {project['title']: project for project in projects}
if self.title in project_dict:
LOG.info(f'Project {self.title} already exists')
self.id = project_dict[self.title]["id"]
self.number = project_dict[self.title]["number"]
self.items = project_dict[self.title]["items"]
self.status_field = project_dict[self.title]["status_field"]
missing_issue_ids = [item['issue_id'] for item in self.items_to_attach if item['issue_id'] not in {item['issue_id'] for item in self.items}]
LOG.info(f'Attaching missing issues to project {self.title}')
if len(missing_issue_ids) > 0:
for id in missing_issue_ids:
self.attach_issue(id)
else:
LOG.info(f'All issues already attached to project {self.title}')
else:
LOG.info(f'Creating missing project {self.title}')
self.create()
for item in self.items_to_attach:
self.attach_issue(item["issue_id"])
def attach_issue(self, issue_id) -> None:
LOG.debug(f'Attaching issue {issue_id} to project {self.title}')
mutation = """
mutation AddProjectV2ItemById($input: AddProjectV2ItemByIdInput!) {
addProjectV2ItemById(input: $input) {
clientMutationId
item {
id
}
}
}
"""
variables = {
"input": {
"projectId": self.id,
"contentId": issue_id,
"clientMutationId": "UNIQUE_ID"
}
}
data = GraphQLClient.get_instance().make_request(mutation, variables)
if data:
LOG.debug(f'Issue {issue_id} attached to project {self.title}')
item_id = data['data']['addProjectV2ItemById']['item']['id']
self.items.append({
"issue_id": issue_id,
"item_id": item_id
})
self.write_state_to_file()
else:
LOG.error(f'Failed to attach issue {issue_id} to project {self.title}')
sys.exit(1)
def get_item_id_from_issue(self, issue: Issue) -> str:
LOG.debug(f'Getting item ID for issue {issue.title} in project {self.title}')
query = """
query ($projectId: ID!, $issueId: ID!) {
projectV2Item(projectId: $projectId, contentId: $issueId) {
id
}
}
"""
variables = {
"projectId": self.id,
"issueId": issue.id
}
data = GraphQLClient.get_instance().make_request(query, variables)
if data:
LOG.debug(f'Got item ID for issue {issue.title} in project {self.title}')
return data['data']['projectV2Item']['id']
else:
LOG.error(f'Failed to get item ID for issue {issue.title} in project {self.title}')
sys.exit(1)
def delete(self) -> None:
query = '''
mutation ($projectId: ID!) {
deleteProjectV2(input: {
projectId: $projectId
}) {
clientMutationId
}
}
'''
variables = {
"projectId": self.id
}
data = GraphQLClient.get_instance().make_request(query, variables)
if data:
self.write_state_to_file(delete=True)
LOG.info(f'Successfully deleted project {self.title}.')
else:
LOG.error(f'Failed to delete project {self.title}')
sys.exit(1)
def set_public(self) -> None:
query = '''
mutation UpdateProjectVisibility($input: UpdateProjectV2Input!) {
updateProjectV2(input: $input) {
projectV2 {
id
title
public
}
}
}
'''
variables = {
"input": {
"projectId": self.id,
"public": True
}
}
data = GraphQLClient.get_instance().make_request(query, variables)
if data:
LOG.info(f'Successfully set project {self.title} to public.')
else:
LOG.error(f'Failed to set project {self.title} to public.')
sys.exit(1)
def set_status_for_item(self, item_id: str, status: str) -> None:
query = '''
mutation updateProjectV2ItemFieldValue($input: UpdateProjectV2ItemFieldValueInput!) {
updateProjectV2ItemFieldValue(input: $input) {
projectV2Item {
databaseId
}
}
}
'''
options = self.status_field.options
option = next((option for option in options if option.name.lower() == status.lower()), None)
variables = {
"input": {
"projectId": self.id,
"itemId": item_id,
"fieldId": self.status_field.id,
"value": {
"singleSelectOptionId": option.id
}
}
}
data = GraphQLClient.get_instance().make_request(query, variables)
if data:
LOG.info(f'Successfully set status for item {item_id} to {status}')
else:
LOG.error(f'Failed to set status for item {item_id} to {status}')
sys.exit(1)
def set_id(self) -> None:
LOG.debug(f'Getting ID for project {self.title}')
query = '''
query ($owner: String!, $name: String!, $projectName: String!) {
repository(owner: $owner, name: $name) {
projectsV2(query: $projectName, first: 1) {
nodes {
id
title
}
}
}
}
'''
variables = {
"owner": RepoInfo.owner.name,
"name": RepoInfo.name,
"projectName": self.title
}
data = GraphQLClient.get_instance().make_request(query, variables)
if data:
projects = data['data']['repository']['projectsV2']['nodes']
for project in projects:
if project['title'] == self.title:
project_id = project['id']
LOG.info(f'Project ID for {self.title}: {project_id}')
self.id = project_id
else:
LOG.critical(f'No project found with title {self.title}')
sys.exit(1)
else:
LOG.critical(f'Query failed.')
sys.exit(1)
def set_items(self) -> None:
query = '''
query ($owner: String!, $repo: String!, $cursor: String) {
repository(owner: $owner, name: $repo) {
projectsV2(first: 20) {
nodes {
items(first: 100, after: $cursor) {
pageInfo {
endCursor
hasNextPage
}
edges {
cursor
node {
id
content {
... on Issue {
id
title
}
}
}
}
}
}
}
}
}
'''
variables = {
"owner": RepoInfo.owner.name,
"repo": RepoInfo.name,
"cursor": None,
}
self.items = []
while True:
data = GraphQLClient.get_instance().make_request(query, variables)
if data:
projects = data['data']['repository']['projectsV2']['nodes']
for project in projects:
for edge in project['items']['edges']:
item_id = edge['node']['id']
issue_id = edge['node']['content']['id']
issue_title = edge['node']['content']['title']
item = {
'id': item_id,
'issue_id': issue_id,
'issue_title': issue_title
}
self.items.append(item)
# Check if there are more items to fetch
if not data['data']['repository']['projectsV2']['nodes'][0]['items']['pageInfo']['hasNextPage']:
break
# Update the cursor to the last item's cursor for the next fetch
variables['cursor'] = data['data']['repository']['projectsV2']['nodes'][0]['items']['pageInfo']['endCursor']
else:
break
def write_state_to_file(self, delete: bool = False):
state = {
"title": self.title,
"id": self.id,
"number": self.number,
"items": self.items,
"status_field": self.status_field
}
curr_state_projects = StateFile.data.get("projects", None)
if curr_state_projects is not None:
for i, project in enumerate(curr_state_projects):
if project['id'] == self.id:
if delete:
del StateFile.data['projects'][i]
break
else:
StateFile.data['projects'][i] = state
break
else:
StateFile.data['projects'].append((state))
else:
StateFile.data['projects'] = [state]
with open("tools/pjstate.yml", 'w') as f:
yaml.safe_dump(StateFile.data, f)
# Custom representer for Option
def option_representer(dumper, data):
return dumper.represent_mapping('!Option', {
'id': data.id,
'name': data.name
})
# Custom constructor for Option
def option_constructor(loader, node):
values = loader.construct_mapping(node)
return Option(values['id'], values['name'])
# Custom representer for Field
def field_representer(dumper, data):
return dumper.represent_mapping('!Field', {
'id': data.id,
'name': data.name,
'options': data.options
})
# Custom constructor for Field
def field_constructor(loader, node):
values = loader.construct_mapping(node)
return Field(values['id'], values['name'], values['options'])
# Register the custom representers and constructors with SafeDumper
yaml.add_representer(Option, option_representer, Dumper=yaml.SafeDumper)
yaml.add_constructor('!Option', option_constructor, Loader=yaml.SafeLoader)
yaml.add_representer(Field, field_representer, Dumper=yaml.SafeDumper)
yaml.add_constructor('!Field', field_constructor, Loader=yaml.SafeLoader)