Commit 8eb39867 authored by Sebastian Latacz's avatar Sebastian Latacz

Add GitHub and GitLab Milestone Implementation

Relevant issue: #62
parent ce295694
Pipeline #22750829 passed with stages
in 49 seconds
"""
This contains the Milestone implementation for GitHub
"""
from datetime import datetime
from IGitt.GitHub import GitHubMixin
from IGitt.Interfaces.Milestone import Milestone
from IGitt.Interfaces import MilestoneStates
from IGitt.GitHub import GitHubToken
from IGitt.Interfaces import post
from IGitt.Interfaces import patch
from IGitt.Interfaces import delete
from IGitt.Interfaces import get
from IGitt.GitHub.GitHubIssue import GitHubIssue
from IGitt.GitHub.GitHubMergeRequest import GitHubMergeRequest
from IGitt.GitHub.GitHubRepository import GitHubRepository
class GitHubMilestone(GitHubMixin, Milestone):
"""
This class represents a milestone on GitHub.
"""
def __init__(self, token: GitHubToken, repository: str, number: int):
"""
Creates a new GitHubMilestone object with the given credentials.
:param token: A Token object to be used for authentication.
:param repository: The full name of the repository
eg. 'sils/repository'.
:param number: The milestones number.
:raises RuntimeError: If something goes wrong (network, auth, ...)
"""
self._token = token
self._repository = repository
self._number = number
self._url = '/repos/{repository}/milestones/{milestone_number}'\
.format(repository=repository, milestone_number=number)
self._issues_url = GitHubMixin.absolute_url(
'/repos/{repository}/issues'.format(repository=self._repository))
@staticmethod
def create(token: GitHubToken,
repository: str,
title: str,
state: MilestoneStates = MilestoneStates.OPEN,
description: str = None,
due_on: datetime = None):
"""
Create a new milestone with given title
:return: GitHubMilestone object of the newly created milestone.
"""
url = '/repos/{repository}/milestones'.format(repository=repository)
if due_on != None:
due_on = datetime.strftime(due_on, '%Y-%m-%dT%H:%M:%SZ')
milestone = post(
token, GitHubMilestone.absolute_url(url), {
'title': title,
'state': str(state),
'description': description,
'due_on': due_on
})
return GitHubMilestone.from_data(milestone, token, repository,
milestone['number'])
@property
def number(self) -> int:
"""
Returns the milestone "number" or id.
"""
return self._number
@property
def title(self) -> str:
"""
Retrieves the title of the milestone.
"""
return self.data['title']
@title.setter
def title(self, new_title):
"""
Sets the title of the milestone.
:param new_title: The new title.
"""
self.data = patch(self._token, self.url, {'title': new_title})
@property
def description(self) -> str:
"""
Retrieves the main description of the milestone.
"""
return self.data['description']
@description.setter
def description(self, new_description):
"""
Sets the description of the milestone
:param new_description: The new description .
"""
self.data = patch(self._token, self.url,
{'description': new_description})
@property
def state(self) -> MilestoneStates:
"""
Get's the state of the milestone.
:return: Either MilestoneStates.OPEN or MilestoneStates.CLOSED.
"""
return MilestoneStates[self.data['state'].upper()]
def close(self):
"""
Closes the milestone.
:raises RuntimeError: If something goes wrong (network, auth...).
"""
self.data = patch(self._token, self.url, {'state': 'closed'})
def reopen(self):
"""
Reopens the milestone.
:raises RuntimeError: If something goes wrong (network, auth...).
"""
self.data = patch(self._token, self.url, {'state': 'open'})
@property
def created(self) -> datetime:
"""
Retrieves a timestamp on when the milestone was created.
"""
return datetime.strptime(self.data['created_at'], '%Y-%m-%dT%H:%M:%SZ')
@property
def updated(self) -> datetime:
"""
Retrieves a timestamp on when the milestone was updated the last time.
"""
return datetime.strptime(self.data['updated_at'], '%Y-%m-%dT%H:%M:%SZ')
@property
def due_date(self) -> datetime:
"""
Retrieves a timestamp on when the milestone is due.
"""
return datetime.strptime(
self.data['due_on'],
'%Y-%m-%dT%H:%M:%SZ') if self.data['due_on'] else None
@due_date.setter
def due_date(self, new_date: datetime):
"""
Sets the due date of the milestone.
It is not possible to set the time. GitHub will always set the time on
the due date to 07:00:00
:param new_date: The new due date.
"""
self.data = patch(
self._token, self.url, {
'due_on':
datetime.strftime(new_date, '%Y-%m-%dT%H:%M:%SZ')
if new_date else None
})
def delete(self):
"""
Deletes the milestone.
This is not possible with GitLab api v4.
:raises RuntimeError: If something goes wrong (network, auth...).
"""
delete(self._token, self.url)
@property
def issues(self) -> set:
"""
Retrieves a set of issue objects that are assigned to this milestone.
"""
return {
GitHubIssue.from_data(res, self._token, self._repository,
res['number'])
for res in get(self._token, self._issues_url,
{'milestone': self._number})
if 'pull_request' not in res
}
@property
def merge_requests(self) -> set:
"""
Retrieves a set of merge_request
objects that are assigned to this milestone.
"""
return {
GitHubMergeRequest.from_data(res, self._token, self._repository,
res['number'])
for res in get(self._token, self._issues_url,
{'milestone': self._number})
if 'pull_request' in res
}
@property
def project(self) -> GitHubRepository:
"""
Returns the repository this milestone is linked with.
"""
return GitHubRepository(self._token, self._repository)
@property
def start_date(self) -> datetime:
"""
Retrieves a timestamp on when the milestone was started.
The start_date does not exist in GitHub.
"""
return None
"""
This contains the Milestone implementation for GitLab
"""
import re
from datetime import datetime
from urllib.parse import quote_plus
from IGitt.GitLab import GitLabMixin
from IGitt.GitLab import get
from IGitt.Interfaces import put
from IGitt.Interfaces import post
from IGitt.Interfaces import delete
from IGitt.GitLab import GitLabOAuthToken, GitLabPrivateToken
from IGitt.Interfaces.Milestone import Milestone
from IGitt.GitLab.GitLabIssue import GitLabIssue
from IGitt.GitLab.GitLabMergeRequest import GitLabMergeRequest
from IGitt.Interfaces import MilestoneStates
from IGitt.GitLab.GitLabRepository import GitLabRepository
state_translation_dict = {
'active': MilestoneStates.OPEN,
'closed': MilestoneStates.CLOSED,
}
class GitLabProjectMilestone(GitLabMixin, Milestone):
"""
This class represents a project level milestone on GitLab.
This class does not support group level milestones.
"""
def __init__(self, token: (GitLabOAuthToken, GitLabPrivateToken), project,
number: int):
"""
Creates a new GitLabProjectMilestone with the given credentials.
:param token: A Token object to be used for authentication.
:param project: The full name of the project. Including the owner
e.g. ``owner/project``.
:param number: The unique milestone identification number (id).
Not The clear text number given on the Web UI
See also https://docs.gitlab.com/ee/api/README.html#id-vs-iid
:raises RuntimeError: If something goes wrong (network, auth, ...)
"""
self._token = token
self._project = project
self._id = number
self._url = '/projects/{project}/milestones/{milestone_id}'.format(
project=quote_plus(project), milestone_id=number)
@staticmethod
def create(token: (GitLabOAuthToken, GitLabPrivateToken),
project,
title: str,
description: str = None,
due_date=None,
start_date=None):
"""
Create a new milestone with given title and body.
>>> from os import environ
>>> milestone = GitLabProjectMilestone.create(
... GitLabOAuthToken(environ['GITLAB_TEST_TOKEN']),
... 'gitmate-test-user/test',
... 'test milestone title',
... 'sample description'
... )
>>> milestone.state
'active'
Delete the milestone to avoid filling the test repo with milestones.
>>> milestone.close()
:return: GitLabProjectMilestone object of the newly created milestone.
"""
url = '/projects/{project}/milestones'.format(
project=quote_plus(project))
if due_date is not None:
due_date = datetime.strftime(due_date, '%Y-%m-%d')
if start_date is not None:
start_date = datetime.strftime(start_date, '%Y-%m-%d')
milestone = post(
token, GitLabProjectMilestone.absolute_url(url), {
'title': title,
'description': description,
'due_date': due_date,
'start_date': start_date
})
return GitLabProjectMilestone.from_data(milestone, token, project,
milestone['id'])
@property
def number(self) -> int:
"""
Returns the milestone "number" or id.
"""
return self._id
@property
def title(self) -> str:
"""
Retrieves the title of the milestone.
"""
return self.data['title']
@title.setter
def title(self, new_title):
"""
Sets the title of the milestone.
:param new_title: The new title.
"""
self.data = put(self._token, self.url, {'title': new_title})
@property
def description(self) -> str:
"""
Retrieves the main description of the milestone.
"""
return self.data['description']
@description.setter
def description(self, new_description):
"""
Sets the description of the milestone
:param new_description: The new description .
"""
self.data = put(self._token, self.url,
{'description': new_description})
@property
def state(self) -> MilestoneStates:
"""
Get's the state of the milestone.
:return: Either MilestoneStates.OPEN or MilestoneStates.CLOSED.
"""
return state_translation_dict[self.data['state']]
def close(self):
"""
Closes the milestone.
:raises RuntimeError: If something goes wrong (network, auth...).
"""
self.data = put(self._token, self.url, {'state_event': 'close'})
def reopen(self):
"""
Reopens the milestone.
:raises RuntimeError: If something goes wrong (network, auth...).
"""
self.data = put(self._token, self.url, {'state_event': 'activate'})
@property
def created(self) -> datetime:
"""
Retrieves a timestamp on when the milestone was created.
"""
return datetime.strptime(self.data['created_at'],
'%Y-%m-%dT%H:%M:%S.%fZ')
@property
def updated(self) -> datetime:
"""
Retrieves a timestamp on when the milestone was updated the last time.
"""
return datetime.strptime(self.data['updated_at'],
'%Y-%m-%dT%H:%M:%S.%fZ')
@property
def start_date(self) -> datetime:
"""
Retrieves a timestamp on when the milestone was started.
"""
return datetime.strptime(
self.data['start_date'],
'%Y-%m-%d') if self.data['start_date'] else None
@start_date.setter
def start_date(self, new_date: datetime):
"""
Sets the start date of the milestone.
:param new_date: The new start date.
"""
# The title is only set because the GitLab APIV4 requires this.
# This issue has been reported:
# https://gitlab.com/gitlab-org/gitlab-ce/issues/46740
self.data = put(
self._token, self.url, {
'start_date':
datetime.strftime(new_date, '%Y-%m-%d') if new_date else None,
'title':
self.title
})
@property
def due_date(self) -> datetime:
"""
Retrieves a timestamp on when the milestone is due.
"""
return datetime.strptime(self.data['due_date'],
'%Y-%m-%d') if self.data['due_date'] else None
@due_date.setter
def due_date(self, new_date: datetime):
"""
Sets the due date of the milestone.
:param new_date: The new due date.
"""
self.data = put(
self._token, self.url, {
'due_date':
datetime.strftime(new_date, '%Y-%m-%d') if new_date else None
})
@staticmethod
def extract_repo_full_name(web_url):
"""
Extracts the repository name from the web_url of the issue
"""
return re.sub(r'https?://gitlab\.com/|/issues/\d', '', web_url)
@property
def issues(self) -> set:
"""
Retrieves a set of issue objects that are assigned to this milestone.
"""
return {
GitLabIssue.from_data(res, self._token,
self.extract_repo_full_name(res['web_url']),
res['iid'])
for res in get(self._token, self.url + '/issues')
}
@property
def merge_requests(self) -> set:
"""
Retrieves a set of merge request objects that are assigned to this
milestone.
"""
return {
GitLabMergeRequest.from_data(res, self._token,
self.extract_repo_full_name(
res['web_url']), res['iid'])
for res in get(self._token, self.url + '/merge_requests')
}
@property
def project(self) -> GitLabRepository:
"""
Returns the repository this milestone is linked with.
"""
return GitLabRepository(self._token, self._project)
def delete(self):
"""
Deletes the milestone.
This is not possible with GitLab api v4.
:raises RuntimeError: If something goes wrong (network, auth...).
"""
delete(self._token, self.url)
"""
This module contains the milestone abstraction class which provides properties
and actions related to milestones.
"""
from datetime import datetime
from IGitt.Interfaces.Repository import Repository
from IGitt.Interfaces import IGittObject
from IGitt.Interfaces import MilestoneStates
class Milestone(IGittObject):
"""
Represents a milestone for GitHub or GitLab or any similar collection of
issues.
"""
@property
def number(self) -> int:
"""
Returns the milestone "number" or id.
"""
raise NotImplementedError
@property
def project(self) -> Repository:
"""
Returns the repository this milestone is linked with.
"""
raise NotImplementedError
@property
def title(self) -> str:
"""
Retrieves the title of the milestone.
"""
raise NotImplementedError
@title.setter
def title(self, new_title):
"""
Sets the title of the milestone.
:param new_title: The new title.
"""
raise NotImplementedError
@property
def description(self) -> str:
"""
Retrieves the main description of the milestone.
"""
raise NotImplementedError
@description.setter
def description(self, new_description):
"""
Sets the description of the milestone
:param new_description: The new description .
"""
raise NotImplementedError
@property
def state(self) -> MilestoneStates:
"""
Get's the state of the milestone.
:return: Either MilestoneStates.OPEN or MilestoneStates.CLOSED.
"""
raise NotImplementedError
def close(self):
"""
Closes the milestone.
:raises RuntimeError: If something goes wrong (network, auth...).
"""
raise NotImplementedError
def reopen(self):
"""
Reopens the milestone.
:raises RuntimeError: If something goes wrong (network, auth...).
"""
raise NotImplementedError
@property
def created(self) -> datetime:
"""
Retrieves a timestamp on when the milestone was created.
"""
raise NotImplementedError
@property
def updated(self) -> datetime:
"""
Retrieves a timestamp on when the milestone was updated the last time.
"""
raise NotImplementedError
@property