Skip to content

Commit

Permalink
Merge pull request puppetlabs#26 from cowofevil/feature/master/QA-266…
Browse files Browse the repository at this point in the history
…7/update_format_for_maint

(QA-2667) Update "parse_status" to Track Maintenance
  • Loading branch information
zreichert authored Oct 20, 2016
2 parents 5e2a05d + 4da678f commit 504c70c
Show file tree
Hide file tree
Showing 23 changed files with 100 additions and 61 deletions.
54 changes: 34 additions & 20 deletions tools/parse_status/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,40 +47,54 @@ The YAML file contents should look like the following:
```yaml
# Project Commitments
---
Alpha: {
commitment: 4
}
Beta: {
-
project: Alpha
commitment: 2
maintenance: no
-
project: Alpha
commitment: 2
maintenance: yes
comment: "Addressed some old tech debt for the project."
-
project: Beta
commitment: 1
}
Charlie: {
maintenance: yes
-
project: Charlie
commitment: 1
}
Delta: {
maintenance: yes
-
project: Delta
commitment: 0,
maintenance: no
comment: "Blocked because of outside vendor"
}
Other: {
commitment: 2,
-
project: Other
commitment: 2
maintenance: no
comment: "Writing proposal for PuppetConf"
}
Consulting: {
commitment: 2,
-
project: Consulting
commitment: 2
# Unspecified "maintenance" key is interpreted as "maintenance: no"
comment: "Mentoring new hire."
}
```
**Content Constraints**
- Do not repeat the same project multiple times.
* If you perform distinct activities within a project use the "comment" key to provide more details.
- Each project can only have one "commitment" key and one "comment" key.
- If you perform distinct activities within a project use the "comment" key to provide more details.
- Set the "maintenance" key to "yes" to indicate that the work was maintenance related.
* Setting the "maintenance" key to "no" implies that the work was feature related.
* Excluding the "maintenance" key implies that the work was feature related.
* A single project can have multiple entries.
- Each project entry can only have one "commitment", "maintenance" or "comment" key.
### YAML File Naming
The `parse_status_app.py` utility expects a directory with YAML files. The YAML file names need to
follow the format `FIRST_LAST.yaml`. For Irish last names with prefixes use the format
`FIRST_PREFIX-LAST.yaml`.
follow the format `<FIRST> <LAST>.yaml` using appropriate casing. For example a report YAML file
name for someone with an Irish last name would look like `Paula McMaw.yaml`.

## Workflow

Expand Down
107 changes: 66 additions & 41 deletions tools/parse_status/parse_status_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,10 @@
import io
from argparse import ArgumentParser
from os import listdir
from os.path import basename, join
from os.path import basename, join, splitext
from fnmatch import filter
from datetime import date
from csv import writer
from re import match
try:
from yaml import load, YAMLError
except ImportError:
Expand All @@ -34,20 +33,23 @@ class ProjectCommitmentRecord(object):
Args:
project_name |str| = The name of the project.
commitment_level |int| = An integer in the range of 0-10 representing commitment.
work_type |str| = The type of work for the given commitment. ("feature" or "maintenance")
comment |str| = An optional free-form comment describing the commitment.
Raises:
|RuntimeError| = Provided commitment level is out of range. (0-10)
"""

def __init__(self, project_name, commitment_level, comment=''):
def __init__(self, project_name, commitment_level, work_type, comment=''):
self._project_name = project_name
self._work_type = work_type
self._comment = comment

if 0 <= commitment_level <= 10:
self._commitment_level = commitment_level
else:
raise RuntimeError('Provided commitment level for the "{}" project '
'is out of range!'.format(project_name))
self._comment = comment

@property
def project_name(self):
Expand All @@ -69,6 +71,16 @@ def commitment_level(self):

return self._commitment_level

@property
def work_type(self):
"""The type of work for the given commitment. ("feature" or "maintenance")
Returns:
|str|
"""

return self._work_type

@property
def comment(self):
"""An optional free-form comment describing the commitment.
Expand Down Expand Up @@ -96,12 +108,13 @@ def __init__(self, team_member, status_date):
self._status_date = status_date
self._commitments = [] #[ProjectCommitmentRecord]

def add_commitment_record(self, project_name, commitment_level, comment=''):
def add_commitment_record(self, project_name, commitment_level, work_type, comment=''):
"""Add a project commitment record for the given team member.
Args:
project_name |str| = The name of the team member associated with project commitments.
commitment_level |int| = An integer in the range of 0-5 representing commitment.
commitment_level |int| = An integer in the range of 0-10 representing commitment.
work_type |str| = The type of work for the given commitment. ("feature" or "maintenance")
comment |str| = An optional free-form comment describing the commitment.
Returns:
Expand All @@ -112,9 +125,10 @@ def add_commitment_record(self, project_name, commitment_level, comment=''):
"""

try:
self._commitments.append(ProjectCommitmentRecord(project_name, commitment_level, comment))
self._commitments.append(
ProjectCommitmentRecord(project_name, commitment_level, work_type, comment))
except RuntimeError as e:
raise RuntimeError('{} For the "{}" team member.'.format(e.msg, self._team_member))
raise RuntimeError('{} for the "{}" team member.'.format(e.msg, self._team_member))

