
4 changed files with 170 additions and 2 deletions
@ -0,0 +1,158 @@ |
|||
#!/usr/bin/python |
|||
|
|||
# Copyright (C) 2012, Benjamin Drung <bdrung@debian.org> |
|||
# |
|||
# Permission to use, copy, modify, and/or distribute this software for any |
|||
# purpose with or without fee is hereby granted, provided that the above |
|||
# copyright notice and this permission notice appear in all copies. |
|||
# |
|||
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES |
|||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF |
|||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR |
|||
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES |
|||
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN |
|||
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF |
|||
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
|||
|
|||
"""Validates a given Debian or Ubuntu distro-info CSV file.""" |
|||
|
|||
import csv |
|||
import datetime |
|||
import optparse |
|||
import os |
|||
import sys |
|||
|
|||
_COLUMNS = { |
|||
"debian": ("version", "codename", "series", "created", "release", "eol"), |
|||
"ubuntu": ("version", "codename", "series", "created", "release", "eol", |
|||
"eol-server"), |
|||
} |
|||
_DATES = ("created", "release", "eol", "eol-server") |
|||
_EARLIER_DATES = ( |
|||
("created", "release"), |
|||
("release", "eol"), |
|||
("eol", "eol-server"), |
|||
) |
|||
_STRINGS = { |
|||
"debian": ("codename", "series"), |
|||
"ubuntu": ("version", "codename", "series"), |
|||
} |
|||
|
|||
def convert_date(string): |
|||
"""Convert a date string in ISO 8601 into a datetime object.""" |
|||
if not string: |
|||
date = None |
|||
else: |
|||
parts = [int(x) for x in string.split("-")] |
|||
if len(parts) == 3: |
|||
(year, month, day) = parts |
|||
date = datetime.date(year, month, day) |
|||
else: |
|||
raise ValueError("Date not in ISO 8601 format.") |
|||
return date |
|||
|
|||
def error(filename, line, message, *args): |
|||
"""Prints an error message""" |
|||
print >> sys.stderr, "%s:%i: %s." % (filename, line, message % args) |
|||
|
|||
def validate(filename, distro): |
|||
"""Validates a given CSV file. |
|||
|
|||
Returns True if the given CSV file is valid and otherwise False. |
|||
""" |
|||
failures = 0 |
|||
content = open(filename).readlines() |
|||
# Remove comments |
|||
for line in xrange(len(content)): |
|||
if content[line].startswith("#"): |
|||
content[line] = "\n" |
|||
csvreader = csv.DictReader(content) |
|||
for row in csvreader: |
|||
# Check for missing columns |
|||
for column in _COLUMNS[distro]: |
|||
if not column in row: |
|||
msg = "Column `%s' is missing" |
|||
error(filename, csvreader.line_num, msg, column) |
|||
failures += 1 |
|||
# Check for additinal columns |
|||
for column in row: |
|||
if not column in _COLUMNS[distro]: |
|||
msg = "Additional column `%s' is specified" |
|||
error(filename, csvreader.line_num, msg, column) |
|||
failures += 1 |
|||
# Check required strings columns |
|||
for column in _STRINGS[distro]: |
|||
if column in row and not row[column]: |
|||
msg = "Empty column `%s' specified" |
|||
error(filename, csvreader.line_num, msg, column) |
|||
failures += 1 |
|||
# Check dates |
|||
for column in _DATES: |
|||
if column in row: |
|||
try: |
|||
row[column] = convert_date(row[column]) |
|||
except ValueError: |
|||
msg = "Invalid date `%s' in column `%s'" |
|||
error(filename, csvreader.line_num, msg, row[column], |
|||
column) |
|||
failures += 1 |
|||
row[column] = None |
|||
# Check required date columns |
|||
column = "created" |
|||
if column in row and not row[column]: |
|||
msg = "No date specified in column `%s'" |
|||
error(filename, csvreader.line_num, msg, column) |
|||
failures += 1 |
|||
# Compare dates |
|||
for (date1, date2) in _EARLIER_DATES: |
|||
if date2 in row and row[date2]: |
|||
if date1 in row and row[date1]: |
|||
# date1 needs to be earlier than date2 |
|||
if row[date1] >= row[date2]: |
|||
msg = ("Date %s of column `%s' needs to be later " |
|||
"than %s of column `%s'") |
|||
error(filename, csvreader.line_num, msg, |
|||
row[date2].isoformat(), date2, |
|||
row[date1].isoformat(), date1) |
|||
failures += 1 |
|||
else: |
|||
# date1 needs to be specified if date1 is specified |
|||
msg = ("A date needs to be specified in column `%s' due " |
|||
"to the given date in column `%s'") |
|||
error(filename, csvreader.line_num, msg, date1, date2) |
|||
failures += 1 |
|||
|
|||
return failures == 0 |
|||
|
|||
def main(): |
|||
"""Main function with command line parameter parsing.""" |
|||
script_name = os.path.basename(sys.argv[0]) |
|||
usage = "%s -d|-u csv-file" % (script_name) |
|||
parser = optparse.OptionParser(usage=usage) |
|||
|
|||
parser.add_option("-d", "--debian", dest="debian", action="store_true", |
|||
default=False, help="validate a Debian CSV file") |
|||
parser.add_option("-u", "--ubuntu", dest="ubuntu", action="store_true", |
|||
default=False, help="validate an Ubuntu CSV file") |
|||
|
|||
(options, args) = parser.parse_args() |
|||
|
|||
if len(args) == 0: |
|||
parser.error("No CSV file specified.") |
|||
elif len(args) > 1: |
|||
parser.error("Multiple CSV files specified: %s" % (", ".join(args))) |
|||
|
|||
if len([x for x in [options.debian, options.ubuntu] if x]) != 1: |
|||
parser.error("You have to select exactly one of --debian, --ubuntu.") |
|||
if options.debian: |
|||
distro = "debian" |
|||
else: |
|||
distro = "ubuntu" |
|||
|
|||
if validate(args[0], distro): |
|||
return 0 |
|||
else: |
|||
return 1 |
|||
|
|||
if __name__ == "__main__": |
|||
sys.exit(main()) |
Loading…
Reference in new issue