diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..67db858 --- /dev/null +++ b/LICENSE @@ -0,0 +1,175 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..90f8a7a --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,6 @@ +include AUTHORS +include ChangeLog +exclude .gitignore +exclude .gitreview + +global-exclude *.pyc \ No newline at end of file diff --git a/README.md b/README.rst similarity index 100% rename from README.md rename to README.rst diff --git a/gerrit-powered-agenda/const.py b/gerrit-powered-agenda/const.py new file mode 100644 index 0000000..7099cb5 --- /dev/null +++ b/gerrit-powered-agenda/const.py @@ -0,0 +1,25 @@ +#! /usr/bin/env python +# +# Copyright 2014 North Dakota State University +# +# 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. + +YAML_FILE_EXT = ('.yaml', '.yml') + +WEEKDAYS = {'Monday': 0, + 'Tuesday': 1, + 'Wednesday': 2, + 'Thursday': 3, + 'Friday': 4, + 'Saturday': 5, + 'Sunday': 6} diff --git a/gerrit-powered-agenda/jobs.py b/gerrit-powered-agenda/jobs.py new file mode 100644 index 0000000..a8861d4 --- /dev/null +++ b/gerrit-powered-agenda/jobs.py @@ -0,0 +1,73 @@ +#! /usr/bin/env python +# +# Copyright 2014 North Dakota State University +# +# 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 logging +import os +import yaml + +import const +from meeting import Meeting + +# logging settings +logging.basicConfig(format='%(asctime)s - %(levelname)s - %(message)s', + level=logging.DEBUG) + +yaml_dir = '../meetings' +ical_dir = '../icals' +# NOTE(jotan): publish_url should be changed to the wiki URL eventually +publish_url = '127.0.0.1' + + +class MeetingJobs: + """Executes post, gate, and check jobs.""" + + def execute_check(self): + """Execute check job.""" + + logging.info('Check job initiated.') + meetings = self.retrieve_meetings(yaml_dir) + + # convert meetings to a list of ical + for m in meetings: + m.write_ical() + logging.info('Wrote %d meetings to iCal' % (len(meetings))) + logging.info('Check job finished.') + + def execute_gate(self): + """Execute gate job.""" + + pass + + def execute_post(self): + """Execute post job.""" + + pass + + def retrieve_meetings(self, yaml_dir): + """Return a list of Meetings initialized from files in yaml_dir.""" + + os.chdir(yaml_dir) + meetings_yaml = [f for f in os.listdir() + if os.path.isfile(f) and + f.endswith(const.YAML_FILE_EXT)] + meetings = [Meeting(yaml.load(open(f, 'r')), f) + for f in meetings_yaml] + logging.info('Loaded %d meetings from YAML' % (len(meetings))) + return meetings + +# entry point +jobs = MeetingJobs() +jobs.execute_check() diff --git a/gerrit-powered-agenda/meeting.py b/gerrit-powered-agenda/meeting.py new file mode 100644 index 0000000..d5597fd --- /dev/null +++ b/gerrit-powered-agenda/meeting.py @@ -0,0 +1,134 @@ +#! /usr/bin/env python +# +# Copyright 2014 North Dakota State University +# +# 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 datetime +import icalendar +import logging +import os +import pytz +import yaml + +import const + + +class Meeting: + """An OpenStack meeting.""" + + def __init__(self, yaml, filename): + """Initialize meeting from yaml file name 'filename'.""" + + self.filename = filename + + # initialize using yaml + self.project = yaml['project'] + self.chair = yaml['chair'] + self.description = yaml['description'] + self.agenda = yaml['agenda'] # this is a list of list of topics + + # create schedule objects + self.schedules = [Schedule(schedule) for schedule in yaml['schedule']] + + def write_ical(self): + """Write this meeting to disk using the iCal format.""" + cal = icalendar.Calendar() + + # add properties to ensure compliance + cal.add('prodid', '-//OpenStack//Gerrit-Powered Meeting Agendas//EN') + cal.add('version', '2.0') + + i = 1 + for schedule in self.schedules: + # one Event per iCal file + event = icalendar.Event() + + # NOTE(jotan): I think the summary field needs to be unique per + # event in an ical file (at least, for it to work with + # Google Calendar) + + event.add('summary', self.project + ' ' + str(i)) + + # add ical description + project_descript = "Project: %s" % (self.project) + chair_descript = "Chair: %s" % (self.chair) + irc_descript = "IRC: %s" % (schedule.irc) + agenda_yaml = yaml.dump(self.agenda, default_flow_style=False) + agenda_descript = "Agenda:\n%s\n" % (agenda_yaml) + descript_descript = "Description: %s" % (self.description) + ical_descript = "\n".join((project_descript, + chair_descript, + irc_descript, + agenda_descript, + descript_descript)) + event.add('description', ical_descript) + + # get starting date + d = datetime.datetime.utcnow() + next_meeting = next_weekday(d, const.WEEKDAYS[schedule.day]) + + next_meeting_dt = datetime.datetime(next_meeting.year, + next_meeting.month, + next_meeting.day, + schedule.time.hour, + schedule.time.minute, + tzinfo=pytz.utc) + event.add('dtstart', next_meeting_dt) + + # add recurrence rule + event.add('rrule', {'freq': schedule.freq}) + + # add meeting length + # TODO(jotan): determine duration to use for OpenStack meetings + event.add('duration', datetime.timedelta(hours=1)) + + # add event to calendar + cal.add_component(event) + i += 1 + + # write ical files to disk + ical_dir = '../icals' + ical_filename = self.filename[:-4] + 'ics' + + if not os.path.exists(ical_dir): + os.makedirs(ical_dir) + os.chdir(ical_dir) + + with open(ical_filename, 'wb') as ics: + ics.write(cal.to_ical()) + + num_events = len(cal.subcomponents) + logging.info('\'%s\' processed. [%d event(s)]' % (ical_filename, + num_events)) + + +class Schedule: + """A meeting schedule.""" + + def __init__(self, sched_yaml): + """Initialize schedule from yaml.""" + + self.time = datetime.datetime.strptime(sched_yaml['time'], '%H%M') + self.day = sched_yaml['day'] + self.irc = sched_yaml['irc'] + self.freq = sched_yaml['frequency'] + + +def next_weekday(ref_date, weekday): + """Return the date of the next weekday after ref_date.""" + + days_ahead = weekday - ref_date.weekday() + if days_ahead <= 0: # target day already happened this week + days_ahead += 7 + return ref_date + datetime.timedelta(days_ahead) diff --git a/gerrit-powered-agenda/util.py b/gerrit-powered-agenda/util.py new file mode 100644 index 0000000..023c2aa --- /dev/null +++ b/gerrit-powered-agenda/util.py @@ -0,0 +1,29 @@ +#! /usr/bin/env python +# +# Copyright 2014 North Dakota State University +# +# 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. + + +class MeetingUtil: + """Utility functions.""" + + def publish(meeting, ical): + """Publish meeting information and ical file to wiki.""" + + pass + + def creates_conflicts(meeting): + """Return whether the meeting would create scheduling conflicts.""" + + pass diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ab63889 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +pbr + +pyyaml +icalendar diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..5ccb419 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,18 @@ +[metadata] +name = gerrit-powered-agenda +summary = Automates process for testing and publishing changes to OpenStack meetings using existing OpenStack project infrastructure. +description-file = + README.rst +author = NDSU IBM Capstone Group +author-email = joshua.tan@ndsu.edu +home-page = https://github.com/openstack-infra/gerrit-powered-agenda +classifier = + Intended Audience :: OpenStack Community + License :: OSI Approved :: Apache Software License + Operating System :: POSIX :: Linux + Programming Language :: Python + Programming Language :: Python :: 3.3 + +[global] +setup-hooks = + pbr.hooks.setup_hook diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..b96e399 --- /dev/null +++ b/setup.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python + +import setuptools + +setuptools.setup( + setup_requires=['pbr'], + pbr=True) diff --git a/src/jobs.py b/src/jobs.py deleted file mode 100644 index 2107379..0000000 --- a/src/jobs.py +++ /dev/null @@ -1,58 +0,0 @@ -import yaml -import icalendar -import pprint -import sys -import os -import uuid -from meeting import Meeting -import logging - -# logging settings -logging.basicConfig(format='%(asctime)s - %(levelname)s - %(message)s', level=logging.DEBUG) - -yaml_dir = '../meetings' -ical_dir = '../icals' -publish_url = '127.0.0.1' - -# should we make execute_gate(), etc. static methods instead? -class MeetingJobs: - """Executes post, gate, and check jobs.""" - - def execute_check(self): - logging.info('Check job initiated.') - meetings = self.load_meetings(yaml_dir) - - # now convert meetings to a list of ical - for m in meetings: - m.write_ical() - logging.info('Wrote %d meetings to iCal' % (len(meetings))) - logging.info('Check job finished.') - - def execute_gate(self): - pass - - def execute_post(self): - pass - - def load_meetings(self, yaml_dir): - os.chdir(yaml_dir) - meetings_yaml = [f for f in os.listdir() if os.path.isfile(f) and f.endswith('yaml')] - meetings = [Meeting(yaml.load(open(f, 'r')), f) for f in meetings_yaml] - logging.info('Loaded %d meetings from YAML' % (len(meetings))) - return meetings - -def pprint_yaml(): - """For now, this is a simple script to import all the yaml files and pretty print it.""" - - # change the current directory to the meetings directory where all the yaml files are located - os.chdir(yaml_dir) - - # get a list of all the yaml files - meetings = [yaml.load(open(f, 'r')) for f in os.listdir() if os.path.isfile(f) and ".yaml" in f] - - for m in meetings: - print(yaml.dump(m)) - -# entry point -jobs = MeetingJobs() -jobs.execute_check() diff --git a/src/meeting.py b/src/meeting.py deleted file mode 100644 index d953436..0000000 --- a/src/meeting.py +++ /dev/null @@ -1,99 +0,0 @@ -import pprint -import pytz -import icalendar -import datetime -import time -import os -import yaml -import logging - -weekdays = { - 'Monday' : 0 - , 'Tuesday' : 1 - , 'Wednesday' : 2 - , 'Thursday' : 3 - , 'Friday' : 4 - , 'Saturday' : 5 - , 'Sunday' : 6 - } - -class Meeting: - """An OpenStack meeting.""" - - def __init__(self, yaml, filename): - - self.filename = filename - - # create yaml object from yaml file. use it initialize following fields. - self.project = yaml['project'] - self.chair = yaml['chair'] - self.description = yaml['description'] - self.agenda = yaml['agenda'] # this is a list of list of topics - - # create schedule objects - self.schedules = [Schedule(s) for s in yaml['schedule']] - - def write_ical(self): - cal = icalendar.Calendar() - - # add properties to ensure compliance - cal.add('prodid', '-//OpenStack//Gerrit-Powered Meeting Agendas//EN') - cal.add('version', '2.0') - - i = 1 - for s in self.schedules: - # one Event per iCal file - event = icalendar.Event() - # I think the summary field needs to be unique per event in an ical file (at least, for it to work with Google Calendar) - event.add('summary', self.project + ' ' + str(i)) - - # add ical description (meeting description, irc, agenda, chair, etc.) - ical_descript = "Project: %s\nChair: %s\nIRC: %s\nAgenda:\n%s\n\nDescription: %s" % (self.project, self.chair, s.irc, yaml.dump(self.agenda, default_flow_style=False), self.description) - event.add('description', ical_descript) - - # get starting date - d = datetime.datetime.utcnow() - next_meeting = next_weekday(d, weekdays[s.day]) # 0 = Monday, 1=Tuesday, 2=Wednesday... - - next_meeting_dt = datetime.datetime(next_meeting.year, next_meeting.month, next_meeting.day, s.time.hour, s.time.minute, tzinfo=pytz.utc) - event.add('dtstart', next_meeting_dt) - - # add recurrence rule - event.add('rrule', {'freq': s.freq}) - - # add meeting length - # TODO: figure out what to do for meeting length. doesn't seem to be specified for any of the openstack meetings - event.add('duration', datetime.timedelta(hours=1)) - - # add event to calendar - cal.add_component(event) - i += 1 - - # write ical files to disk - ical_dir = '../icals' - ical_filename = self.filename[:-4] + 'ics' - - if not os.path.exists(ical_dir): - os.makedirs(ical_dir) - os.chdir(ical_dir) - - with open(ical_filename, 'wb') as ics: - ics.write(cal.to_ical()) - - logging.info('\'%s\' processed. Contains %d events.' % (ical_filename, len(cal.subcomponents))) - -class Schedule: - """A meeting schedule.""" - - def __init__(self, sched_yaml): - self.time = datetime.datetime.strptime(sched_yaml['time'], '%H%M') - self.day = sched_yaml['day'] - self.irc = sched_yaml['irc'] - self.freq = sched_yaml['frequency'] - -# https://stackoverflow.com/questions/6558535/python-find-the-date-for-the-first-monday-after-a-given-a-date -def next_weekday(d, weekday): - days_ahead = weekday - d.weekday() - if days_ahead <= 0: # Target day already happened this week - days_ahead += 7 - return d + datetime.timedelta(days_ahead) diff --git a/src/util.py b/src/util.py deleted file mode 100644 index 84d86c3..0000000 --- a/src/util.py +++ /dev/null @@ -1,15 +0,0 @@ -class MeetingUtil: - """Utility functions.""" - - def convert_to_ical(yaml): - pass - - def publish(meeting, ical): - pass - - def check_conflicts(meeting): - pass - - def check_syntax(yaml): - pass - diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..f0dd00b --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1 @@ +hacking>=0.5.6,<0.8 diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..fdfad68 --- /dev/null +++ b/tox.ini @@ -0,0 +1,14 @@ + +[testenv] +deps = -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt + +[testenv:pep8] +commands = flake8 + +[testenv:venv] +commands = {posargs} + +[flake8] +show-source = True +exclude = .venv,.tox,dist,doc,*.egg \ No newline at end of file