-
Notifications
You must be signed in to change notification settings - Fork 20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add API endpoint to get a Phabricator Revision from Lando #4
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,18 +1,104 @@ | ||
# This Source Code Form is subject to the terms of the Mozilla Public | ||
# License, v. 2.0. If a copy of the MPL was not distributed with this | ||
# file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||
""" | ||
Revision API | ||
See the OpenAPI Specification for this API in the spec/swagger.yml file. | ||
""" | ||
from connexion import problem | ||
from landoapi.phabricator_client import PhabricatorClient | ||
|
||
|
||
def search(): | ||
pass | ||
def get(api_key, revision_id): | ||
""" API endpoint at /revisions/{id} to get revision data. """ | ||
phab = PhabricatorClient(api_key) | ||
revision = phab.get_revision(id=revision_id) | ||
|
||
if not revision: | ||
# We could not find a matching revision. | ||
return problem( | ||
404, | ||
'Revision not found', | ||
'The requested revision does not exist', | ||
type='https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404' | ||
) | ||
|
||
def get(id): | ||
# We could not find a matching revision. | ||
return problem( | ||
404, | ||
'Revision not found', | ||
'The requested revision does not exist', | ||
type='https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404' | ||
) | ||
return _format_revision(phab, revision, include_parents=True), 200 | ||
|
||
|
||
def _format_revision( | ||
phab, revision, include_parents=False, last_author=None, last_repo=None | ||
): | ||
""" Formats a revision given by Phabricator to match Lando's spec. | ||
|
||
See the swagger.yml spec for the Revision definition. | ||
|
||
Args: | ||
phab: The PhabricatorClient to use to make additional requests. | ||
revision: The initial revision to format. | ||
include_parents: A flag to choose whether this method will recursively | ||
load parent revisions and format them as well. | ||
last_author: A hash of the author who created the revision. This is | ||
mainly used by this method itself when recursively loading parent | ||
revisions so as to prevent excess requests for what is often the | ||
same author on each parent revision. | ||
last_repo: A hash of the repo that this revision belongs to. This is | ||
mainly used by this method itself when recursively loading parent | ||
revisions so as to prevent excess requests for what is often the | ||
same repo on each parent revision. | ||
Returns: | ||
A hash of the formatted revision information. | ||
""" | ||
|
||
# Load the author if it isn't the same as the child revision's author. | ||
if last_author and revision['authorPHID'] == last_author['phid']: | ||
author = last_author | ||
else: | ||
raw_author = phab.get_user(revision['authorPHID']) | ||
author = { | ||
'phid': raw_author['phid'], | ||
'username': raw_author['userName'], | ||
'real_name': raw_author['realName'], | ||
'url': raw_author['uri'], | ||
'image_url': raw_author['image'], | ||
} | ||
|
||
# Load the repo if it isn't the same as the child revision's repo. | ||
if last_repo and revision['repositoryPHID'] == last_repo['phid']: | ||
repo = last_repo | ||
else: | ||
raw_repo = phab.get_repo(revision['repositoryPHID']) | ||
repo = { | ||
'phid': raw_repo['phid'], | ||
'short_name': raw_repo['name'], | ||
'full_name': raw_repo['fullName'], | ||
'url': raw_repo['uri'], | ||
} | ||
|
||
# This recursively loads the parent of a revision, and the parents of | ||
# that parent, and so on, ultimately creating a linked-list type structure | ||
# that connects the dependent revisions. | ||
parent_revisions = [] | ||
if include_parents: | ||
parent_phids = revision['auxiliary']['phabricator:depends-on'] | ||
for parent_phid in parent_phids: | ||
parent_revision_data = phab.get_revision(phid=parent_phid) | ||
if parent_revision_data: | ||
parent_revisions.append( | ||
_format_revision(phab, parent_revision_data, True) | ||
) | ||
|
||
return { | ||
'id': int(revision['id']), | ||
'phid': revision['phid'], | ||
'url': revision['uri'], | ||
'date_created': int(revision['dateCreated']), | ||
'date_modifed': int(revision['dateModified']), | ||
'status': int(revision['status']), | ||
'status_name': revision['statusName'], | ||
'summary': revision['summary'], | ||
'test_plan': revision['testPlan'], | ||
'author': author, | ||
'repo': repo, | ||
'parent_revisions': parent_revisions, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
# This Source Code Form is subject to the terms of the Mozilla Public | ||
# License, v. 2.0. If a copy of the MPL was not distributed with this | ||
# file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||
|
||
import os | ||
import requests | ||
|
||
|
||
class PhabricatorClient: | ||
""" A class to interface with Phabricator's Conduit API. | ||
|
||
All request methods in this class will throw a PhabricatorAPIException if | ||
Phabricator returns an error response. If there is an actual problem with | ||
the request to the server or decoding the JSON response, this class will | ||
bubble up the exception. These exceptions can be one of the request library | ||
exceptions or a JSONDecodeError. | ||
""" | ||
|
||
def __init__(self, api_key): | ||
self.api_url = os.getenv('PHABRICATOR_URL') + '/api' | ||
self.api_key = api_key | ||
|
||
def get_revision(self, id=None, phid=None): | ||
""" Gets a revision as defined by the Phabricator API. | ||
|
||
Args: | ||
id: The id of the revision if known. This can be in the form of | ||
an integer or an integer prefixed with 'D', e.g. 'D12345'. | ||
phid: The phid of the revision to be used if the id isn't provided. | ||
|
||
Returns: | ||
A hash of the revision data just as it is returned by Phabricator. | ||
Returns None, if the revision doesn't exist, or if the api key that | ||
was used to create the PhabricatorClient doesn't have permission to | ||
view the revision. | ||
""" | ||
result = None | ||
if id: | ||
id_num = str(id).strip().replace('D', '') | ||
result = self._GET('/differential.query', {'ids[]': [id_num]}) | ||
elif phid: | ||
result = self._GET('/differential.query', {'phids[]': [phid]}) | ||
return result[0] if result else None | ||
|
||
def get_current_user(self): | ||
""" Gets the information of the user making this request. | ||
|
||
Returns: | ||
A hash containing the information of the user that owns the api key | ||
that was used to initialize this PhabricatorClient. | ||
""" | ||
return self._GET('/user.whoami') | ||
|
||
def get_user(self, phid): | ||
""" Gets the information of the user based on their phid. | ||
|
||
Args: | ||
phid: The phid of the user to lookup. | ||
|
||
Returns: | ||
A hash containing the user information, or an None if the user | ||
could not be found. | ||
""" | ||
result = self._GET('/user.query', {'phids[]': [phid]}) | ||
return result[0] if result else None | ||
|
||
def get_repo(self, phid): | ||
""" Get basic information about a repo based on its phid. | ||
|
||
Args: | ||
phid: The phid of the repo to lookup. | ||
|
||
Returns: | ||
A hash containing the repo info, or None if the repo isn't found. | ||
""" | ||
result = self._GET('/phid.query', {'phids[]': [phid]}) | ||
return result.get(phid) if result else None | ||
|
||
def _request(self, url, data=None, params=None, method='GET'): | ||
data = data if data else {} | ||
data['api.token'] = self.api_key | ||
response = requests.request( | ||
method=method, | ||
url=self.api_url + url, | ||
params=params, | ||
data=data, | ||
timeout=10 | ||
).json() | ||
|
||
if response['error_code']: | ||
exp = PhabricatorAPIException(response.get('error_info')) | ||
exp.error_code = response.get('error_code') | ||
exp.error_info = response.get('error_info') | ||
raise exp | ||
|
||
return response.get('result') | ||
|
||
def _GET(self, url, data=None, params=None): | ||
return self._request(url, data, params, 'GET') | ||
|
||
def _POST(self, url, data=None, params=None): | ||
return self._request(url, data, params, 'POST') | ||
|
||
|
||
class PhabricatorAPIException(Exception): | ||
""" An exception class to handle errors from the Phabricator API """ | ||
error_code = None | ||
error_info = None |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this the file from
python-phabricator
? That project says it's licensed under Apache 2.0. If this code is from that project, then our project becomes a mixed-license work (not a bad thing), which means we should include a NOTICE file in the project root that contains the Apache 2.0 license text.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, as you see in the comments above I decided against using any library since making the requests ourselves was actually pretty simple and it makes our testing story a lot easier with requests-mock. So this is a file that I wrote.