cbf3cc9338
This makes sphinx-build use the number of cores available to make the docs build parallel. On my 16 core system, this cut the time by ~50%. We don't have as many cores available in the gate, but it should provide a slight improvement in overall job time. Change-Id: I7ad4d87a3dba10475a0fcf797a9aa2d4b1ad0eb5 Signed-off-by: Sean McGinnis <sean.mcginnis@gmail.com>
237 lines
7.9 KiB
Python
237 lines
7.9 KiB
Python
# 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 json
|
|
import os
|
|
import os.path
|
|
import subprocess
|
|
|
|
from docutils.io import FileOutput
|
|
from docutils import nodes
|
|
from docutils.parsers import rst
|
|
import icalendar
|
|
from sphinx.util import logging
|
|
import yaml
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
class PendingICS(nodes.Element):
|
|
|
|
def __init__(self, data_source, series_name, data, last_update):
|
|
super(PendingICS, self).__init__()
|
|
self._data_source = data_source
|
|
self._series_name = series_name
|
|
self._data = data
|
|
self.last_update = last_update
|
|
|
|
|
|
class ICS(rst.Directive):
|
|
|
|
option_spec = {
|
|
'source': rst.directives.unchanged,
|
|
'name': rst.directives.unchanged,
|
|
}
|
|
has_content = False
|
|
last_update = ''
|
|
|
|
def _load_data(self, env, data_source):
|
|
rel_filename, filename = env.relfn2path(data_source)
|
|
|
|
# Attempt to get the last update time for our timestamp
|
|
try:
|
|
last_update = subprocess.check_output(
|
|
[
|
|
'git', 'log', '-n1', '--format=%ad',
|
|
'--date=format:%Y-%m-%d %H:%M:%S',
|
|
'--', filename,
|
|
]
|
|
).decode('utf-8').strip()
|
|
self.last_update = datetime.datetime.strptime(
|
|
last_update, '%Y-%m-%d %H:%M:%S')
|
|
LOG.info('[ics] Last update of %s was %s',
|
|
rel_filename, last_update)
|
|
except (subprocess.CalledProcessError, ValueError):
|
|
self.last_update = datetime.datetime.utcnow()
|
|
|
|
if data_source.endswith('.yaml'):
|
|
with open(filename, 'r') as f:
|
|
return yaml.safe_load(f)
|
|
elif data_source.endswith('.json'):
|
|
with open(filename, 'r') as f:
|
|
return json.load(f)
|
|
else:
|
|
raise NotImplementedError('cannot load file type of %s' %
|
|
data_source)
|
|
|
|
def run(self):
|
|
env = self.state.document.settings.env
|
|
|
|
try:
|
|
data_source = self.options['source']
|
|
except KeyError:
|
|
error = self.state_machine.reporter.error(
|
|
'No source set for ics directive',
|
|
nodes.literal_block(self.block_text, self.block_text),
|
|
line=self.lineno)
|
|
return [error]
|
|
|
|
try:
|
|
series_name = self.options['name']
|
|
except KeyError:
|
|
error = self.state_machine.reporter.error(
|
|
'No name set for ics directive',
|
|
nodes.literal_block(self.block_text, self.block_text),
|
|
line=self.lineno)
|
|
return [error]
|
|
|
|
data = self._load_data(env, data_source)
|
|
|
|
node = PendingICS(data_source, series_name, data, self.last_update)
|
|
node.document = self.state.document
|
|
return [node]
|
|
|
|
|
|
_global_calendar = icalendar.Calendar()
|
|
_global_calendar.add('prodid', '-//releases.openstack.org//EN')
|
|
_global_calendar.add('version', '2.0')
|
|
_global_calendar.add('X-WR-CALNAME', 'OpenStack Release Schedule')
|
|
|
|
|
|
def _format_description(node):
|
|
"Given a node, get its text and remove line breaks in paragraphs."
|
|
text = node.astext()
|
|
parts = text.split('\n\n')
|
|
return '\n\n'.join(p.replace('\n', ' ') for p in parts)
|
|
|
|
|
|
def doctree_resolved(app, doctree, docname):
|
|
builder = app.builder
|
|
|
|
for node in doctree.traverse(PendingICS):
|
|
|
|
series_name = node._series_name
|
|
data = node._data
|
|
|
|
LOG.info('building {} calendar'.format(series_name))
|
|
|
|
cal = icalendar.Calendar()
|
|
cal.add('prodid', '-//releases.openstack.org//EN')
|
|
cal.add('X-WR-CALNAME', '{} schedule'.format(series_name))
|
|
|
|
for week in data['cycle']:
|
|
if not week.get('name'):
|
|
continue
|
|
|
|
event = icalendar.Event()
|
|
|
|
summary = []
|
|
for item in week.get('x-project', []):
|
|
try:
|
|
# Look up the cross-reference name to get the
|
|
# section, then get the title from the first child
|
|
# node.
|
|
title = doctree.ids[item].children[0].astext()
|
|
except Exception as e:
|
|
# NOTE(dhellmann): Catching "Exception" is a bit
|
|
# ugly, but given the complexity of the expression
|
|
# above there are a bunch of ways things might
|
|
# fail.
|
|
LOG.info('could not get title for {}: {}'.format(item, e))
|
|
title = item
|
|
summary.append(title)
|
|
if summary:
|
|
summary_text = ' (' + '; '.join(summary) + ')'
|
|
else:
|
|
summary_text = ''
|
|
event.add(
|
|
'summary',
|
|
'{} {}{}'.format(series_name.title(),
|
|
week['name'],
|
|
summary_text),
|
|
)
|
|
|
|
event.add('dtstamp', node.last_update)
|
|
event.add('uid', '%s-%s' % (series_name, week.get('name')))
|
|
|
|
start = datetime.datetime.strptime(week['start'], '%Y-%m-%d')
|
|
event.add('dtstart', icalendar.vDate(start.date()))
|
|
|
|
# NOTE(dhellmann): ical assumes a time of midnight, so in
|
|
# order to have the event span the final day of the week
|
|
# we have to add an extra day.
|
|
raw_end = datetime.datetime.strptime(week['end'], '%Y-%m-%d')
|
|
end = raw_end + datetime.timedelta(days=1)
|
|
event.add('dtend', icalendar.vDate(end.date()))
|
|
|
|
# Look up the cross-reference name to get the
|
|
# section, then add the full description to the
|
|
# text.
|
|
description = [
|
|
_format_description(doctree.ids[item])
|
|
for item in week.get('x-project', [])
|
|
if item in doctree.ids
|
|
]
|
|
if description:
|
|
event.add('description', '\n\n'.join(description))
|
|
|
|
cal.add_component(event)
|
|
_global_calendar.add_component(event)
|
|
|
|
output_full_name = os.path.join(
|
|
builder.outdir,
|
|
docname + '.ics',
|
|
)
|
|
output_dir_name = os.path.dirname(output_full_name)
|
|
if not os.path.exists(output_dir_name):
|
|
os.makedirs(output_dir_name)
|
|
destination = FileOutput(
|
|
destination_path=output_full_name,
|
|
encoding='utf-8',
|
|
)
|
|
LOG.info('generating {}'.format(output_full_name))
|
|
destination.write(cal.to_ical())
|
|
|
|
# Remove the node that the writer won't understand.
|
|
node.parent.replace(node, [])
|
|
|
|
|
|
def build_finished(app, exception):
|
|
if exception is not None:
|
|
return
|
|
builder = app.builder
|
|
output_full_name = os.path.join(
|
|
builder.outdir,
|
|
'schedule.ics',
|
|
)
|
|
output_dir_name = os.path.dirname(output_full_name)
|
|
if not os.path.exists(output_dir_name):
|
|
os.makedirs(output_dir_name)
|
|
destination = FileOutput(
|
|
destination_path=output_full_name,
|
|
encoding='utf-8',
|
|
)
|
|
LOG.info('generating {}'.format(output_full_name))
|
|
destination.write(_global_calendar.to_ical())
|
|
|
|
|
|
def setup(app):
|
|
LOG.info('initializing ICS extension')
|
|
app.add_directive('ics', ICS)
|
|
app.connect('doctree-resolved', doctree_resolved)
|
|
app.connect('build-finished', build_finished)
|
|
return {
|
|
'parallel_read_safe': True,
|
|
'parallel_write_safe': True,
|
|
}
|