def validate(self):
"""Validate that the commitment level is 10 for the given team member.
Expand All @@ -138,7 +152,12 @@ def commitment_status(self):
|[[obj]]|
"""

return [[self._team_member, self._status_date, x.project_name, x.commitment_level, x.comment]
return [[self._team_member,
self._status_date,
x.project_name,
x.commitment_level,
x.work_type,
x.comment]
for x in self._commitments]


Expand All @@ -160,6 +179,7 @@ def __init__(self, yaml_reports, report_output_path, report_date):
self._report_output_path = report_output_path
self._report_date = report_date
self._status_database = [] # [ProjectCommitmentStatus]
self._empty_reports = [] # List of empty file report paths

self._load_yaml_reports()

Expand All @@ -173,30 +193,10 @@ def _get_team_member_name(self, yaml_file_path):
|str| = A human readable team member name derived from the YAML file path.
Raises:
|RuntimeError| = The YAML file name does not match the expected pattern.
|None|
"""

first_last_pattern = r"([a-z]+)_([a-z\-]+).yaml"
irish_last_pattern = r"([a-z]+)\-([a-z]+)"

try:
match_obj = match(first_last_pattern, basename(yaml_file_path))
first_name = match_obj.group(1).capitalize()
last_name = match_obj.group(2)

# Account for Irish last names.
if match(irish_last_pattern, last_name):
last_name = match(irish_last_pattern, last_name).group(1).capitalize() + \
match(irish_last_pattern, last_name).group(2).capitalize()
else:
last_name = last_name.capitalize()

team_member_name = "{} {}".format(first_name, last_name)
except (IndexError, AttributeError):
raise RuntimeError('The YAML file name "{}" does not match'
' the expected pattern!'.format(yaml_file_path))

return team_member_name
return splitext(basename(yaml_file_path))[0]

def _fix_smart_quotes(self, input_string):
"""Coerce smart quotes into straight quotes. Do nothing if smart quotes are not detected.
Expand All @@ -213,11 +213,12 @@ def _fix_smart_quotes(self, input_string):

return input_string.replace(u'\u201c','"').replace(u'\u201d','"')

def _import_commitment_status(self, team_member, info_dict):
def _import_commitment_status(self, team_member, info_list):
"""Import project commitment records for a given user.
Args:
info_dict |{str:str}| = A nested dictionary containing commitment records.
team_member |str| = The team member name to collect status for.
info_list |[{str:str}]| = A list of dictionaries containing commitment records.
Returns:
|None|
Expand All @@ -230,11 +231,19 @@ def _import_commitment_status(self, team_member, info_dict):
status = ProjectCommitmentStatus(team_member, self._report_date)

try:
for project in info_dict:
commitment_level = int(info_dict[project]['commitment'])
comment = info_dict[project]['comment'] if 'comment' in info_dict[project] else ""

status.add_commitment_record(project, commitment_level, comment)
for record in info_list:
project = record['project']
commitment_level = int(record['commitment'])
comment = record['comment'] if 'comment' in record else ""

if 'maintenance' in record:
if type(record['maintenance']) is not bool:
raise RuntimeError("The 'maintenance' key must be either 'yes' or 'no'!")
work_type = 'maintenance' if record['maintenance'] else 'feature'
else:
work_type = 'feature'

status.add_commitment_record(project, commitment_level, work_type, comment)
except KeyError:
raise RuntimeError('The YAML report does not follow expected format!')

Expand Down Expand Up @@ -264,8 +273,9 @@ def _load_yaml_reports(self):
with io.open(yaml_report, 'r', encoding='utf8') as yamlfile:
yamlfile_contents = self._fix_smart_quotes(yamlfile.read())

# Ignore empty files.
if yamlfile_contents == '':
# Record and ignore empty files.
if not yamlfile_contents:
self._empty_reports.append(yaml_report)
continue

self._import_commitment_status(self._get_team_member_name(yaml_report),
Expand Down Expand Up @@ -295,6 +305,16 @@ def generate_csv_report(self):
except (IOError, OSError):
raise RuntimeError('Failed to write report to disk!')

@property
def empty_reports(self):
"""A list of paths to empty report files.
Returns:
|[str]|
"""

return self._empty_reports


#===================================================================================================
# Functions
Expand All @@ -319,7 +339,7 @@ def get_yaml_files(yaml_dir):


#===================================================================================================
# Main
# Main & Command-line Parser
#===================================================================================================
def parse_cli(argv):
"""Parse the command-line and validate user input.
Expand Down Expand Up @@ -390,6 +410,11 @@ def main(argv):

report.generate_csv_report()

print('\nEmpty reports:')
if not report.empty_reports: print(' None')
for empty_report_path in report.empty_reports:
print(" {}".format(empty_report_path))

print('\nSuccess!')
except RuntimeError as e:
exit_code = 1
Expand Down
Empty file.

0 comments on commit 504c70c

Please sign in to comment.