191 lines
6.1 KiB
Python
Executable File
191 lines
6.1 KiB
Python
Executable File
#!/usr/bin/python3
|
|
|
|
"""
|
|
Create a draft of a new issue.
|
|
"""
|
|
|
|
import argparse
|
|
import datetime
|
|
import logging
|
|
import os
|
|
import string
|
|
import sys
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
LOG.setLevel(logging.INFO)
|
|
|
|
DRAFT_PATH = 'draft'
|
|
TEMPLATE_PATH = os.path.join('draft', 'DRAFT_TEMPLATE')
|
|
PREVIOUS_ISSUE_SEARCH_PATHS = ['draft', 'content']
|
|
EVENTS_LIST_PLACEHOLDER = '<!-- Events list goes here -->'
|
|
|
|
|
|
def path_from_root(path):
|
|
""" Returns a path computed relative to the repository root.
|
|
|
|
This is determined by computing the path to this script, then
|
|
traversing up one directory and appending the `path` argument.
|
|
"""
|
|
|
|
self_path = os.path.abspath(__file__)
|
|
self_dir = os.path.dirname(self_path)
|
|
root_path, _ = os.path.split(self_dir)
|
|
return os.path.join(root_path, path)
|
|
|
|
|
|
def get_template_path():
|
|
""" Returns the path to the template file. """
|
|
return path_from_root(TEMPLATE_PATH)
|
|
|
|
|
|
def default_draft_path():
|
|
""" Returns the default path where the draft issue should be written. """
|
|
return path_from_root(DRAFT_PATH)
|
|
|
|
|
|
def issue_filename(date):
|
|
""" Compute the issue filename, given the date. """
|
|
return date.isoformat() + '-this-week-in-rust.md'
|
|
|
|
|
|
def default_date():
|
|
""" Returns a datetime.date, that is the estimated next issue date. """
|
|
# If no input date is specified, we assume that this will be run ~7 days
|
|
# before the next issue is released. Just in case an issue comes out early
|
|
# or late, we'll pick the Wednesday that is between 4 and 10 days in the
|
|
# future.
|
|
starting_day = datetime.date.today() + datetime.timedelta(4)
|
|
# Compute the number of days until the next Wednesday.
|
|
# weekday() returns 0(Monday)..6(Sunday). We want 2.
|
|
delta = (2 - starting_day.weekday()) % 7
|
|
date = starting_day + datetime.timedelta(delta)
|
|
return date
|
|
|
|
|
|
def compute_issue_number(date):
|
|
# 2022-01-05 is issue 424; we can calculate number of weeks since then.
|
|
ref_date = datetime.date(2022, 1, 5)
|
|
delta = date - ref_date
|
|
if delta.days % 7 == 0:
|
|
return 424 + int(delta.days / 7)
|
|
raise Exception('failed to compute issue number')
|
|
|
|
|
|
def read_previous_issue(date):
|
|
""" Read the previous issue, and return the file contents. """
|
|
|
|
previous = date - datetime.timedelta(7)
|
|
filename = issue_filename(previous)
|
|
for path in PREVIOUS_ISSUE_SEARCH_PATHS:
|
|
full_path = os.path.join(path_from_root(path), filename)
|
|
try:
|
|
# Read and return the file contents
|
|
return open(full_path).read()
|
|
except Exception as e:
|
|
# Note: this error doesn't necessarily indicate something
|
|
# is wrong, because we're searching multiple paths.
|
|
LOG.debug(f'failed to read {full_path}: {e}')
|
|
pass
|
|
raise Exception(f'failed to read previous issue {filename}')
|
|
|
|
|
|
def read_previous_events(date):
|
|
""" Attempt to copy the events list from the previous issue. """
|
|
|
|
EVENTS_START = 'Rusty Events between'
|
|
EVENTS_END = 'If you are running a Rust event'
|
|
|
|
previous_issue = read_previous_issue(date)
|
|
events_list = ''
|
|
found_events = False
|
|
for line in previous_issue.splitlines():
|
|
if found_events:
|
|
if line.strip().startswith(EVENTS_END):
|
|
# We found the end of the events list.
|
|
# Strip blank lines at the beginning and end.
|
|
return events_list.strip('\n')
|
|
else:
|
|
# Copy this line
|
|
events_list += line + '\n'
|
|
else:
|
|
if line.strip().startswith(EVENTS_START):
|
|
# Start copying lines after this one.
|
|
found_events = True
|
|
|
|
# If we made it here, something went wrong with the extraction.
|
|
# Most likely, the previous issue doesn't have the strings we are
|
|
# searching for (EVENTS_START, EVENTS_END).
|
|
raise Exception(f'failed to extract the previous events list')
|
|
|
|
|
|
def create_draft(date):
|
|
""" Return a new issue draft based on the template file. """
|
|
# Events listing ends on issue_date + 28 days.
|
|
end_date = date + datetime.timedelta(28)
|
|
|
|
try:
|
|
events_list = read_previous_events(date)
|
|
except Exception as e:
|
|
LOG.warning(f'failed to copy previous events: {e}')
|
|
events_list = EVENTS_LIST_PLACEHOLDER
|
|
|
|
params = {
|
|
'twir_issue_number': compute_issue_number(date),
|
|
'twir_issue_date': date.isoformat(),
|
|
'twir_events_end_date': end_date.isoformat(),
|
|
'twir_events_list': events_list,
|
|
}
|
|
|
|
template_path = get_template_path()
|
|
template = open(template_path).read()
|
|
template = string.Template(template)
|
|
return template.substitute(params)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument('--date', default=None, metavar='YYYY-MM-DD',
|
|
help='draft date (defaults to Wednesday ~7 days away)')
|
|
parser.add_argument('--draft-path', default=None, metavar='DIR',
|
|
help='directory to write the new draft to')
|
|
parser.add_argument('--dry-run', action='store_true',
|
|
help="don't write the file; print it to stdout")
|
|
parser.add_argument('--debug', action='store_true',
|
|
help='be more verbose')
|
|
args = parser.parse_args()
|
|
if args.debug:
|
|
LOG.setLevel(logging.DEBUG)
|
|
LOG.debug(f'command-line arguments: {args}')
|
|
|
|
# Compute the issue date.
|
|
if args.date:
|
|
date = datetime.date.fromisoformat(args.date)
|
|
else:
|
|
date = default_date()
|
|
LOG.debug(f'issue date {date}')
|
|
|
|
# Create the draft filename: draft/YYYY-MM-DD-this-week-in-rust.md
|
|
if args.draft_path:
|
|
draft_path = args.draft_path
|
|
else:
|
|
draft_path = default_draft_path()
|
|
filename = os.path.join(draft_path, issue_filename(date))
|
|
|
|
# Create the draft text, and either write it to a file, or to stdout.
|
|
draft = create_draft(date)
|
|
if args.dry_run:
|
|
print(draft)
|
|
else:
|
|
open(filename, 'x').write(draft)
|
|
|
|
|
|
def setup_logging():
|
|
log_stdout = logging.StreamHandler(sys.stdout)
|
|
logging.getLogger('').addHandler(log_stdout)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
setup_logging()
|
|
main()
|