add/enhance docstrings

Change-Id: I955450cb3dabb38efc2f9e163981d71a55fc4a6f
Signed-off-by: Doug Hellmann <doug@doughellmann.com>
This commit is contained in:
Doug Hellmann 2018-04-29 20:07:25 -04:00
parent a041252855
commit dc98f770e2
7 changed files with 74 additions and 5 deletions

View File

@ -19,8 +19,16 @@ LOG = logging.getLogger(__name__)
def requester(url, params={}, headers={}): def requester(url, params={}, headers={}):
"""A requests wrapper to consistently retry HTTPS queries""" """A requests wrapper to consistently retry HTTPS queries
:param url: The URL to get.
:type url: str
:param params: Additional parameters to provide.
:type params: dict(str, str)
:param headers: Additional headers to set.
:type params: dict(str, str)
"""
# Try up to 3 times # Try up to 3 times
retry = requests.Session() retry = requests.Session()
retry.mount("https://", requests.adapters.HTTPAdapter(max_retries=3)) retry.mount("https://", requests.adapters.HTTPAdapter(max_retries=3))
@ -28,7 +36,15 @@ def requester(url, params={}, headers={}):
def decode_json(raw): def decode_json(raw):
"""Trap JSON decoding failures and provide more detailed errors""" """Trap JSON decoding failures and provide more detailed errors
Remove ')]}' XSS prefix from data if it is present, then decode it
as JSON and return the results.
:param raw: Response text from API
:type raw: str
"""
# Gerrit's REST API prepends a JSON-breaker to avoid XSS vulnerabilities # Gerrit's REST API prepends a JSON-breaker to avoid XSS vulnerabilities
if raw.text.startswith(")]}'"): if raw.text.startswith(")]}'"):

View File

@ -14,6 +14,16 @@ import shelve
class Cache: class Cache:
"""Data cache with transparent key management
Keys passed to methods are expected to be tuples of strings but
are converted to something the underlying implementation can
store and retrieve.
Values stored in the cache are pickled before being written and
unpickled before being returned.
"""
def __init__(self, filename): def __init__(self, filename):
self._shelf = shelve.open(filename) self._shelf = shelve.open(filename)

View File

@ -22,20 +22,24 @@ MEMBER_LOOKUP_URL = 'https://openstackid-resources.openstack.org/'
class Affiliation: class Affiliation:
"A Foundation member relationship to an employer"
def __init__(self, data): def __init__(self, data):
self._data = data self._data = data
@property @property
def organization(self): def organization(self):
"The name of the employer"
return self._data['organization']['name'] return self._data['organization']['name']
@property @property
def is_current(self): def is_current(self):
"Boolean indicating if the affiliation is set to be current"
return self._data.get('is_current', False) return self._data.get('is_current', False)
@property @property
def start_date(self): def start_date(self):
"Start date of affiliation, if given"
start = self._data['start_date'] start = self._data['start_date']
if start: if start:
return datetime.datetime.utcfromtimestamp(start) return datetime.datetime.utcfromtimestamp(start)
@ -43,16 +47,32 @@ class Affiliation:
@property @property
def end_date(self): def end_date(self):
"End date of affiliation, if given"
end = self._data['end_date'] end = self._data['end_date']
if end: if end:
return datetime.datetime.utcfromtimestamp(end) return datetime.datetime.utcfromtimestamp(end)
return None return None
def active(self, when): def active(self, when):
"""Is the affiliation was in effect on the date specified.
If we have a current affiliation without start and end dates,
assume it is active.
Otherwise the start date and end dates are compared to the
date provided to determine if it falls within the inclusive
range.
Although the argument needs to be a datetime instance, only
the date portion is used for comparison. We assume that
someone does not change affiliations on the same day.
:param when: The date to check for active status
:type when: datetime.datetime
"""
if not self.start_date and not self.end_date and self.is_current: if not self.start_date and not self.end_date and self.is_current:
return True return True
# Compare only the date portion so we don't have to worry
# about the time of day.
if self.start_date and self.start_date.date() > when.date(): if self.start_date and self.start_date.date() > when.date():
return False return False
if self.end_date and self.end_date.date() < when.date(): if self.end_date and self.end_date.date() < when.date():
@ -61,6 +81,7 @@ class Affiliation:
class Member: class Member:
"A person who is a member of the Foundation"
def __init__(self, email, data): def __init__(self, email, data):
self.email = email self.email = email
@ -68,6 +89,7 @@ class Member:
@property @property
def name(self): def name(self):
"The person's full name"
return ' '.join([self._data['first_name'], self._data['last_name']]) return ' '.join([self._data['first_name'], self._data['last_name']])
@property @property
@ -100,11 +122,18 @@ def lookup_member(email):
}, },
headers={'Accept': 'application/json'}, headers={'Accept': 'application/json'},
) )
return apis.decode_json(raw)['data'][0] return apis.decode_json(raw)['data'][0]
def fetch_member(email, cache): def fetch_member(email, cache):
"""Find the member in the cache or look it up in the API.
:param email: Email address of the member to look for.
:type email: str
:param cache: Storage for repeated lookups.
:type cache: goal_tools.cache.Cache
"""
key = ('member', email) key = ('member', email)
if key in cache: if key in cache:
LOG.debug('found %s cached', email) LOG.debug('found %s cached', email)

View File

@ -62,6 +62,7 @@ def query_gerrit(method, params={}):
def _to_datetime(s): def _to_datetime(s):
"Convert a string to a datetime.datetime instance"
# Ignore the trailing decimal seconds. # Ignore the trailing decimal seconds.
s = s.rpartition('.')[0] s = s.rpartition('.')[0]
return datetime.datetime.strptime(s, '%Y-%m-%d %H:%M:%S') return datetime.datetime.strptime(s, '%Y-%m-%d %H:%M:%S')
@ -72,6 +73,7 @@ Participant = collections.namedtuple(
class Review: class Review:
"The history of one code review"
def __init__(self, id, data): def __init__(self, id, data):
self._id = id self._id = id
@ -127,6 +129,14 @@ class Review:
def fetch_review(review_id, cache): def fetch_review(review_id, cache):
"""Find the review in the cache or look it up in the API.
:param review_id: Review ID of the review to look for.
:type review_id: str
:param cache: Storage for repeated lookups.
:type cache: goal_tools.cache.Cache
"""
key = ('review', review_id) key = ('review', review_id)
if key in cache: if key in cache:
LOG.debug('found %s cached', review_id) LOG.debug('found %s cached', review_id)

View File

@ -25,6 +25,7 @@ LOG = logging.getLogger(__name__)
class DateColumn(columns.FormattableColumn): class DateColumn(columns.FormattableColumn):
"Format a datetime.datetime to make it serializable."
def human_readable(self): def human_readable(self):
return str(self._value) return str(self._value)

View File

@ -23,6 +23,8 @@ from goal_tools import caching
class WhoHelped(app.App): class WhoHelped(app.App):
"""Tool for extracting data and statistics about contributors to projects.
"""
def __init__(self): def __init__(self):
version_info = pbr.version.VersionInfo('goal-tools') version_info = pbr.version.VersionInfo('goal-tools')

View File

@ -16,6 +16,7 @@ from goal_tools import foundation
class ShowMember(show.ShowOne): class ShowMember(show.ShowOne):
"Show a Foundation member's basic information."
def get_parser(self, prog_name): def get_parser(self, prog_name):
parser = super().get_parser(prog_name) parser = super().get_parser(prog_name)