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()