b8db60eeabe9061747116c56f239a554a3e900f5
[pygrader.git] / bin / pg.py
1 #!/usr/bin/env python3
2 #
3 # Copyright (C) 2012 W. Trevor King <wking@tremily.us>
4 #
5 # This file is part of pygrader.
6 #
7 # pygrader is free software: you can redistribute it and/or modify it under the
8 # terms of the GNU General Public License as published by the Free Software
9 # Foundation, either version 3 of the License, or (at your option) any later
10 # version.
11 #
12 # pygrader is distributed in the hope that it will be useful, but WITHOUT ANY
13 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License along with
17 # pygrader.  If not, see <http://www.gnu.org/licenses/>.
18
19 """Manage grades from the command line
20 """
21
22 import configparser as _configparser
23 from email.mime.text import MIMEText as _MIMEText
24 import email.utils as _email_utils
25 import inspect as _inspect
26 import logging as _logging
27 import logging.handlers as _logging_handlers
28 import os.path as _os_path
29 import sys as _sys
30
31 import pgp_mime as _pgp_mime
32
33 import pygrader as _pygrader
34 from pygrader import __version__
35 from pygrader import LOG as _LOG
36 from pygrader import color as _color
37 from pygrader.email import test_smtp as _test_smtp
38 from pygrader.email import Responder as _Responder
39 from pygrader.mailpipe import mailpipe as _mailpipe
40 from pygrader.storage import initialize as _initialize
41 from pygrader.storage import load_course as _load_course
42 from pygrader.tabulate import tabulate as _tabulate
43 from pygrader.template import assignment_email as _assignment_email
44 from pygrader.template import course_email as _course_email
45 from pygrader.template import student_email as _student_email
46 from pygrader.todo import print_todo as _todo
47
48
49 if __name__ == '__main__':
50     from argparse import ArgumentParser as _ArgumentParser
51
52     parser = _ArgumentParser(description=__doc__)
53     parser.add_argument(
54         '-v', '--version', action='version',
55         version='%(prog)s {}'.format(_pgp_mime.__version__))
56     parser.add_argument(
57         '-d', '--base-dir', dest='basedir', default='.',
58         help='Base directory containing grade data')
59     parser.add_argument(
60         '-e', '--encoding', dest='encoding', default='utf-8',
61         help=('Override the default file encoding selection '
62               '(useful when running from procmail)'))
63     parser.add_argument(
64         '-c', '--color', default=False, action='store_const', const=True,
65         help='Color printed output with ANSI escape sequences')
66     parser.add_argument(
67         '-V', '--verbose', default=0, action='count',
68         help='Increase verbosity')
69     parser.add_argument(
70         '-s', '--syslog', default=False, action='store_const', const=True,
71         help='Log to syslog (rather than stderr)')
72     subparsers = parser.add_subparsers(title='commands')
73
74     smtp_parser = subparsers.add_parser(
75         'smtp', help=_test_smtp.__doc__.splitlines()[0])
76     smtp_parser.set_defaults(func=_test_smtp)
77     smtp_parser.add_argument(
78         '-a', '--author',
79         help='Your address (email author)')
80     smtp_parser.add_argument(
81         '-t', '--target', dest='targets', action='append',
82         help='Address for the email recipient')
83
84     initialize_parser = subparsers.add_parser(
85         'initialize', help=_initialize.__doc__.splitlines()[0])
86     initialize_parser.set_defaults(func=_initialize)
87     initialize_parser.add_argument(
88         '-D', '--dry-run', default=False, action='store_const', const=True,
89         help="Don't actually send emails, create files, etc.")
90
91     tabulate_parser = subparsers.add_parser(
92         'tabulate', help=_tabulate.__doc__.splitlines()[0])
93     tabulate_parser.set_defaults(func=_tabulate)
94     tabulate_parser.add_argument(
95         '-s', '--statistics', default=False, action='store_const', const=True,
96         help='Calculate mean and standard deviation for each assignment')
97
98     email_parser = subparsers.add_parser(
99         'email', help='Send emails containing grade information')
100     email_parser.add_argument(
101         '-D', '--dry-run', default=False, action='store_const', const=True,
102         help="Don't actually send emails, create files, etc.")
103     email_parser.add_argument(
104         '-a', '--author',
105         help='Your name (email author), defaults to course robot')
106     email_parser.add_argument(
107         '--cc', action='append', help='People to carbon copy')
108     email_subparsers = email_parser.add_subparsers(title='type')
109     assignment_parser = email_subparsers.add_parser(
110         'assignment', help=_assignment_email.__doc__.splitlines()[0])
111     assignment_parser.set_defaults(func=_assignment_email)
112     assignment_parser.add_argument(
113         'assignment', help='Name of the target assignment')
114     student_parser = email_subparsers.add_parser(
115         'student', help=_student_email.__doc__.splitlines()[0])
116     student_parser.set_defaults(func=_student_email)
117     student_parser.add_argument(
118         '-o', '--old', default=False, action='store_const', const=True,
119         help='Include already-notified information in emails')
120     student_parser.add_argument(
121         '-s', '--student', dest='student',
122         help='Explicitly select the student to notify (instead of everyone)')
123     course_parser = email_subparsers.add_parser(
124         'course', help=_course_email.__doc__.splitlines()[0])
125     course_parser.set_defaults(func=_course_email)
126     course_parser.add_argument(
127         '-t', '--target', dest='targets', action='append',
128         help='Name, alias, or group for the email recipient(s)')
129
130     mailpipe_parser = subparsers.add_parser(
131         'mailpipe', help=_mailpipe.__doc__.splitlines()[0])
132     mailpipe_parser.set_defaults(func=_mailpipe)
133     mailpipe_parser.add_argument(
134         '-D', '--dry-run', default=False, action='store_const', const=True,
135         help="Don't actually send emails, create files, etc.")
136     mailpipe_parser.add_argument(
137         '-m', '--mailbox', choices=['maildir', 'mbox'],
138         help=('Instead of piping a message in via stdout, you can also read '
139               'directly from a mailbox.  This option specifies the format of '
140               'your target mailbox.'))
141     mailpipe_parser.add_argument(
142         '-i', '--input', dest='input_', metavar='INPUT',
143         help='Path to the mailbox containing messages to be processed')
144     mailpipe_parser.add_argument(
145         '-o', '--output',
146         help=('Path to the mailbox that will recieve successfully processed '
147               'messages.  If not given, successfully processed messages will '
148               'be left in the input mailbox'))
149     mailpipe_parser.add_argument(
150         '-l', '--max-late', default=0, type=float,
151         help=('Grace period in seconds before an incoming assignment is '
152               'actually marked as late'))
153     mailpipe_parser.add_argument(
154         '-r', '--respond', default=False, action='store_const', const=True,
155         help=('Send automatic response emails to acknowledge incoming '
156               'messages.'))
157     mailpipe_parser.add_argument(
158         '-t', '--trust-email-infrastructure',
159         default=False, action='store_const', const=True,
160         help=('Send automatic response emails even if the target has not '
161               'registered a PGP key.'))
162     mailpipe_parser.add_argument(
163         '-c', '--continue-after-invalid-message',
164         default=False, action='store_const', const=True,
165         help=('Send responses to invalid messages and continue processing '
166               'further emails (default is to die with an error message).'))
167
168     todo_parser = subparsers.add_parser(
169         'todo', help=_todo.__doc__.splitlines()[0])
170     todo_parser.set_defaults(func=_todo)
171     todo_parser.add_argument(
172         'source', help='Name of source file/directory')
173     todo_parser.add_argument(
174         'target', help='Name of target file/directory')
175
176
177 #    p.add_option('-t', '--template', default=None)
178
179     args = parser.parse_args()
180
181     if args.verbose:
182         _LOG.setLevel(max(_logging.DEBUG, _LOG.level - 10*args.verbose))
183         _pgp_mime.LOG.setLevel(_LOG.level)
184     if args.syslog:
185         syslog = _logging_handlers.SysLogHandler(address="/dev/log")
186         syslog.setFormatter(_logging.Formatter('%(name)s: %(message)s'))
187         for handler in list(_LOG.handlers):
188             _LOG.removeHandler(handler)
189         _LOG.addHandler(syslog)
190         for handler in list(_pgp_mime.LOG.handlers):
191             _pgp_mime.LOG.removeHandler(handler)
192         _pgp_mime.LOG.addHandler(syslog)
193     _color.USE_COLOR = args.color
194
195     _pygrader.ENCODING = args.encoding
196
197     config = _configparser.ConfigParser()
198     config.read([
199             _os_path.expanduser(_os_path.join('~', '.config', 'smtplib.conf')),
200             ], encoding=_pygrader.ENCODING)
201
202     func_args = _inspect.getargspec(args.func).args
203     kwargs = {}
204
205     if 'basedir' in func_args:
206         kwargs['basedir'] = args.basedir
207
208     if 'course' in func_args:
209         course = _load_course(basedir=args.basedir)
210         active_groups = course.active_groups()
211         kwargs['course'] = course
212         if hasattr(args, 'assignment'):
213             kwargs['assignment'] = course.assignment(name=args.assignment)
214         if hasattr(args, 'cc') and args.cc:
215             kwargs['cc'] = [course.person(name=cc) for cc in args.cc]
216         for attr in ['author', 'student']:
217             if hasattr(args, attr):
218                 name = getattr(args, attr)
219                 if name is None and attr == 'author':
220                     kwargs[attr] = course.robot
221                 else:
222                     kwargs[attr] = course.person(name=name)
223         for attr in ['targets']:
224             if hasattr(args, attr):
225                 people = getattr(args, attr)
226                 if people is None:
227                     people = ['professors']  # for the course email
228                 kwargs[attr] = []
229                 for person in people:
230                     if person in active_groups:
231                         kwargs[attr].extend(course.find_people(group=person))
232                     else:
233                         kwargs[attr].extend(course.find_people(name=person))
234         for attr in ['dry_run', 'mailbox', 'output', 'input_', 'max_late',
235                      'old', 'statistics', 'trust_email_infrastructure',
236                      'continue_after_invalid_message']:
237             if hasattr(args, attr):
238                 kwargs[attr] = getattr(args, attr)
239     elif args.func == _test_smtp:
240         for attr in ['author', 'targets']:
241             if hasattr(args, attr):
242                 kwargs[attr] = getattr(args, attr)
243     elif args.func == _todo:
244         for attr in ['source', 'target']:
245             if hasattr(args, attr):
246                 kwargs[attr] = getattr(args, attr)
247
248     if 'use_color' in func_args:
249         kwargs['use_color'] = args.color
250
251     if ('smtp' in func_args and
252         not kwargs.get('dry_run', False) and
253         'smtp' in config.sections()):
254         params = _pgp_mime.get_smtp_params(config)
255         kwargs['smtp'] = _pgp_mime.get_smtp(*params)
256         del params
257
258     if hasattr(args, 'respond') and getattr(args, 'respond'):
259         kwargs['respond'] = _Responder(
260             smtp=kwargs.get('smtp', None),
261             dry_run=kwargs.get('dry_run', False))
262
263     _LOG.debug('execute {} with {}'.format(args.func, kwargs))
264     try:
265         ret = args.func(**kwargs)
266     finally:
267         smtp = kwargs.get('smtp', None)
268         if smtp:
269             _LOG.info('disconnect from SMTP server')
270             smtp.quit()
271     if ret is None:
272         ret = 0
273     _sys.exit(ret)