PageRenderTime 45ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/pybitbucket/pullrequest.py

https://bitbucket.org/Tufts/python-bitbucket
Python | 381 lines | 290 code | 42 blank | 49 comment | 16 complexity | f636259cca87c371911341ac10a7b5e3 MD5 | raw file
  1. # -*- coding: utf-8 -*-
  2. from __future__ import unicode_literals
  3. """
  4. Defines the PullRequest resource and registers the type with the Client.
  5. Classes:
  6. - PullRequestState: enumerates the possible states of a pull request
  7. - PullRequestPayload: encapsulates payload for creating
  8. and modifying pull requests
  9. - PullRequest: represents a pull request for code review
  10. """
  11. from functools import partial
  12. from uritemplate import expand
  13. from voluptuous import Schema, Required, Optional
  14. from pybitbucket.bitbucket import (
  15. Bitbucket, BitbucketBase, Client, PayloadBuilder, Enum)
  16. class PullRequestState(Enum):
  17. OPEN = 'OPEN'
  18. MERGED = 'MERGED'
  19. DECLINED = 'DECLINED'
  20. class PullRequestPayload(PayloadBuilder):
  21. """
  22. A builder object to help create payloads
  23. for creating and updating pull requests.
  24. """
  25. schema = Schema({
  26. Required('title'): str,
  27. Optional('description'): str,
  28. Optional('close_source_branch'): bool,
  29. Optional('reviewers'): [{Required('username'): str}],
  30. Required('destination'): {
  31. Required('branch'): {
  32. Required('name'): str
  33. },
  34. Optional('commit'): {
  35. Required('hash'): str
  36. }
  37. },
  38. Required('source'): {
  39. Required('branch'): {
  40. Required('name'): str
  41. },
  42. Required('repository'): {
  43. Required('full_name'): str
  44. },
  45. Optional('commit'): {
  46. Required('hash'): str
  47. }
  48. }
  49. })
  50. def __init__(
  51. self,
  52. payload=None,
  53. destination_repository_owner=None,
  54. destination_repository_name=None):
  55. super(self.__class__, self).__init__(payload=payload)
  56. self._destination_repository_owner = destination_repository_owner
  57. self._destination_repository_name = destination_repository_name
  58. @property
  59. def destination_repository_owner(self):
  60. return self._destination_repository_owner
  61. @property
  62. def destination_repository_name(self):
  63. return self._destination_repository_name
  64. def add_title(self, title):
  65. new = self._payload.copy()
  66. new['title'] = title
  67. return PullRequestPayload(
  68. payload=new,
  69. destination_repository_owner=self._destination_repository_owner,
  70. destination_repository_name=self._destination_repository_name)
  71. def add_description(self, description):
  72. new = self._payload.copy()
  73. new['description'] = description
  74. return PullRequestPayload(
  75. payload=new,
  76. destination_repository_owner=self._destination_repository_owner,
  77. destination_repository_name=self._destination_repository_name)
  78. def add_close_source_branch(self, close):
  79. new = self._payload.copy()
  80. new['close_source_branch'] = close
  81. return PullRequestPayload(
  82. payload=new,
  83. destination_repository_owner=self._destination_repository_owner,
  84. destination_repository_name=self._destination_repository_name)
  85. def add_reviewer_by_username(self, username):
  86. new = self._payload.copy()
  87. reviewers = self._payload.get('reviewers', [])
  88. if {'username': username} not in reviewers:
  89. reviewers.append({'username': username})
  90. new['reviewers'] = reviewers
  91. return PullRequestPayload(
  92. payload=new,
  93. destination_repository_owner=self._destination_repository_owner,
  94. destination_repository_name=self._destination_repository_name)
  95. def add_reviewer(self, user):
  96. return self.add_reviewer_by_username(user.username)
  97. def add_reviewers_from_usernames(self, usernames):
  98. new = self._payload.copy()
  99. reviewers = self._payload.get('reviewers', [])
  100. for username in usernames:
  101. if {'username': username} not in reviewers:
  102. reviewers.append({'username': username})
  103. new['reviewers'] = reviewers
  104. return PullRequestPayload(
  105. payload=new,
  106. destination_repository_owner=self._destination_repository_owner,
  107. destination_repository_name=self._destination_repository_name)
  108. def add_destination_repository_owner(self, owner):
  109. return PullRequestPayload(
  110. payload=self._payload.copy(),
  111. destination_repository_owner=owner,
  112. destination_repository_name=self._destination_repository_name)
  113. def add_destination_repository_name(self, name):
  114. return PullRequestPayload(
  115. payload=self._payload.copy(),
  116. destination_repository_owner=self._destination_repository_owner,
  117. destination_repository_name=name)
  118. def add_destination_repository_full_name(self, full_name):
  119. owner, name = full_name.split('/', 1)
  120. return PullRequestPayload(
  121. payload=self._payload.copy(),
  122. destination_repository_owner=owner,
  123. destination_repository_name=name)
  124. def add_destination_repository(self, repository):
  125. owner, name = repository.full_name.split('/', 1)
  126. return PullRequestPayload(
  127. payload=self._payload.copy(),
  128. destination_repository_owner=owner,
  129. destination_repository_name=name)
  130. def add_destination_branch_name(self, name):
  131. new = self._payload.copy()
  132. destination = self._payload.get('destination', {})
  133. destination['branch'] = destination.get('branch', {})
  134. destination['branch']['name'] = name
  135. new['destination'] = destination
  136. return PullRequestPayload(
  137. payload=new,
  138. destination_repository_owner=self._destination_repository_owner,
  139. destination_repository_name=self._destination_repository_name)
  140. def add_destination_branch(self, branch):
  141. owner, name = branch.repository.full_name.split('/', 1)
  142. return PullRequestPayload(
  143. payload=self._payload.copy(),
  144. destination_repository_owner=owner,
  145. destination_repository_name=name) \
  146. .add_destination_branch_name(branch.name)
  147. def add_destination_commit_by_hash(self, hash):
  148. new = self._payload.copy()
  149. destination = self._payload.get('destination', {})
  150. destination['commit'] = destination.get('commit', {})
  151. destination['commit']['hash'] = hash
  152. new['destination'] = destination
  153. return PullRequestPayload(
  154. payload=new,
  155. destination_repository_owner=self._destination_repository_owner,
  156. destination_repository_name=self._destination_repository_name)
  157. def add_destination_commit(self, commit):
  158. owner, name = commit.repository.full_name.split('/', 1)
  159. return PullRequestPayload(
  160. payload=self._payload.copy(),
  161. destination_repository_owner=owner,
  162. destination_repository_name=name) \
  163. .add_destination_commit_by_hash(commit.hash)
  164. def add_source_branch_name(self, name):
  165. new = self._payload.copy()
  166. source = self._payload.get('source', {})
  167. source['branch'] = source.get('branch', {})
  168. source['branch']['name'] = name
  169. new['source'] = source
  170. return PullRequestPayload(
  171. payload=new,
  172. destination_repository_owner=self._destination_repository_owner,
  173. destination_repository_name=self._destination_repository_name)
  174. def add_source_repository_full_name(self, full_name):
  175. new = self._payload.copy()
  176. source = self._payload.get('source', {})
  177. source['repository'] = source.get('repository', {})
  178. source['repository']['full_name'] = full_name
  179. new['source'] = source
  180. return PullRequestPayload(
  181. payload=new,
  182. destination_repository_owner=self._destination_repository_owner,
  183. destination_repository_name=self._destination_repository_name)
  184. def add_source_branch(self, branch):
  185. return self.add_source_branch_name(branch.name) \
  186. .add_source_repository_full_name(branch.repository.full_name)
  187. def add_source_commit_by_hash(self, hash):
  188. new = self._payload.copy()
  189. source = self._payload.get('source', {})
  190. source['commit'] = source.get('commit', {})
  191. source['commit']['hash'] = hash
  192. new['source'] = source
  193. return PullRequestPayload(
  194. payload=new,
  195. destination_repository_owner=self._destination_repository_owner,
  196. destination_repository_name=self._destination_repository_name)
  197. def add_source_commit(self, commit):
  198. return self.add_source_commit_by_hash(commit.hash) \
  199. .add_source_repository_full_name(commit.repository.full_name)
  200. class PullRequest(BitbucketBase):
  201. id_attribute = 'id'
  202. resource_type = 'pullrequests'
  203. templates = {
  204. 'create': (
  205. '{+bitbucket_url}' +
  206. '/2.0/repositories' +
  207. '{/owner,repository_name}' +
  208. '/pullrequests')
  209. }
  210. @staticmethod
  211. def is_type(data):
  212. return (PullRequest.has_v2_self_url(data))
  213. def attr_from_subchild(self, target_attribute, child, child_object):
  214. if self.data.get(child, {}).get(child_object, {}):
  215. setattr(
  216. self,
  217. target_attribute,
  218. self.client.convert_to_object(
  219. self.data[child][child_object]))
  220. def __init__(self, data, client=Client()):
  221. super(PullRequest, self).__init__(data, client=client)
  222. self.attr_from_subchild(
  223. 'source_commit', 'source', 'commit')
  224. self.attr_from_subchild(
  225. 'source_repository', 'source', 'repository')
  226. self.attr_from_subchild(
  227. 'destination_commit', 'destination', 'commit')
  228. self.attr_from_subchild(
  229. 'destination_repository', 'destination', 'repository')
  230. # Special treatment for approve, decline, merge, and diff
  231. if data.get('links', {}).get('approve', {}).get('href', {}):
  232. url = data['links']['approve']['href']
  233. # Approve is a POST on the approve link
  234. setattr(self, 'approve', partial(
  235. self.post_approval, template=url))
  236. # Unapprove is a DELETE on the approve link
  237. setattr(self, 'unapprove', partial(
  238. self.delete_approval, template=url))
  239. if data.get('links', {}).get('decline', {}).get('href', {}):
  240. url = data['links']['decline']['href']
  241. # Decline is a POST
  242. setattr(self, 'decline', partial(
  243. self.post, client=client, url=url, json=None))
  244. if data.get('links', {}).get('merge', {}).get('href', {}):
  245. url = data['links']['merge']['href']
  246. # Merge is a POST
  247. setattr(self, 'merge', partial(
  248. self.post, client=client, url=url, json=None))
  249. if data.get('links', {}).get('diff', {}).get('href', {}):
  250. url = data['links']['diff']['href']
  251. # Diff returns plain text
  252. setattr(self, 'diff', partial(
  253. self.content, url=url))
  254. def content(self, url):
  255. response = self.client.session.get(url)
  256. Client.expect_ok(response)
  257. return response.content
  258. @classmethod
  259. def create(
  260. cls,
  261. payload,
  262. repository_name=None,
  263. owner=None,
  264. client=None):
  265. """Create a new Pull Request.
  266. :param payload: the options for creating the new Pull Request
  267. :type payload: PullRequestPayload
  268. :param repository_name: name of the destination repository,
  269. also known as repo_slug. Optional, if provided in the payload.
  270. :type repository_name: str
  271. :param owner: the owner of the destination repository.
  272. Optional, if provided in the payload.
  273. :type owner: str
  274. :param client: the configured connection to Bitbucket.
  275. If not provided, assumes an Anonymous connection.
  276. :type client: bitbucket.Client
  277. :returns: the new repository object.
  278. :rtype: PullRequest
  279. :raises: ValueError
  280. """
  281. client = client or Client()
  282. owner = (
  283. owner or
  284. payload.destination_repository_owner)
  285. repository_name = (
  286. repository_name or
  287. payload.destination_repository_name)
  288. if not (owner and repository_name):
  289. raise ValueError('owner and repository_name are required')
  290. json = payload.validate().build()
  291. api_url = expand(
  292. cls.templates['create'], {
  293. 'bitbucket_url': client.get_bitbucket_url(),
  294. 'owner': owner,
  295. 'repository_name': repository_name,
  296. })
  297. return cls.post(api_url, json=json, client=client)
  298. @staticmethod
  299. def find_pullrequest_by_id_in_repository(
  300. pullrequest_id,
  301. repository_name,
  302. owner=None,
  303. client=None):
  304. """
  305. A convenience method for finding a specific pull request.
  306. In contrast to the pure hypermedia driven method on the Bitbucket
  307. class, this method returns a PullRequest object, instead of the
  308. generator.
  309. """
  310. client = client or Client()
  311. owner = owner or client.get_username()
  312. return next(
  313. Bitbucket(client=client).repositoryPullRequestByPullRequestId(
  314. owner=owner,
  315. repository_name=repository_name,
  316. pullrequest_id=pullrequest_id))
  317. @staticmethod
  318. def find_pullrequests_for_repository_by_state(
  319. repository_name,
  320. owner=None,
  321. state=None,
  322. client=None):
  323. """
  324. A convenience method for finding pull requests for a repository.
  325. The method is a generator PullRequest objects.
  326. If no owner is provided, this method assumes client can provide one.
  327. If no state is provided, the server will assume open pull requests.
  328. """
  329. client = client or Client()
  330. owner = owner or client.get_username()
  331. if (state is not None):
  332. PullRequestState(state)
  333. return Bitbucket(client=client).repositoryPullRequestsInState(
  334. owner=owner,
  335. repository_name=repository_name,
  336. state=state)
  337. Client.bitbucket_types.add(PullRequest)