Skip to content
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 3 commits into from
Jun 5, 2017
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions dev-requirements.txt
Original file line number Diff line number Diff line change
@@ -25,3 +25,6 @@ mccabe==0.6.1 \
py==1.4.33 \
--hash=sha256:81b5e37db3cc1052de438375605fb5d3b3e97f950f415f9143f04697c684d7eb \
--hash=sha256:1f9a981438f2acc20470b301a07a496375641f902320f70e31916fe3377385a9
requests-mock==1.3.0 \
--hash=sha256:23edd6f7926aa13b88bf79cb467632ba2dd5a253034e9f41563f60ed305620c7

3 changes: 3 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -10,9 +10,12 @@ services:
dockerfile: ./docker/Dockerfile-dev
volumes:
- ./:/app
ports:
- "8888:80"
environment:
- PORT=80
- VERSION_PATH=/version.json
- PHABRICATOR_URL=https://mozphab.dev.mozaws.net
py3-linter:
build:
context: ./
106 changes: 96 additions & 10 deletions landoapi/api/revisions.py
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,
}
108 changes: 108 additions & 0 deletions landoapi/phabricator_client.py
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
Copy link
Contributor

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.

Copy link
Contributor Author

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.

# 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
Loading
Oops, something went wrong.