From bf88100e43e4b24273b5145ef386d8ec0208d8df Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Mon, 8 Oct 2018 20:10:18 -0400 Subject: [PATCH] add a script to randomly assign TC members as liaisons This script reads the TC liaison assignments from the wiki and fills in the gaps by randomly assigning members to work with teams. Change-Id: I1d7eaad0e78fd020472fa560f08b5e7bfb9028b5 Signed-off-by: Doug Hellmann --- openstack_governance/_wiki.py | 93 +++++++++++++++++++++++++++++++ requirements.txt | 1 + tools/assign_liaisons.py | 101 ++++++++++++++++++++++++++++++++++ 3 files changed, 195 insertions(+) create mode 100644 openstack_governance/_wiki.py create mode 100644 tools/assign_liaisons.py diff --git a/openstack_governance/_wiki.py b/openstack_governance/_wiki.py new file mode 100644 index 000000000..6286bb708 --- /dev/null +++ b/openstack_governance/_wiki.py @@ -0,0 +1,93 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Do dirty things with wikis. +""" + +import collections +import itertools + +import mwclient + + +def get_page_section(page_content, section_start): + "Return iterable of lines making up a section of a wiki page." + lines = page_content.splitlines() + lines = itertools.dropwhile( + lambda x: x != section_start, + lines, + ) + next(lines) # skip the start_section line + lines = itertools.takewhile( + lambda x: not x.startswith('='), # another section or subsection + lines, + ) + return lines + + +def get_wiki_table(section_content): + """Return iterable of dicts making up rows of a wiki table. + + Assumes there is only one table per section. + + """ + lines = itertools.dropwhile( + lambda x: x != '{| class="wikitable"', + section_content, + ) + headings = [] + items = [] + for line in lines: + if line == '|-': + continue + elif line.startswith('!'): + headings = [h.strip() for h in line.lstrip('!').split('!!')] + elif line in ['}', '|}']: + # end of table + break + elif line.startswith('|'): + items.extend(i.strip() for i in line.lstrip('|').split('||')) + + if len(items) == len(headings): + row = { + h: i + for (h, i) in zip(headings, items) + } + yield row + items = [] + + +def get_wiki_page(name): + "Return the text of a wiki page as a string." + site = mwclient.Site('wiki.openstack.org') + page = site.Pages[name] + return page.text() + + +def get_liaison_data(): + "Return team -> liaisons mapping" + project_to_liaisons = collections.OrderedDict() + wiki_page = get_wiki_page('OpenStack_health_tracker') + section = get_page_section(wiki_page, '=== Project Teams ===') + table = get_wiki_table(section) + + for row in table: + if not row: + continue + liaisons = [ + m.strip() + for m in row['TC members'].split(',') + if m.strip() + ] + project_to_liaisons[row['Group']] = liaisons + + return project_to_liaisons diff --git a/requirements.txt b/requirements.txt index 0a3582058..c766810c7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ pydot2>=1.0.32 PyYAML>=3.1.0 six>=1.9.0 yamlordereddictloader +mwclient==0.8.1 diff --git a/tools/assign_liaisons.py b/tools/assign_liaisons.py new file mode 100644 index 000000000..6b0870635 --- /dev/null +++ b/tools/assign_liaisons.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 + +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import argparse +import collections +import random +import textwrap + +from openstack_governance import _wiki +from openstack_governance import members +from openstack_governance import projects + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + '--member-file', + default='reference/members', + help='location of members file, (%(default)s)', + ) + parser.add_argument( + '--projects-file', + default='reference/projects.yaml', + help='location of projects.yaml, (%(default)s)', + ) + args = parser.parse_args() + + member_nics = [ + m['irc'] + for m in members.parse_members_file(args.member_file) + ] + + project_data = projects.load_project_file(args.projects_file) + project_names = list(project_data.keys()) + + num_teams = len(project_names) + assignments_per = num_teams // (len(member_nics) // 2) + + print('TEAMS', num_teams) + print('TC ', len(member_nics)) + print('PER ', assignments_per) + + # Determine how many assignments everyone has by reading the wiki + # page. + + project_to_liaisons = _wiki.get_liaison_data() + + member_counts = collections.Counter({ + nic: 0 + for nic in member_nics + }) + for team, liaisons in project_to_liaisons.items(): + for member in liaisons: + member_counts.update({member: 1}) + + print('\nAlready assigned:') + for member, count in sorted(member_counts.items()): + print('{:12}: {}'.format(member, count)) + + choices = [] + for member, count in sorted(member_counts.items()): + choices.extend([member] * (assignments_per - count)) + + # Make sure we have a list in order that isn't assigning the same + # person to a team twice. + print() + for team, liaisons in project_to_liaisons.items(): + while len(liaisons) < 2: + random.shuffle(choices) + next_choice = choices.pop() + while next_choice in liaisons: + choices.insert(0, next_choice) + next_choice = choices.pop() + print('assigning {} to {}'.format(next_choice, team)) + liaisons.append(next_choice) + + print(textwrap.dedent(''' + === Project Teams === + + {| class="wikitable" + |- + ! Group !! TC members''')) + + for team, liaisons in project_to_liaisons.items(): + print('|-\n| {} || {}'.format(team, ', '.join(liaisons))) + + print('|}\n') + +if __name__ == '__main__': + main()