Check-in [6f59ae2981]
Not logged in

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Merged from dev to rel in preparation for releasing version 0.2.0.
Timelines: family | ancestors | descendants | both | rel
Files: files | file ages | folders
SHA1:6f59ae298129a93d3f962ce84b01c56f0b60532c
User & Date: mvnathan 2014-09-20 23:40:35
Context
2014-09-20
23:43
Merged last-minute change from dev to rel. check-in: 556d092628 user: mvnathan tags: rel
23:40
Merged from dev to rel in preparation for releasing version 0.2.0. check-in: 6f59ae2981 user: mvnathan tags: rel
23:27
Looks like we're ready to release version 0.2.0... check-in: 24981c6c57 user: mvnathan tags: dev
2014-09-05
01:10
Releasing version 0.1.0 check-in: 7beb4a5e75 user: mvnathan tags: rel, v0.1.0
Changes

Changes to py/morg.py.

1
2
3
4
5
6
7
8
9
10
11
..
85
86
87
88
89
90
91



92
93
94
95
96
97
98
99
100
101
102
#!/usr/bin/env python
#
# @file  morg.py
# @brief Morg TODO list manager's main function and related code.
#

#
# Copyright (C) 2014 The Morg Project Developers
#
# See wiki/copyright.wiki in the top-level directory of the Morg source
# distribution for the full list of authors who have contributed to this
................................................................................
def main():
    try:
        args = morglib.args.parse(sys.argv[1:], str(version()))
        morglib.log.init(args.debug_level, args.log_file)
        logger.info('starting Morg')
        morglib.args.dump(args)




        if (args.command):
            morglib.interpreter.execute(args.command[0], args.command)
        else:
            morglib.command.repl(morglib.interpreter)

    except morglib.args.args_error, e:
        sys.stderr.write('{}: {}\n'.
                         format(os.path.basename(sys.argv[0]), e))
        sys.exit(2)

    except morglib.error, e:



|







 







>
>
>

|

|







1
2
3
4
5
6
7
8
9
10
11
..
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
#!/usr/bin/env python
#
# @file  morg.py
# @brief Morg list manager's main function and related code.
#

#
# Copyright (C) 2014 The Morg Project Developers
#
# See wiki/copyright.wiki in the top-level directory of the Morg source
# distribution for the full list of authors who have contributed to this
................................................................................
def main():
    try:
        args = morglib.args.parse(sys.argv[1:], str(version()))
        morglib.log.init(args.debug_level, args.log_file)
        logger.info('starting Morg')
        morglib.args.dump(args)

        logger.info('opening database {}'.format(args.database))
        db = morglib.db(args.database, morglib.properties.defaults)

        if (args.command):
            morglib.interpreter.execute(args.command[0], args.command, db)
        else:
            morglib.command.repl(morglib.interpreter, db)

    except morglib.args.args_error, e:
        sys.stderr.write('{}: {}\n'.
                         format(os.path.basename(sys.argv[0]), e))
        sys.exit(2)

    except morglib.error, e:

Changes to py/morglib/__init__.py.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
..
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55



56
57
58
59





60
61
62
63
64
65
66
#
# @file  __init__.py
# @brief Initialization for morglib module.
#
# Much of Morg's functionality is encapsulated within the morglib
# package. Thus, clients can use Morg as a standalone application or
# build a custom task management application using its library.
#

#
# Copyright (C) 2014 The Morg Project Developers
#
# See wiki/copyright.wiki in the top-level directory of the Morg source
# distribution for the full list of authors who have contributed to this
................................................................................
import pkgutil
import inspect
import importlib
import os
import sys

# Pull in various morglib modules so clients don't have to import them
# explicitly and to facilitate automatic initialization of Morg
# interpreter's dispatch table.
for _, modname, __ in pkgutil.iter_modules([os.path.dirname(__file__)]):
    full_name = '{}.{}'.format(__name__, modname)
    if (full_name not in sys.modules):
        importlib.import_module(full_name)

# Pull in exception base class and alias it so clients can use it simply
# as morglib.error instead of the fully qualified and hideously verbose
# form morglib.morg_error.error_base, which is quite a handful.
from morg_error import error_base as error




#------------------------------- EXPORTS --------------------------------

__all__ = ['args', 'command', 'log', 'morg_error']






#------------------------- COMMAND INTERPRETER --------------------------

# Helper to check if a given module belongs to morglib or not
class is_morglib_module:
    def __init__(self, morglib_module_name):
        self._module_name = morglib_module_name






|







 







|










>
>
>



|
>
>
>
>
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
..
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#
# @file  __init__.py
# @brief Initialization for morglib module.
#
# Much of Morg's functionality is encapsulated within the morglib
# package. Thus, clients can use Morg as a standalone application or
# build a custom list management application using its library.
#

#
# Copyright (C) 2014 The Morg Project Developers
#
# See wiki/copyright.wiki in the top-level directory of the Morg source
# distribution for the full list of authors who have contributed to this
................................................................................
import pkgutil
import inspect
import importlib
import os
import sys

# Pull in various morglib modules so clients don't have to import them
# explicitly and also to facilitate automatic initialization of the Morg
# interpreter's dispatch table.
for _, modname, __ in pkgutil.iter_modules([os.path.dirname(__file__)]):
    full_name = '{}.{}'.format(__name__, modname)
    if (full_name not in sys.modules):
        importlib.import_module(full_name)

# Pull in exception base class and alias it so clients can use it simply
# as morglib.error instead of the fully qualified and hideously verbose
# form morglib.morg_error.error_base, which is quite a handful.
from morg_error import error_base as error

# Expose morglib.database.database simply as morglib.db
from database import database as db

#------------------------------- EXPORTS --------------------------------

__all__ = ['args',
           'command',
           'database',
           'log',
           'morg_error',
           'properties',]

#------------------------- COMMAND INTERPRETER --------------------------

# Helper to check if a given module belongs to morglib or not
class is_morglib_module:
    def __init__(self, morglib_module_name):
        self._module_name = morglib_module_name

Changes to py/morglib/args.py.

57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
..
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
...
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
...
149
150
151
152
153
154
155





















































156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
...
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
...
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
    @param  ver (string) Version string for -v option.
    @return argparse.Namespace object containing parsed options.

    This function parses the command-line arguments and returns an
    argparse.Namespace object with the following keys:

        @li <tt> config_dir  </tt>
        @li <tt> tasks_file  </tt>
        @li <tt> debug_log   </tt>
        @li <tt> debug_level </tt>

    The value of the <tt>config_dir</tt> key is a string specifying the
    directory where the user's Morg configs are stored.

    The <tt>tasks_file</tt> key is a string specifying the SQLite file
    that Morg should use as its tasks database.

    The <tt>debug_log</tt> key is a string pointing to the log file into
    which Morg should output debug messages.

    The <tt>debug_level</tt> key is an integer from the standard Python
    logging module that specifies how much debug info is desired.

................................................................................

    defaults = _init_defaults()
    parser.add_argument('-c', '--config-dir',
                        default = defaults['config_dir'],
                        help = ('directory containing Morg configs ' +
                                '(default: %(default)s)'),
                        metavar = 'DIR')
    parser.add_argument('-f', '--tasks-file',
                        default = defaults['tasks_file'],
                        help = 'tasks database (default: %(default)s)',
                        metavar = 'FILE')
    parser.add_argument('-l', '--log-file',
                        default = defaults['debug_log'],
                        help = 'debug log (default: %(default)s)',
                        metavar = 'LOG')
    parser.add_argument('-d', '--debug-level',
                        type = _validate_debug_level,
................................................................................
    parser.add_argument('command', nargs = argparse.REMAINDER,
                        help = 'Morg command to run',
                        metavar = 'CMD')

    args = parser.parse_args(argv)
    return args

#---------------------------- DEBUG SUPPORT -----------------------------

def dump(args):
    '''Dump command-line args to debug log.

    @param args argparse.Namespace object returned by parse() function.

    Be sure to call this function after logging has been initialized with
    morglib.log.init(). Otherwise, expect trouble.

    '''
    for attr in dir(args):
        if (attr.startswith('_')):
            continue
        logger.debug('{} = {}'.format(attr, getattr(args, attr)))

#--------------------------- PRIVATE HELPERS ----------------------------

class argv_parser(argparse.ArgumentParser):
    '''Argument parser that raises an exception instead of exiting.

    When argparse.ArgumentParser encounters an error, it simply quits
    with an error message. This helper class changes that behaviour so
    that an exception is raised instead, which allows us to customize how
................................................................................
        this arguments parser raises an exception so that Morg's main
        function can react as it sees fit rather than being beholden to
        the argparse module's whims.

        '''
        raise args_error('{}'.format(msg))























































def _init_defaults():
    '''Initialize default values for various command-line options.

    @return Dictionary containing default values.

    This function returns a dictionary of strings to strings with the
    following keys:

        @li <tt> config_dir  </tt>
        @li <tt> tasks_file  </tt>
        @li <tt> debug_log   </tt>
        @li <tt> debug_level </tt>

    First, it initializes the values corresponding to the above keys with
    hard-coded strings. Then, it examines the environment to look for
    definitions of Morg's supported environment variables and replaces
    the hard-coded values with the values of the environment variables.
................................................................................

    '''
    home = os.environ['HOME'] if ('HOME' in os.environ) else '.'
    dot_morg = os.path.join(home, '.morg')

    # Hard-coded defaults
    defaults = {'config_dir' : dot_morg,
                'tasks_file' : os.path.join(dot_morg, 'tasks.sqlite'),
                'debug_log'  : os.path.join(dot_morg, 'debug.log'),
                'debug_level': '2'}

    # Environment variables override hard-coded defaults
    for k in defaults:
        v = 'MORG_{}'.format(k.upper())
        if (v in os.environ):
            defaults[k] = os.environ[v]
    return  defaults


def _help_before_options():
    help_text = '''
Morg is a TODO list manager that helps you keep track of all the things
you need to get done. Morg does not adhere to any particular time
management methodology. Instead, it focuses on implementing a flexible
data model and providing functions to manipulate that model so that you
can adapt it to your particular needs and style.
'''
    return help_text

................................................................................

Some of the above options can also be specified via environment
variables:

        Option        Environment Variable
        ------        --------------------
        -c            MORG_CONFIG_DIR
        -f            MORG_TASKS_FILE
        -l            MORG_DEBUG_LOG
        -d            MORG_DEBUG_LEVEL

Command-line arguments override environment variables, which, in turn,
override the hard-coded defaults.

Usually, after the command-line options, you will provide arguments that
Morg interprets as a command to execute. These would be commands to, for
example, create a new task, search your tasks list, edit a task, and so
on.

Without a command, Morg will drop you into interactive mode, which will
issue a prompt and wait for you to type in commands until you exit.
'''
    return help_text








|






|
|







 







|
|
|







 







<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|







 







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>










|







 







|













|







 







|








|







57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
..
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
...
108
109
110
111
112
113
114
















115
116
117
118
119
120
121
122
...
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
...
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
...
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
    @param  ver (string) Version string for -v option.
    @return argparse.Namespace object containing parsed options.

    This function parses the command-line arguments and returns an
    argparse.Namespace object with the following keys:

        @li <tt> config_dir  </tt>
        @li <tt> database    </tt>
        @li <tt> debug_log   </tt>
        @li <tt> debug_level </tt>

    The value of the <tt>config_dir</tt> key is a string specifying the
    directory where the user's Morg configs are stored.

    The <tt>database</tt> key is a string specifying the SQLite file that
    Morg should use as its items database.

    The <tt>debug_log</tt> key is a string pointing to the log file into
    which Morg should output debug messages.

    The <tt>debug_level</tt> key is an integer from the standard Python
    logging module that specifies how much debug info is desired.

................................................................................

    defaults = _init_defaults()
    parser.add_argument('-c', '--config-dir',
                        default = defaults['config_dir'],
                        help = ('directory containing Morg configs ' +
                                '(default: %(default)s)'),
                        metavar = 'DIR')
    parser.add_argument('-f', '--database',
                        default = defaults['database'],
                        help = 'items database (default: %(default)s)',
                        metavar = 'FILE')
    parser.add_argument('-l', '--log-file',
                        default = defaults['debug_log'],
                        help = 'debug log (default: %(default)s)',
                        metavar = 'LOG')
    parser.add_argument('-d', '--debug-level',
                        type = _validate_debug_level,
................................................................................
    parser.add_argument('command', nargs = argparse.REMAINDER,
                        help = 'Morg command to run',
                        metavar = 'CMD')

    args = parser.parse_args(argv)
    return args

















#--------------------------- HELPER CLASSES -----------------------------

class argv_parser(argparse.ArgumentParser):
    '''Argument parser that raises an exception instead of exiting.

    When argparse.ArgumentParser encounters an error, it simply quits
    with an error message. This helper class changes that behaviour so
    that an exception is raised instead, which allows us to customize how
................................................................................
        this arguments parser raises an exception so that Morg's main
        function can react as it sees fit rather than being beholden to
        the argparse module's whims.

        '''
        raise args_error('{}'.format(msg))

class print_help(argparse.Action):
    '''An action to print help without exiting.

    By default, argparse.ArgumentParser exits after printing help.
    However, when Morg is in interactive mode, we don't want a command's
    --help option to end up quitting the application. This class helps
    accomplish that.

    To use it, a command's implementation should follow the argument
    parsing approach illustrated below:

    @verbatim
        import morglib.args as args

        def parse(argv):
            parser = args.argv_parser(add_help = False)
            parser.add_argument('-h', '--help',
                                action = args.print_help,
                                nargs  = 0, default = False)
            # etc.
    @endverbatim

    The above will disable argparse's built-in help processing and use
    this one instead.

    Then, if --help is given on the command-line, the resulting
    argparse.Namespace object will have an attribute named help, which
    will be set to True. Otherwise, the namespace's help attribute will
    be False. Command objects can use this flag to determine whether to
    proceed to command execution or return.

    '''
    def __call__(self, parser, namespace, values, option_string = None):
        setattr(namespace, self.dest, True)
        parser.print_help()

#---------------------------- DEBUG SUPPORT -----------------------------

def dump(args):
    '''Dump command-line args to debug log.

    @param args argparse.Namespace object returned by parse() function.

    Be sure to call this function after logging has been initialized with
    morglib.log.init(). Otherwise, expect trouble.

    '''
    for attr in dir(args):
        if (attr.startswith('_')):
            continue
        logger.debug('{} = {}'.format(attr, getattr(args, attr)))

#--------------------------- PRIVATE HELPERS ----------------------------

def _init_defaults():
    '''Initialize default values for various command-line options.

    @return Dictionary containing default values.

    This function returns a dictionary of strings to strings with the
    following keys:

        @li <tt> config_dir  </tt>
        @li <tt> database    </tt>
        @li <tt> debug_log   </tt>
        @li <tt> debug_level </tt>

    First, it initializes the values corresponding to the above keys with
    hard-coded strings. Then, it examines the environment to look for
    definitions of Morg's supported environment variables and replaces
    the hard-coded values with the values of the environment variables.
................................................................................

    '''
    home = os.environ['HOME'] if ('HOME' in os.environ) else '.'
    dot_morg = os.path.join(home, '.morg')

    # Hard-coded defaults
    defaults = {'config_dir' : dot_morg,
                'database'   : os.path.join(dot_morg, 'items.sqlite'),
                'debug_log'  : os.path.join(dot_morg, 'debug.log'),
                'debug_level': '2'}

    # Environment variables override hard-coded defaults
    for k in defaults:
        v = 'MORG_{}'.format(k.upper())
        if (v in os.environ):
            defaults[k] = os.environ[v]
    return  defaults


def _help_before_options():
    help_text = '''
Morg is a list manager that helps you keep track of all the things
you need to get done. Morg does not adhere to any particular time
management methodology. Instead, it focuses on implementing a flexible
data model and providing functions to manipulate that model so that you
can adapt it to your particular needs and style.
'''
    return help_text

................................................................................

Some of the above options can also be specified via environment
variables:

        Option        Environment Variable
        ------        --------------------
        -c            MORG_CONFIG_DIR
        -f            MORG_DATABASE
        -l            MORG_DEBUG_LOG
        -d            MORG_DEBUG_LEVEL

Command-line arguments override environment variables, which, in turn,
override the hard-coded defaults.

Usually, after the command-line options, you will provide arguments that
Morg interprets as a command to execute. These would be commands to, for
example, create a new item, search your items list, edit an item, and so
on.

Without a command, Morg will drop you into interactive mode, which will
issue a prompt and wait for you to type in commands until you exit.
'''
    return help_text

Changes to py/morglib/command.py.

27
28
29
30
31
32
33

34
35
36
37
38
39
40
..
56
57
58
59
60
61
62




63
64
65
66
67
68
69
70

71
72
73
74
75
76
77
...
138
139
140
141
142
143
144
145
146
147
148
149

150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177

178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198

199
200
201
202
203
204





205
206
207
208

209
210
211
212
213
214
215
...
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
# You should have received a copy of the GNU General Public License
# along with Morg. If not, see <http://www.gnu.org/licenses/>.
#

#------------------------------- IMPORTS --------------------------------

# Morg

import morg_error

# Standard library
import cmd
import readline
import shlex
import logging
................................................................................

    All Morg commands should derive from this class and implement the
    <tt>__call__</tt> method, which will be passed a list of strings
    containing the command-line used to invoke that command. The first
    member of this list will be the command name itself; the remaining
    elements are the command-line arguments.





    To automate help processing, each command should be able to handle a
    <tt>--help</tt> option.

    '''
    def __call__(self, argv):
        '''Execute command.

        @param argv (string list) Arguments provided via command-line.


        The call method acts as the means to execute commands. In this
        base class, we simply raise an exception to indicate that a
        subclass has failed to implement the desired functionality.

        '''
        raise unimplemented_cmd(argv[0])
................................................................................
        '''Return an iterator over the keys.'''
        return self._dispatch_table.iterkeys()

    def iteritems(self):
        '''Return an iterator over the key-value pairs.'''
        return self._dispatch_table.iteritems()

    def execute(self, key, args):
        '''Execute specified command.

        @param key (string) Command name.
        @param args (string list) Command-line arguments.


        '''
        try:
            logger.debug('executing command: {}'.format(args))
            f = self[key]
            f(args)
        except KeyError:
            logger.debug('unable to find command {}'.format(key))
            potential_completions = filter(lambda k: k.startswith(key),
                                           self._dispatch_table)
            n = len(potential_completions)
            logger.debug('found {} potential completions'.format(n))
            if (n <= 0):
                raise unknown_cmd(key)
            elif (n == 1):
                k = potential_completions[0]
                f = self[k]
                logger.debug('{} is a unique prefix for {}'.format(key, k))
                args = [k] + args[1:]
                f(args)
            else:
                raise ambiguous_prefix(key, potential_completions)

class unknown_cmd(morg_error.error_base):
    '''For reporting unknown commands.'''
    def __str__(self):
        return '{}: no such command'.format(self.message)


class ambiguous_prefix(morg_error.error_base):
    '''For reporting ambiguous command prefixes.'''
    def __init__(self, prefix, completions):
        msg = ('ambiguous prefix: {}; can complete: {}'.
               format(prefix, ', '.join(completions)))
        morg_error.error_base.__init__(self, msg)

#-------------------------------- REPL ----------------------------------

class repl(cmd.Cmd):
    '''Interactive mode's main loop.

    This class implements a read-eval-print loop (i.e., a REPL) so that
    users can interact with Morg beyond the "one-liner" permitted by the
    invocation via command-line arguments.

    '''
    def __init__(self, interpreter):
        '''Construct repl object.

        @param interpreter (dispatcher) For executing Morg commands.


        When instantiating the <tt>repl</tt> class, you should supply it
        with the <tt>morglib.interpreter</tt> so that this object knows
        how to interpret and execute the commands it receives
        interactively.






        '''
        cmd.Cmd.__init__(self)
        self.prompt = 'morg> '
        self._interpreter = interpreter


        # Helper function object to call the _dispatch_cmd() method,
        # passing it the command name plus the input line.
        class _dispatch:
            def __init__(self, cmd_name, repl_obj):
                self._cmd  = cmd_name
                self._repl = repl_obj
................................................................................
        simply splits the input line using shell-like syntax and then
        uses the morglib interpreter to execute the command.

        '''
        try:
            args = shlex.split(line)
            if (args):
                self._interpreter.execute(args[0], args)

        except morg_error.error_base, e:
            logger.error(e)
            sys.stderr.write('{}\n'.format(e))

    def default(self, line):
        '''How to deal with unrecognized input.







>







 







>
>
>
>




|



>







 







|




>





|
|
|
|
|
|
|
|
|
|
|
|
|
<
<
<
<



|
|
<
>
|
<
<
<
<












|



>






>
>
>
>
>




>







 







|







27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
..
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
...
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174




175
176
177
178
179

180
181




182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
...
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
# You should have received a copy of the GNU General Public License
# along with Morg. If not, see <http://www.gnu.org/licenses/>.
#

#------------------------------- IMPORTS --------------------------------

# Morg
import morg_util
import morg_error

# Standard library
import cmd
import readline
import shlex
import logging
................................................................................

    All Morg commands should derive from this class and implement the
    <tt>__call__</tt> method, which will be passed a list of strings
    containing the command-line used to invoke that command. The first
    member of this list will be the command name itself; the remaining
    elements are the command-line arguments.

    In addition to the command-line arguments, each command's
    <tt>__call__</tt> method will receive a database object, which it can
    use to search and/or manipulate the Morg database as required.

    To automate help processing, each command should be able to handle a
    <tt>--help</tt> option.

    '''
    def __call__(self, argv, db):
        '''Execute command.

        @param argv (string list) Arguments provided via command-line.
        @param db (morglib.database) Interface object for Morg's database.

        The call method acts as the means to execute commands. In this
        base class, we simply raise an exception to indicate that a
        subclass has failed to implement the desired functionality.

        '''
        raise unimplemented_cmd(argv[0])
................................................................................
        '''Return an iterator over the keys.'''
        return self._dispatch_table.iterkeys()

    def iteritems(self):
        '''Return an iterator over the key-value pairs.'''
        return self._dispatch_table.iteritems()

    def execute(self, key, args, db):
        '''Execute specified command.

        @param key (string) Command name.
        @param args (string list) Command-line arguments.
        @param db (morglib.database) Morg's database interface object.

        '''
        try:
            logger.debug('executing command: {}'.format(args))
            f = self[key]
            f(args, db)
        except KeyError:
            try:
                logger.debug('unable to find command {}; trying completions'.
                             format(key))
                k = morg_util.complete(key, self._dispatch_table)
                logger.debug('{} expands to {}'.format(key, k))
                f = self[k]
                args =  [k] + args[1:]
                f(args, db)
            except morg_util. completions_error, e:
                logger.debug('completions failed: {}'.format(e))
                raise unknown_cmd(key, e)





class unknown_cmd(morg_error.error_base):
    '''For reporting unknown commands.'''
    def __init__(self, kmd, reason = None):
        msg = '{}: no such command'.format(kmd)

        if (reason):
            msg = '{} ({})'.format(msg, reason)




        morg_error.error_base.__init__(self, msg)

#-------------------------------- REPL ----------------------------------

class repl(cmd.Cmd):
    '''Interactive mode's main loop.

    This class implements a read-eval-print loop (i.e., a REPL) so that
    users can interact with Morg beyond the "one-liner" permitted by the
    invocation via command-line arguments.

    '''
    def __init__(self, interpreter, db):
        '''Construct repl object.

        @param interpreter (dispatcher) For executing Morg commands.
        @param db (morglib.database) To pass to command objects.

        When instantiating the <tt>repl</tt> class, you should supply it
        with the <tt>morglib.interpreter</tt> so that this object knows
        how to interpret and execute the commands it receives
        interactively.

        Additionally, all Morg command objects have to be passed the
        database object so they can operate on it as required. Thus, you
        should also pass Morg's database interface object to this
        constructor.

        '''
        cmd.Cmd.__init__(self)
        self.prompt = 'morg> '
        self._interpreter = interpreter
        self._db = db

        # Helper function object to call the _dispatch_cmd() method,
        # passing it the command name plus the input line.
        class _dispatch:
            def __init__(self, cmd_name, repl_obj):
                self._cmd  = cmd_name
                self._repl = repl_obj
................................................................................
        simply splits the input line using shell-like syntax and then
        uses the morglib interpreter to execute the command.

        '''
        try:
            args = shlex.split(line)
            if (args):
                self._interpreter.execute(args[0], args, self._db)

        except morg_error.error_base, e:
            logger.error(e)
            sys.stderr.write('{}\n'.format(e))

    def default(self, line):
        '''How to deal with unrecognized input.

Added py/morglib/database.py.































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
#
# @file  database.py
# @brief Encapsulation of Morg's items database.
#

#
# Copyright (C) 2014 The Morg Project Developers
#
# See wiki/copyright.wiki in the top-level directory of the Morg source
# distribution for the full list of authors who have contributed to this
# project.
#

#
# This file is part of Morg.
#
# Morg is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your
# option) any later version.
#
# Morg is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# for more details.
#
# You should have received a copy of the GNU General Public License
# along with Morg. If not, see <http://www.gnu.org/licenses/>.
#

#------------------------------- IMPORTS --------------------------------

# morglib
import morg_util
import morg_error

# SQLite wrapper
import apsw

# Standard library
import socket
import hashlib
import getpass
import logging
import time
import os

#---------------------------- MODULE LOGGER -----------------------------

logger = logging.getLogger(__name__)

#----------------------------- EXCEPTIONS -------------------------------

class database_error(morg_error.error_base):
    '''For reporting various database errors.'''

    def __init__(self, msg, reason = None):
        '''Construct database error.

        @param msg (string) Message describing the error.
        @param reason (any) Additional information about the error.

        Typically, reason will be an APSW exception object indicating
        exactly what went wrong. However, this parameter can be any
        object; this method will convert it to a string. Additionally,
        reason is optional (defaults to None); if not supplied, the
        <tt>database_error</tt> will not contain any further details
        about the problem.

        '''
        if (reason):
            msg = '{} (reason: {})'.format(msg, str(reason))
        morg_error.error_base.__init__(self, msg)

class connection_error(database_error):
    '''For reporting inability to open Morg's SQLite database.'''

    def __init__(self, sqlite_file, reason = None):
        '''Construct connection error.

        @param sqlite_file (string) Name of Morg's SQLite database file.
        @param reason (any) Additional information about connection failure.

        Typically, reason will be an exception object indicating why we
        were unable to connect to the SQLite database. However, this
        parameter can be any object; this method will convert it to a
        string. Additionally, reason is optional (defaults to None); if
        not supplied, the <tt>connection_error</tt> will not contain any
        further details about why Morg was unable to open the SQLite
        file.

        '''
        msg = 'unable to open {}'.format(sqlite_file)
        database_error.__init__(self, msg, reason)

class sanity_check_error(database_error):
    '''For reporting databases that Morg cannot use.'''

    def __init__(self, sqlite_file, reason = None):
        '''Construct sanity check error.

        @param sqlite_file (string) Name of Morg's SQLite database file.
        @param reason (any) Additional information about connection failure.

        Typically, reason will be a string explaining the problem with
        the SQLite file. However, this parameter can be any object and
        will, via the base class constructor, be converted to a string.

        '''
        msg = 'cannot use database {}'.format(sqlite_file)
        database_error.__init__(self, msg, reason)

class sql_error(database_error):
    '''For reporting inability to execute SQL queries.'''

    def __init__(self, query, reason = None):
        '''Construct SQL error.

        @param query (string) The SQL query that could not be executed.
        @param reason (any) Additional information about SQL failure.

        Typically, reason will be an exception object indicating why we
        were unable to execute the SQL query. However, this parameter can
        be any object (it will be converted to a string). Additionally,
        reason is optional (defaults to None); if not supplied, the
        <tt>sql_error</tt> will not contain any further details about why
        Morg was unable to execute the query.

        '''
        msg = 'unable to execute SQL query "{}"'.format(query)
        database_error.__init__(self, msg, reason)

class property_error(database_error):
    '''For reporting errors related to item properties.'''

    def __init__(self, n, t, r = None, e = None):
        '''Construct a property error.

        @param n (string) Property name.
        @param t (string) Property type.
        @param r (string) Client-supplied range specification (if any).
        @param e (any) Additional information (if any) about error.

        If supplied, e will usually be a string or exception further
        describing the problem.

        '''
        msg = ('property error: name = "{}", type = {}, range = {}'.
               format(n, t, r))
        database_error.__init__(self, msg, e)

#--------------------------- ITEMS DATABASE -----------------------------

class database:
    '''Encapsulation of interface to Morg's database of items.

    This class provides clients a high-level API for dealing with the
    Morg database. With it, you can add and remove items and properties
    and even execute arbitrary SQL queries.

    The Morg application instantiates this class early on and then passes
    that object to all <tt>morglib.command</tt> instances to allow them
    to easily implement their respective functionalities by manipulating
    the Morg database as required.

    '''

    # Substantial changes to the structure of the Morg database should be
    # accompanied by increments to the following version number.
    SCHEMA_VERSION = 1

    # Hard-coded string to help identify whether an SQLite file is a Morg
    # database or not.
    MAGIC_PHRASE = ('This is a Morg database.\n' +
                    'Alter it manually at your own peril.')

    def __init__(self, sqlite_file, properties = None):
        '''Connect to Morg database.

        @param sqlite_file (string) Name of SQLite file containing items.
        @param properties (dict) Initial set of item properties.

        This method opens the SQLite file passed to it. If the file
        doesn't already exist, it will be created (which could involve
        creating the entire directory hierarchy specified in the
        <tt>sqlite_file</tt> parameter).

        Failure to open the SQLite file will result in a
        <tt>connection_error</tt> exception.

        Once the database has been successfully opened, the constructor
        will proceed to check if the database is empty or not. In an
        empty database, Morg will create a watermark table so that it can
        subsequently identify that SQLite file as one that it can use.

        Failure to create the watermark table will lead to a
        <tt>sql_error</tt>.

        If the database is not empty, Morg will perform a sanity check
        using the watermark table to see if the database to which it is
        connected was its creation and, therefore, usable by it. If that
        is not the case, the constructor will raise a
        <tt>sanity_check_error</tt>.

        Refer to the documentation of the <tt>watermark</tt> class for
        further information about Morg's simple watermarking system.

        After the watermark creation or verification phase, the
        constructor proceeds to creating the basic database structure
        required by Morg. At the very least, Morg needs an <tt>item</tt>
        table to record all your items as well as several property tables
        that define and record the various properties associated with
        items. Preexisting item and property tables will be left
        untouched.

        However, the database constructor allows you to define new
        properties during initialization. These initial properties are
        specified via the properties parameter, which is a dict that maps
        strings to 2-tuples. The dict keys are the names of properties.
        The 2-tuple corresponding to each key, i.e., property name,
        should contain the property type and its range.

        The property type can be one of the following (case-insensitive)
        strings:

        @li <tt> integer </tt>
        @li <tt> real    </tt>
        @li <tt> text    </tt>

        The property range may be either <tt>None</tt> or a string
        describing the range of values that particular property may take
        on. Commas can be used to separate elements in a list of discrete
        values. Double dots can be used for numeric ranges. Note that
        even if a particular property does not have any restrictions on
        its range, its 2-tuple in the properties dict cannot omit the
        <tt>None</tt> for the second element; i.e., each key's value
        <b>must</b> be a 2-tuple.

        Here are a few illustrative examples:

        @verbatim
            props = {'role'       : ('text',    'President,Glutton,Buffoon'),
                     'priority'   : ('integer', '1,2,3,5,7'),
                     'importance' : ('real',    '-1..+1'),
                     'description': ('text',    None)}
        @endverbatim

        In the above, an item may be associated with one of three roles,
        viz., President, Glutton, or Buffoon. Its priority may be one of
        the prime numbers below ten. The item's importance may lie in the
        range [-1, +1]. And, finally, it may sport a textual description
        that can be any string.

        '''
        try:
            logger.info('connecting to database {}'.format(sqlite_file))
            self._db = apsw.Connection(sqlite_file)

        except apsw.Error, e:
            logger.error('first attempt to open {} failed ({})'.
                         format(sqlite_file, e))

            dirs = os.path.dirname(sqlite_file)
            logger.debug('creating directory hierarchy {}'.format(dirs))
            try:
                os.makedirs(dirs)
            except os .error, e:
                logger.error('unable to create directory hierarchy {} ({})'.
                             format(dirs, e))
                raise connection_error(sqlite_file, e)

            try:
                logger.info('second attempt to open {}'.format(sqlite_file))
                self._db = apsw.Connection(sqlite_file)
            except apsw.Error, e:
                logger .error('second attempt to open {} also failed ({})'.
                              format  (sqlite_file, e))
                raise connection_error(sqlite_file, e)

        if (self.is_empty()):
            logger.info('{} is empty'.format(sqlite_file))
            self._init_morg_table()
        else:
            logger.info('{} has data'.format(sqlite_file))
            self._sanity_check(sqlite_file)

        self._init_item_table()
        self._init_property_tables(properties)

    # Create and populate watermark table
    def _init_morg_table(self):
        with self._db:
            logger.info('creating morg table')
            wm = watermark()
            wm.dump()
            self.execute(wm.create())

            logger.info('populating morg table')
            columns = wm.columns()
            insert  = ('insert into morg ({}) values ({})'.
                       format(','.join(columns),
                              ','.join('?' * len(columns))))
            self.execute(insert, tuple(wm))

    # Try and confirm we're dealing with a Morg database
    def _sanity_check(self, sqlite_file):
        logger.info('checking morg table')
        try:
            morg = self.execute('select * from morg')
        except sql_error, e:
            raise sanity_check_error(sqlite_file,
                                     'unable to get data from morg table')

        n = len(morg)
        logger.debug('morg table has {} rows'.format(n))
        if (n < 1):
            raise sanity_check_error(sqlite_file, 'morg table has no data')
        if (n > 1):
            raise sanity_check_error(sqlite_file,
                                     'morg table has too much data')

        wm = watermark(morg[0])
        wm.dump()
        if (not wm):
            raise sanity_check_error(sqlite_file, 'magic phrase mismatch')
        logger.info('looks like this is a morg database')

    # Create item table if necessary
    def _init_item_table(self):
        logger.info('creating item table')
        create =  '''create table if not exists item(
                         id integer primary key autoincrement)'''
        self.execute(create)

    # Create and/or update the property tables
    def _init_property_tables(self, properties):
        logger.info('creating property table')
        create =  '''create table if not exists property(
                         id    integer primary key autoincrement,
                         name  text not null collate nocase unique,
                         type  text not null collate nocase
                                   check (type in ('integer', 'real', 'text')),
                         range text)'''
        self.execute(create)

        logger.info('retrieving current list of properties')
        rows = self.execute('select name, type, range from property')
        property_table = {}
        for n, t, r in rows:
            logger.debug('property name: {}, type: {}, range: {}'.
                         format (n, t, r))
            property_table[n] = (t, r)

        if (properties):
            logger.info('adding new properties as required')
            for n, (t, r) in properties.iteritems():
                if (n in property_table):
                    logger.debug('property {} already exists'.format(n))
                    continue
                self.add_property(n, t, r)

    def add_item(self, properties):
        '''Add an item to the Morg database, assigning it the given properties.

        @param properties (dict) The new item's various properties.

        The properties dictionary maps property names (strings) to
        property values, which may be either integers, floats, or
        strings.

        Property names may be abbreviated to the shortest, unique prefix.
        This function will search the master property table for a match
        and substitute the abbreviation with the full name.

        Text properties with a defined range may also have their values
        abbreviated.

        This function performs all its database operations in a
        transaction. Thus, any failures will roll back the Morg database
        to its previous state.

        '''
        if (not properties):
            msg = 'cannot create an item without any properties'
            raise property_error(None, None, None, msg)

        logger.info('adding new item')
        for p, v in properties.iteritems():
            logger.debug('property {} = {}'.format(p, v))

        with self._db:
            self.execute('insert into item default values')
            item_id = self._db.last_insert_rowid()
            logger.debug('inserted item {}'.format(item_id))

            logger.debug('creating table item_{} for new item'.format(item_id))
            create = '''create table if not exists item_{} (
                            property_id integer not null,
                            value_id integer not null)'''.format(item_id)
            self.execute(create)

            logger.debug('recording properties for new item {}'.format(item_id))
            for p, v in properties.iteritems():
                prop = self.find_property(p)
                val  = cast_property_value(v, prop['name'],
                                              prop['type'], prop['range'])

                prop_id = prop['id']
                msg = ('inserting value ({}) for item {} into property_{}'.
                       format(val, item_id, prop_id))
                logger.debug(msg)
                insert = '''insert into property_{} (item_id, value)
                                values (?,?)'''.format(prop_id)
                self.execute(insert, (item_id, val))
                val_id = self._db.last_insert_rowid()
                logger.debug('inserted row {} into property_{}'.
                             format(val_id, prop_id))

                logger.debug('recording property ID {} value ID {} in item_{}'.
                             format(prop_id, val_id, item_id))
                insert = '''insert into item_{} (property_id, value_id)
                                values (?,?)'''.format(item_id)
                self.execute(insert, (prop_id, val_id))

        logger.info('successfully added new item {}'.format(item_id))

    def add_property(self, n, t, r = None):
        '''Add a property and create its table.

        @param n (string) Property name.
        @param t (string) Property type.
        @param r (string) Property range.

        This function inserts the given property name, type, and range
        into Morg's "master" <tt>property</tt> table.

        The name of a property can be pretty much anything you like.
        However, keep in mind that property names are case-insensitive
        and must be unique, non-empty strings. Thus, attempting to
        duplicate a property will end up violating the relevant SQL
        column constraint and result in a <tt>sql_error</tt>. Trying to
        use a zero-length string will result in a
        <tt>property_error</tt>.

        The property type can be one of the following (case-insensitive)
        strings:

        @li <tt> integer </tt>
        @li <tt> real    </tt>
        @li <tt> text    </tt>

        If applicable, you may specify the range of values a property may
        assume. For numeric properties, you may use the syntax
        <tt>min..max</tt> to specify the inclusive range
        <tt>[min, max]</tt>.

        All three property types may also take on discrete values. You
        can specify such lists of values by separating the values with
        commas.

        If we successfully insert the property name, type, and range into
        the <tt>property</tt> table, we will proceed to creating the
        table that will hold all the instances of that property. This
        table is named <tt>property_NNN</tt>, where <tt>NNN</tt> is the
        row ID of the property's entry in the <tt>property</tt> table.

        This function is atomic w.r.t. the following actions:

        @li Insert (n, r, t) into <tt>property</tt> table
        @li Create <tt>property_NNN</tt> table

        Thus, if either of the above actions fails, the database will be
        rolled back to its state prior to the execution of any of the
        above actions.

        '''
        if (len(n) <= 0):
            raise property_error(n, t, r, 'unnamed property')

        with self._db:
            logger.debug('adding new property name: {}, type: {}, range: {}'.
                         format (n, t, r))
            insert  = 'insert into property (name, type, range) values (?,?,?)'
            self.execute(insert,(n, t, r))

            select  = '''select id from property where(
                             name = (?) and type = (?))'''
            i, = self.execute(select, (n, t))[0]
            logger.debug('creating table property_{} for property "{}"'.
                         format(i, n))
            create  = ('''create table if not exists property_{}(
                              id integer primary key autoincrement,
                              item_id integer not null,
                              value {} not null {})'''.
                       format(i, t, range_constraint('value', t, r)))
            self.execute(create)

    def find_property(self, prop_name):
        '''Search property table for a matching property name.

        @param  prop_name (string) Property name to match.
        @return Dict containing matching property.

        This method searches the <tt>property</tt> table for a property
        whose name matches <tt>prop_name</tt>, which can be an
        abbreviation. Failure to find a match, unique or otherwise, will result
        in a <tt>property_error</tt>.

        On success, this method will return a dictionary containing the
        matching row from the <tt>property</tt> table. The keys and value
        types of the returned dictionary are as follows:

        @li <tt> id </tt>: int
        @li <tt> name </tt>: str
        @li <tt> type </tt>: str
        @li <tt> range </tt>: str or None

        '''
        logger.debug('looking for property matching "{}"'.format(prop_name))
        select = ("select * from property where (name like '{}%')".
                  format(prop_name))
        rows = self.execute(select)

        n = len(rows)
        if (n < 1):
            raise property_error(prop_name, None, None, 'no such property')
        if (n > 1):
            msg = ('prefix "{}" can match properties: {}'.
                   format(prop_name,
                          ', '.join(map(lambda r: r[1], rows))))
            raise property_error(prop_name, None, None, msg)

        logger.debug('found property ({}) matching "{}"'.
                     format(', '.join(map(str, rows[0])), prop_name))
        return dict(zip(('id', 'name', 'type', 'range' ), rows[0]))

    def is_empty(self):
        '''Check if database has anything in it.

        @return True if database is empty, False otherwise.

        Once a connection to the Morg SQLite database has been
        established, clients can use this function to see if that
        database actually has any data in it or not.

        This function executes a SQL query to determine whether or not
        the database is empty. If that query fails for some reason or
        returns an unexpected result, this function will raise a
        <tt>sql_error</tt> exception.

        '''
        query = 'select count(*) from sqlite_master where type = "table"'
        rows  = self.execute(query)

        r = len(rows)
        if (r != 1):
            logger.error('query {} returned {} rows'.format(query, r))
            raise sql_error(query, 'query returned {} rows'.format(r))
        c = len(rows[0])
        if (c != 1):
            logger.error('query {} returned {} columns'.format(query, c))
            raise sql_error(query, 'query returned {} columns'.format(c))

        t = rows[0][0]
        logger.debug('database has {} tables'.format(t))
        return t == 0

    def execute(self, query, bindings = None):
        '''Execute SQL query.

        @param  query (string) The SQL query to be executed.
        @param  bindings (tuple or dict) Bindings for query.
        @return List of rows of tuples containing query results.

        This function executes the specified query and returns the
        results. If the query fails for some reason, it will raise a
        sql_error exception.

        @note This is a very thin wrapper around the execute() method of
        APSW's Cursor objects. Refer to the APSW documentation for the
        details of bindings.

        '''
        try:
            logger.debug('executing query: {}'.format(query))
            cursor = self._db.cursor()
            return list(cursor.execute(query, bindings))
        except apsw.Error, e:
            logger.error('failed query: {}; reason: {}'.format(query, e))
            raise sql_error(query, e)

#----------------------- WATERMARK ENCAPSULATION ------------------------

class watermark:
    '''Helper class for Morg's database watermark.

    This class eases creation and verification of the <tt>morg</tt>
    watermark table so that Morg can identify SQLite files as "belonging"
    to it.

    Please note that the watermark system Morg uses is not foolproof. You
    can easily enough insert and populate such a table in an arbitrary
    SQLite file and "trick" Morg into using that file as its database. As
    a matter of fact, this class is designed specifically to facilitate
    such "Morgification" without requiring you to jump through
    unnecessary hoops.

    For example, assuming the <tt>morglib</tt> package is in your Python
    path, here is a quick Python program to generate a SQL script that
    you can then use to "Morgify" an SQLite file:

    @verbatim
        import morglib
        f = open('morgify.sql', 'w')
        f.write(morglib.database.watermark().sql())
    @endverbatim

    Now, you can fire up the SQLite command-line tool on any SQLite
    database and have it execute <tt>morgify.sql</tt> to create the
    watermark table so that Morg can use that file.

    Alternatively, you could do the whole thing from Python as
    illustrated below:

    @verbatim
        import morglib
        import apsw

        db = apsw.Connection('foo.sqlite')
        with db:
            wm  = morglib.database.watermark()
            cur = db.cursor()
            cur.execute(wm.create())
            cur.execute('insert into morg values (?,?,?,?,?)', tuple(wm))
    @endverbatim

    After you run the above program, <tt>foo.sqlite</tt> will have the
    necessary magic to allow Morg to use it as an items database.

    To prevent Morg from using an SQLite file, all you have to do is
    delete the <tt>morg</tt> table from it (or just mangle its contents
    so that the sanity check fails). <tt>morglib</tt>, however, does not
    provide a straightforward means of demorgification of a database. For
    that, you'll have to resort to the SQLite command-line.

    One possible use for Morgification is to be able to share the same
    database between Morg and another application by storing all their
    data in the same SQLite file. As long as the two applications don't
    step on each others' toes (e.g., naming their tables the same), this
    should work just fine.

    Having said that, however, simply because you can do something
    doesn't mean you should.

    @note The purpose of the Morg watermark is only to record some
    metadata and to assure Morg that the SQLite file it is using is most
    likely a valid Morg database. The idea is not to enforce some strict,
    tamperproof barrier that prevents intentional or unintentional
    cross-application data pollution. That may not even be feasible with
    SQLite; after all, you can always fire up the SQLite tool and do
    whatever you like to the data in there.

    '''
    def __init__(self, metadata = None):
        '''Construct a watermark object.

        @param metadata (tuple or dict) Watermark table's data.

        The <tt>morg</tt> table contains exactly one row, which has the
        following columns:

        @li <tt> schema_version </tt>
        @li <tt> timestamp      </tt>
        @li <tt> hostname       </tt>
        @li <tt> username       </tt>
        @li <tt> magic_phrase   </tt>

        The schema version is an integer that helps track the structure
        of the database. It can be used, for example, to upgrade older
        data models to more current ones.

        The time stamp records the creation time (GMT) of the database in
        the format <tt>yyyy-mm-dd HH:MM:SS</tt>.

        The host name records the fully qualified domain name of the
        machine on which this Morg database was created.

        The user name identifies the login name of the user who created
        the database.

        Finally, the magic phrase is the SHA-1 ID of the string produced
        by concatenating the schema version, time stamp, host name, user
        name, and a hard-coded identification string used by Morg.

        If the metadata parameter is not given, this constructor will use
        default values for the above-mentioned fields of the watermark
        table. If it is given in the form of a dict, we expect the keys
        to be the column names stated above and the values to be as
        described above. If given as a tuple, we expect the tuple to
        contain only the values in the order shown above.

        '''
        self._metadata = {}
        self._metadata['schema_version'] = database.SCHEMA_VERSION
        try:
            self._metadata['timestamp' ] = time.strftime('%Y-%m-%d %H:%M:%S',
                                                        time.gmtime())
        except Exception:
            logger.warning('unable to get current time')
            self._metadata['timestamp' ] = '0000-00-00 00:00:00'
        try:
            self._metadata['hostname'  ] = socket.getfqdn(socket.gethostname())
        except Exception:
            logger.warning('unable to get hostname')
            self._metadata['hostname'  ] = 'unknown.host.name'
        try:
            self._metadata['username'  ] = getpass.getuser()
        except Exception:
            logger.warning('unable to get current login name')
            self._metadata['username'  ] = 'anonymous'
        self._metadata['magic_phrase'  ] = self.magic_phrase()

        if (isinstance(metadata, dict)):
            for key in self._metadata  :
                if (key  in  metadata) :
                    self._metadata[key] = metadata[key]

        if (isinstance(metadata,tuple)):
            for key, val in zip(self.columns(), metadata):
                self._metadata[key] = val

    def __iter__(self):
        '''Iterator over watermark metadata (in correct order).'''
        class _iterator_adaptor:
            def __init__(self, wm):
                self._metadata = []
                for key in wm.columns():
                    self._metadata.append((key, wm._metadata[key]))
                self._iter = iter(self._metadata)

            def next(self):
                column, value = self._iter.next()
                return  value

        return _iterator_adaptor(self)

    def __nonzero__(self):
        '''Confirm that stored and computed magic phrases match.'''
        stored   = self._metadata['magic_phrase']
        computed = self.magic_phrase()
        logger.debug('magic phrase: stored = {}, computed = {}'.
                     format(stored, computed))
        return stored == computed

    def columns(self):
        '''Return column names of <tt>morg</tt> table in correct order.'''
        return ('schema_version',
                'timestamp',
                'hostname' ,
                'username' ,
                'magic_phrase')

    def magic_phrase(self):
        '''Encode metadata into an identity string.'''
        metadata = []
        for key in self.columns()[:-1]:
            metadata.append(str(self._metadata[key]))
        metadata.append(database.MAGIC_PHRASE)
        return hashlib.sha1('\n'.join(metadata)).hexdigest()

    def create(self):
        '''Return SQL statement for creating watermark table.'''
        return '''create table if not exists morg(
                      schema_version  integer,
                      timestamp       text,
                      hostname        text,
                      username        text,
                      magic_phrase    text)'''

    def sql(self):
        '''Return SQL statements for creating and populating watermark table.'''
        insert = ('insert into morg (\n        {})\n    values (\n        {})'.
                  format(',\n        '.join(self.columns()),
                         ',\n        '.join(map(lambda v: ("'{}'".format(v)
                                                           if isinstance(v, str)
                                                           else str(v)),
                                                tuple(self)))))
        return '{};\n{};\n'.format(self.create(), insert)

    def dump(self):
        '''Debug support.'''
        for key, val in self._metadata.iteritems():
            logger.debug('{} = {}'.format(key, val))

#------------------------------- HELPERS --------------------------------

def range_constraint(column_name, column_type, range_spec):
    '''Parse range specification and return column constraint clause.

    @param  column_name (string) Name of column that needs a constraint.
    @param  column_type (string) Type of column that needs a constraint.
    @param  range_spec  (string or None) Range spec to be translated to SQL.
    @return String containing SQL constraint clause for column.

    Each item property has a name, a type, and, optionally, a range. Morg
    allows users to specify ranges using double dots to indicate min and
    max values for a numeric property and commas to list discrete values
    for any supported type.

    This function converts the range specification strings described
    above into a valid SQL constraint clause for the named column.

    '''
    if (range_spec is None) or (len(range_spec) <= 0):
        return ''

    column_type = column_type.lower()

    # Let's see if we're dealing with a numeric range
    if ('..' in range_spec):
        if (column_type == 'text'):
            raise property_error(column_name, column_type, range_spec,
                                 "using .. in text column's range spec")
        bounds = range_spec.split('..')
        if (len(bounds) != 2):
            raise property_error(column_name, column_type, range_spec,
                                 "more than one .. in " +
                                 "numeric column's range spec")
        try:
            # Convert bounds from string to int or float
            ctor = int if (column_type == 'integer') else float
            bounds = map(lambda v: ctor(v), bounds)
        except Exception, e:
            raise property_error(column_name, column_type, range_spec, e)
        return ('check ({} between {} and {})'.
                format(column_name, min(bounds), max(bounds)))

    # Assume we're dealing with discrete values
    if (column_type == 'text'):
        fmt = lambda v: "'{}'".format(v) # wrap strings in single quotes
    elif (column_type == 'integer'):
        fmt = lambda v: '{}'.format(int(v)) # convert strings to int
    else:
        fmt = lambda v: '{}'.format(float(v)) # convert strings to float
    try:
        return ('check ({} in ({}))'.
                format(column_name,
                       ','.join(map(fmt, range_spec.split(',')))))
    except Exception, e:
        raise property_error(column_name, column_type, range_spec, e)

def cast_property_value(v, n, t, r):
    '''Cast property value into proper type, possibly completing text prefixes.

    @param  v (string, int, float) The property value to be cast.
    @param  n (string) Property name.
    @param  t (string) Property type.
    @param  r (string or None) Property range.
    @return Property value as string, int, or float.

    This function casts v according to type t. If t is text and it has a
    range specified, v can be a prefix for one of the allowed values. In
    that case, we will search r for an unambiguous prefix that matches v.
    Failure to find a suitable match will result in a
    <tt>property_error</tt> exception.

    Numeric values that cannot be converted into their corresponding
    types will also yield a <tt>property_error</tt> exception.

    '''
    if (t == 'integer'):
        try:
            return int(v)
        except Exception, e:
            msg = ('unable to convert property value {} to int '.format(v) +
                   '(exception: {})'.format(e))
            raise property_error(n, t, r, msg)

    if (t == 'real'):
        try:
            return float(v)
        except Exception, e:
            msg = ('unable to convert property value {} to float '.format(v) +
                   '(exception: {})'.format(e))
            raise property_error(n, t, r, msg)

    # Property type must be text
    if (r is not None):
        try:
            logger.debug('looking for value matching "{}"'.format(v))
            val = morg_util.complete(v, r.split(','))
            logger.debug('found property value "{}" matching "{}"'.
                         format(val, v))
            return val
        except morg_util.completions_error, e:
            raise property_error(n, t, r, e)
    return v

#------------------------------------------------------------------------

##############################################
# Editor config:                             #
##############################################
# Local Variables:                           #
# indent-tabs-mode: nil                      #
# py-indent-offset: 4                        #
# python-indent: 4                           #
# End:                                       #
##############################################
# vim: set expandtab shiftwidth=4 tabstop=4: #
##############################################

Changes to py/morglib/edit.py.

1
2
3
4
5
6
7
8
9
10
..
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#
# @file  edit.py
# @brief Command to edit tasks and properties.
#

#
# Copyright (C) 2014 The Morg Project Developers
#
# See wiki/copyright.wiki in the top-level directory of the Morg source
# distribution for the full list of authors who have contributed to this
................................................................................
#---------------------------- MODULE LOGGER -----------------------------

logger = logging.getLogger(__name__)

#---------------------------- EDIT COMMAND ------------------------------

class edit(command.base):
    '''Command for editing tasks and properties.

    '''
    pass

#------------------------------------------------------------------------

##############################################


|







 







|







1
2
3
4
5
6
7
8
9
10
..
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#
# @file  edit.py
# @brief Command to edit items and properties.
#

#
# Copyright (C) 2014 The Morg Project Developers
#
# See wiki/copyright.wiki in the top-level directory of the Morg source
# distribution for the full list of authors who have contributed to this
................................................................................
#---------------------------- MODULE LOGGER -----------------------------

logger = logging.getLogger(__name__)

#---------------------------- EDIT COMMAND ------------------------------

class edit(command.base):
    '''Command for editing items and properties.

    '''
    pass

#------------------------------------------------------------------------

##############################################

Changes to py/morglib/exit.py.

36
37
38
39
40
41
42
43
44
45
46

47
48
49
50
51
52
53
# Standard library
import sys

#---------------------------- EXIT COMMANDS -----------------------------

class exit(command.base):
    '''Command to exit Morg.'''
    def __call__(self, argv):
        '''Execute exit command.

        @param argv (string list) Exit command-line.


        '''
        if (len(argv) > 1 and argv[1] in ['-h', '--help']):
            print(_help())
        else:
            sys.exit(0)








|



>







36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# Standard library
import sys

#---------------------------- EXIT COMMANDS -----------------------------

class exit(command.base):
    '''Command to exit Morg.'''
    def __call__(self, argv, db):
        '''Execute exit command.

        @param argv (string list) Exit command-line.
        @param db (morglib.database) Items database.

        '''
        if (len(argv) > 1 and argv[1] in ['-h', '--help']):
            print(_help())
        else:
            sys.exit(0)

Changes to py/morglib/help.py.

44
45
46
47
48
49
50
51
52
53
54

55
56
57
58
59
60
61
..
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#---------------------------- HELP COMMAND ------------------------------

class help(command.base):
    '''Command for printing help about other commands.'''
    def __init__(self):
        self._help = None

    def __call__(self, argv):
        '''Execute help command.

        @param argv (string list) Help command-line.


        Without any parameters, prints the list of available commands and
        some general comments.

        With a parameter, executes the specified command, passing it the
        --help option. Thus, all commands should implement --help to be
        able to print something useful when the user asks for help.
................................................................................
            if (kmd.startswith('-')): # ignore any command-line options
                kmd = 'help'
        if (kmd is 'help'):
            if (self._help is None):
                self._help = _cache_help_text()
            print(self._help)
        else:
            morglib.interpreter.execute(kmd, [kmd, '--help'])


def _cache_help_text():
    prolog = 'The following commands are available:'

    commands = []
    for k, c in morglib.interpreter.iteritems():







|



>







 







|







44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
..
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#---------------------------- HELP COMMAND ------------------------------

class help(command.base):
    '''Command for printing help about other commands.'''
    def __init__(self):
        self._help = None

    def __call__(self, argv, db):
        '''Execute help command.

        @param argv (string list) Help command-line.
        @param db (morglib.database) Items database.

        Without any parameters, prints the list of available commands and
        some general comments.

        With a parameter, executes the specified command, passing it the
        --help option. Thus, all commands should implement --help to be
        able to print something useful when the user asks for help.
................................................................................
            if (kmd.startswith('-')): # ignore any command-line options
                kmd = 'help'
        if (kmd is 'help'):
            if (self._help is None):
                self._help = _cache_help_text()
            print(self._help)
        else:
            morglib.interpreter.execute(kmd, [kmd, '--help'], db)


def _cache_help_text():
    prolog = 'The following commands are available:'

    commands = []
    for k, c in morglib.interpreter.iteritems():

Changes to py/morglib/ls.py.

1
2
3
4
5
6
7
8
9
10
..
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#
# @file  ls.py
# @brief Command to list tasks and properties matching search parameters.
#

#
# Copyright (C) 2014 The Morg Project Developers
#
# See wiki/copyright.wiki in the top-level directory of the Morg source
# distribution for the full list of authors who have contributed to this
................................................................................
#---------------------------- MODULE LOGGER -----------------------------

logger = logging.getLogger(__name__)

#--------------------------- SEARCH COMMAND -----------------------------

class ls(command.base):
    '''Command for listing tasks and properties.

    '''
    pass

#------------------------------------------------------------------------

##############################################


|







 







|







1
2
3
4
5
6
7
8
9
10
..
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#
# @file  ls.py
# @brief Command to list items and properties matching search parameters.
#

#
# Copyright (C) 2014 The Morg Project Developers
#
# See wiki/copyright.wiki in the top-level directory of the Morg source
# distribution for the full list of authors who have contributed to this
................................................................................
#---------------------------- MODULE LOGGER -----------------------------

logger = logging.getLogger(__name__)

#--------------------------- SEARCH COMMAND -----------------------------

class ls(command.base):
    '''Command for listing items and properties.

    '''
    pass

#------------------------------------------------------------------------

##############################################

Added py/morglib/morg_util.py.





































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
#
# @file  morg_util.py
# @brief Various helper classes and functions.
#

#
# Copyright (C) 2014 The Morg Project Developers
#
# See wiki/copyright.wiki in the top-level directory of the Morg source
# distribution for the full list of authors who have contributed to this
# project.
#

#
# This file is part of Morg.
#
# Morg is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your
# option) any later version.
#
# Morg is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# for more details.
#
# You should have received a copy of the GNU General Public License
# along with Morg. If not, see <http://www.gnu.org/licenses/>.
#

#------------------------------- IMPORTS --------------------------------

# Morg
import morg_error

#------------------------- PREFIX COMPLETIONS ---------------------------

class completions_error(morg_error.error_base):
    '''For reporting ambiguous prefixes or those that don't match.'''
    pass

class unmatched_prefix(completions_error):
    '''When a prefix doesn't match any of the candidates.'''
    def __init__(self, prefix, candidates):
        msg = ('prefix "{}" does not match any of ({})'.
               format(prefix, ', '.join(candidates)))
        completions_error.__init__(self, msg)

class ambiguous_prefix(completions_error):
    '''When a prefix matches too many candidates.'''
    def __init__(self, prefix, candidates):
        msg = ('ambiguous prefix "{}"; can complete ({})'.
               format(prefix, ', '.join(candidates)))
        completions_error.__init__(self, msg)

def complete(abbrev, candidates):
    '''Expand abbreviation given list of candidates.

    @param  abbrev (str) Prefix to be expanded.
    @param  candidates (str iterable) Sequence of possible prefix completions.
    @return Abbreviation expanded to one of the candidates or exception.

    This function searches the list of candidate strings to see if any of
    them are a unique match for the supplied abbreviation. If so, it will
    return the matching candidate string. If there are no matches, it
    will raise an <tt>unmatched_prefix</tt> exception. If there are too
    many possible completions, it will raise an <tt>ambiguous_prefix</tt>
    exception.

    Here are some examples of how this function works:

    @verbatim
        complete('f', ('foo', 'bar', 'baz')) ==> 'foo'
        complete('x', ('foo', 'bar', 'baz')) ==> unmatched_prefix
        complete('b', ('foo', 'bar', 'baz')) ==> ambiguous_prefix
    @endverbatim

    '''
    completions = filter(lambda s: s.startswith(abbrev), candidates)
    if (len(completions) < 1):
        raise unmatched_prefix(abbrev, candidates)
    if (len(completions) > 1):
        raise ambiguous_prefix(abbrev, completions)
    return completions[0]

#------------------------------------------------------------------------

##############################################
# Editor config:                             #
##############################################
# Local Variables:                           #
# indent-tabs-mode: nil                      #
# py-indent-offset: 4                        #
# python-indent: 4                           #
# End:                                       #
##############################################
# vim: set expandtab shiftwidth=4 tabstop=4: #
##############################################

Changes to py/morglib/new.py.

1
2
3
4
5
6
7
8
9
10
..
28
29
30
31
32
33
34


35
36
37

38
39
40
41
42
43
44
45
46


47



48
49
























































































































































































50
51
52
53
54
55
56
#
# @file  new.py
# @brief Command to create new tasks and properties.
#

#
# Copyright (C) 2014 The Morg Project Developers
#
# See wiki/copyright.wiki in the top-level directory of the Morg source
# distribution for the full list of authors who have contributed to this
................................................................................
# along with Morg. If not, see <http://www.gnu.org/licenses/>.
#

#------------------------------- IMPORTS --------------------------------

# Morg
import command



# Standard library
import logging


#---------------------------- MODULE LOGGER -----------------------------

logger = logging.getLogger(__name__)

#--------------------------- CREATE COMMAND -----------------------------

class new(command.base):
    '''Command for creating new tasks and properties.






    '''
    pass

























































































































































































#------------------------------------------------------------------------

##############################################
# Editor config:                             #
##############################################
# Local Variables:                           #


|







 







>
>



>








|
>
>

>
>
>
|
<
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







1
2
3
4
5
6
7
8
9
10
..
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
#
# @file  new.py
# @brief Command to create new items and properties.
#

#
# Copyright (C) 2014 The Morg Project Developers
#
# See wiki/copyright.wiki in the top-level directory of the Morg source
# distribution for the full list of authors who have contributed to this
................................................................................
# along with Morg. If not, see <http://www.gnu.org/licenses/>.
#

#------------------------------- IMPORTS --------------------------------

# Morg
import command
import args
import morg_util

# Standard library
import logging
import argparse

#---------------------------- MODULE LOGGER -----------------------------

logger = logging.getLogger(__name__)

#--------------------------- CREATE COMMAND -----------------------------

class new(command.base):
    '''Command for creating new items and properties.'''
    def __call__(self, argv, db):
        '''Execute new command.

        @param argv (string list) Command-line arguments.
        @param db (morglib.database) Items database.

        '''

        arg = _parse(argv[1:])
        logger.debug("parsed new command's arguments")
        args.dump(arg)
        if (arg.help):
            return

        if (arg.property is None):
            logger.info('building properties list for new item')
            properties = {}
            for p in arg.properties:
                s = p.split('=')
                if (len(s) < 2):
                    raise args.args_error('property {} has no value'.
                                          format(s[0]))
                if (len(s) > 2):
                    raise args.args_error('property {} has multiple values'.
                                          format(s[0]))
                properties[s[0].strip()] = s[1].strip()
            db.add_item(properties)

        else:
            n, t, r = arg.property, arg.property_type, arg.property_range
            logger.debug('creating new property ({}, {}, {})'.format(n, t, r))
            db.add_property(n, t, r)

#------------------------ COMMAND-LINE PARSING --------------------------

def _parse(argv):
    fmt = argparse.RawDescriptionHelpFormatter
    parser = args.argv_parser(description     = _help_before_options(),
                              epilog          = _help_after__options(),
                              prog            = 'new',
                              add_help        = False,
                              formatter_class = fmt)

    parser.add_argument('-h', '--help',
                        action = args.print_help, nargs = 0, default = False,
                        help = 'print this help')

    parser.add_argument('-p', '--property',
                        help = 'add a property instead of a task',
                        metavar = 'NAME')
    parser.add_argument('-t', '--property-type',
                        default = 'text', type = _validate_property_type,
                        help = 'type of property to add (default: %(default)s)',
                        metavar = 'TYPE')
    parser.add_argument('-r', '--property-range',
                        help = 'range of values property may assume',
                        metavar = 'RANGE')

    parser.add_argument('properties', nargs = argparse.REMAINDER,
                        help = 'property specifications for new task',
                        metavar = 'PROPERTIES')

    return parser.parse_args(argv)

def _help_before_options():
    help_text = '''
Use the new command to add items or properties to the Morg database.
Without any options, this command will add a new item to the database,
using the positional arguments as property specifications for the item.

With the -p option, however, you can add a new property instead of a new
item. You can use -t and -r to specify the new property's type and range.
Positional arguments in this case will be ignored. Similarly, -t and -r
will be ignored without -p.
'''
    return help_text

def _help_after__options():
    help_text = '''
Here is an example showing how to add a new item (it uses some of Morg's
predefined properties):

    new name=test type=task status=wip notes='this is a test'

As you can see, properties are specified as name-value pairs with the
names and values being separated from each other by an equals sign.
Moreover, you should quote names and values that have spaces in them.

If you wish to surround the equals signs with spaces, you should enclose
the entire property specification in quotes. Here is an example:

    new 'odd property name = value with spaces'

If you supply incorrect types for various properties or exceed their
defined ranges, the database will reject them and not add the requested
item. For example, the predefined property day has a type of integer and
a range of [1,7] and the predefined property price has type real without
any range. Thus, assuming you haven't changed Morg's predefined defaults,
the following command is doomed to failure:

    new day=10 price=foo

You don't have to type out the property names in full. You can abbreviate
them to unique prefixes. For example, Morg's default configuration
includes two properties that begin with the letter 'n', viz., name and
notes. Thus, you could just type:

    new na=foo no='well this is just dandy'

However, trying the following will result in an error because Morg won't
be able to tell whether you mean 'n' for name or 'n' for notes:

    new n=oops

Text properties with defined ranges may also be abbreviated. To
illustrate, consider this command:

    n na='what ever' t=t stat=w no='cryptic but less to type!'

Assuming you're using Morg's default settings, the above would create a
new item with the following properties:

    name      what ever
    type      task
    status    wip
    notes     cryptic but less to type!

In the above command, the first 'n' stands for the new command. Since
Morg strives to support shorthand wherever possible, its commands can
also be abbreviated. The "t=t" construct specifies the property named
"type," which has type text and range (task,shop). Thus, the 't' before
the equals expands to "type" and the 't' after the equals to "task."

Similarly, "stat" expands to "status" and 'w' to "wip" (the other
supported values for the status property are "todo" and "done").

When you're adding a new property instead of a task, keep in mind that
Morg only supports integer, real, and text as the possible property
types. That is, the -t option will only accept one of the following
strings: integer, real, text.

For the sake of convenience, you may abbreviate the type strings to any
acceptable unique prefix. Since the three types don't share any common
initial substrings, you could get away with supplying just the first
character of the type as shown below:

    new -p foo -t i

That will create a new integer property named foo that can take on any
integral value supported by the version of SQLite installed on your
system.

To restrict properties to specific ranges, use the -r option as shown
below:

    new -p role -r president,glutton,buffoon

That defines a new text property named role that can take on the values
president, glutton, or buffoon.

As usual, if the property name and/or values have spaces in them, use
quotes:

    new -p 'Job Title' -r 'Vice President,Male Gigolo,One Hit Wonder'

Numeric property ranges can also use commas:

    new -p foo -t i 1,3,5,7,9

Additionally, for numeric types, you can use double dots to specify min
and max values:

    new -p urgency -t r -r 1..-1

Note that we put the min value -1 at the right of the double dot. That's
because it would have confused the new command's command-line parser into
thinking of it as an option -1. But don't worry, Morg's database will do
the right thing when it sees the above range and flip the bounds around
as required.

Any other syntax for property ranges will result in an error.
'''
    return help_text

# Allow property types to be abbreviated and ensure they're restricted to
# the ones that Morg supports.
def _validate_property_type(optval):
    try:
        return morg_util.complete(optval, ('integer', 'real', 'text'))
    except morg_util.completions_error, e:
        raise args.args_error('unknown property type {} ({})'.
                              format(optval, e))

#------------------------------------------------------------------------

##############################################
# Editor config:                             #
##############################################
# Local Variables:                           #

Added py/morglib/properties.py.









































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#
# @file  properties.py
# @brief Morg's predefined item properties.
#

#
# Copyright (C) 2014 The Morg Project Developers
#
# See wiki/copyright.wiki in the top-level directory of the Morg source
# distribution for the full list of authors who have contributed to this
# project.
#

#
# This file is part of Morg.
#
# Morg is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your
# option) any later version.
#
# Morg is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# for more details.
#
# You should have received a copy of the GNU General Public License
# along with Morg. If not, see <http://www.gnu.org/licenses/>.
#

#------------------------ PREDEFINED PROPERTIES -------------------------

# These are Morg's predefined properties, which are geared towards
# managing TODO and shopping lists. But users can augment this list or
# even override it wholesale.
defaults = {'name'        : ('text'   , None           ),
            'type'        : ('text'   , 'task,shop'    ),
            'subitem_id'  : ('integer', None           ),
            'parent_id'   : ('integer', None           ),
            'day'         : ('integer', '1..7'         ),
            'date'        : ('integer', '1..31'        ),
            'week'        : ('integer', '1..5'         ),
            'month'       : ('integer', '1..12'        ),
            'year'        : ('integer', None           ),
            'start_hour'  : ('integer', '0..23'        ),
            'start_minute': ('integer', '0..59'        ),
            'end_hour'    : ('integer', '0..23'        ),
            'end_minute'  : ('integer', '0..59'        ),
            'priority'    : ('integer', None           ),
            'status'      : ('text'   , 'todo,wip,done'),
            'store'       : ('text'   , None           ),
            'price'       : ('real'   , None           ),
            'quantity'    : ('integer', None           ),
            'notes'       : ('text'   , None           ),}

#------------------------------------------------------------------------

##############################################
# Editor config:                             #
##############################################
# Local Variables:                           #
# indent-tabs-mode: nil                      #
# py-indent-offset: 4                        #
# python-indent: 4                           #
# End:                                       #
##############################################
# vim: set expandtab shiftwidth=4 tabstop=4: #
##############################################

Changes to py/morglib/rm.py.

1
2
3
4
5
6
7
8
9
10
..
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#
# @file  rm.py
# @brief Command to deleting tasks and properties.
#

#
# Copyright (C) 2014 The Morg Project Developers
#
# See wiki/copyright.wiki in the top-level directory of the Morg source
# distribution for the full list of authors who have contributed to this
................................................................................
#---------------------------- MODULE LOGGER -----------------------------

logger = logging.getLogger(__name__)

#--------------------------- DELETE COMMAND -----------------------------

class rm(command.base):
    '''Command for deleting tasks and properties.

    '''
    pass

#------------------------------------------------------------------------

##############################################


|







 







|







1
2
3
4
5
6
7
8
9
10
..
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#
# @file  rm.py
# @brief Command to deleting items and properties.
#

#
# Copyright (C) 2014 The Morg Project Developers
#
# See wiki/copyright.wiki in the top-level directory of the Morg source
# distribution for the full list of authors who have contributed to this
................................................................................
#---------------------------- MODULE LOGGER -----------------------------

logger = logging.getLogger(__name__)

#--------------------------- DELETE COMMAND -----------------------------

class rm(command.base):
    '''Command for deleting items and properties.

    '''
    pass

#------------------------------------------------------------------------

##############################################

Changes to wiki/faq.wiki.

1
2
3
4
5
6
7
8
9
10



11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
..
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75




































































76
77
78
79
80
81
82
<title>FAQ</title>

<h1>Frequently Asked Questions</h1>

[#general-questions|About Morg]
  #  [#what-is-morg|What is Morg?]
  #  [#why-yatlm|Why write yet another TODO list manager?]
  #  [#why-named-morg|Why is it called "Morg?"]

[#config-questions|Using Morg]




[#dev-questions|Design and Implementation]
  #  [#why-fossil|Why Fossil instead of git or mercurial?]

<hr>

<a name="general-questions"></a>
<h2>About Morg</h2>

<a name="what-is-morg"></a>
<h3>What is Morg?</h3>

Morg is a TODO list manager that:

  *  Allows you to create tasks to any level of nesting
  *  Allows you to associate tasks with any number of properties
  *  Define custom properties, tailoring your time management to your tastes
  *  Stores its data in an SQLite database
  *  Syncs your TODO list across devices without a third-party server
  *  Can encrypt your TODO list so it's only readable by you
  *  Has a command-line interface as well as a GUI

<hr>

<a name="why-yatlm"></a>
<h3>Why write yet another TODO list manager?</h3>

Because I couldn't find any others that quite fit the bill. Every one of
the others I tried were either too complicated, too limited, or too tied
in to a particular methodology. Furthermore, most seemed to need an
Internet connection in order to work and relied on a third-party server
for syncing. I tend to be leery of such an arrangement.

................................................................................
"Mnorg" is pronounced the same as "mnemonic," i.e., a silent "M").

In the end, I just dropped the "n." Think of it as a contraction of
"morgue," which gives the program a fitting, albeit cheesy, tag line:

<center>
<em>
Morg: Helping you stay on top of all the things you need to get done
before you end up in the morgue...
</em>
</center>

<hr>

<a name="config-questions"></a>
<h2>Using Morg</h2>





































































<hr>

<a name="dev-questions"></a>
<h2>Design and Implementation</h2>

<a name="why-fossil"></a>






|



>
>
>












|

|
|


|
|




|
|







 







|
|







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
..
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
<title>FAQ</title>

<h1>Frequently Asked Questions</h1>

[#general-questions|About Morg]
  #  [#what-is-morg|What is Morg?]
  #  [#why-yalm|Why write yet another list manager?]
  #  [#why-named-morg|Why is it called "Morg?"]

[#config-questions|Using Morg]
  #  [#dependencies|What all do I need to be able to run Morg?]
  #  [#installation|How do I install Morg?]
  #  [#basic-usage-info|How do I use Morg?]

[#dev-questions|Design and Implementation]
  #  [#why-fossil|Why Fossil instead of git or mercurial?]

<hr>

<a name="general-questions"></a>
<h2>About Morg</h2>

<a name="what-is-morg"></a>
<h3>What is Morg?</h3>

Morg is a list manager that:

  *  Allows you to create arbitrary lists of items to any level of nesting
  *  Allows you to associate items with any number of properties
  *  Define custom properties, tailoring your time management to your tastes
  *  Stores its data in an SQLite database
  *  Syncs your lists across devices without a third-party server
  *  Can encrypt your lists so they're readable only by you
  *  Has a command-line interface as well as a GUI

<hr>

<a name="why-yalm"></a>
<h3>Why write yet another list manager?</h3>

Because I couldn't find any others that quite fit the bill. Every one of
the others I tried were either too complicated, too limited, or too tied
in to a particular methodology. Furthermore, most seemed to need an
Internet connection in order to work and relied on a third-party server
for syncing. I tend to be leery of such an arrangement.

................................................................................
"Mnorg" is pronounced the same as "mnemonic," i.e., a silent "M").

In the end, I just dropped the "n." Think of it as a contraction of
"morgue," which gives the program a fitting, albeit cheesy, tag line:

<center>
<em>
Morg: Helping you stay on top of all the things you need to get and get
done before you end up in the morgue...
</em>
</center>

<hr>

<a name="config-questions"></a>
<h2>Using Morg</h2>

<a name="dependencies"></a>
<h3>What all do I need to be able to run Morg?</h3>

You will need the following packages installed on your system:

  *  Fossil
  *  Python 2.7
  *  SQLite 3
  *  APSW

<hr>

<a name="installation"></a>
<h3>How do I install Morg?</h3>

For now, there is no neat package that you can install. Instead, you have
to check-out the source code, which requires Fossil to be installed on
your system.

First, create directories for storing the Fossil repository and Morg
source code:

<verbatim>
    cd
    mkdir fossil morg
</verbatim>

Now, you can clone the Morg repository and check-out the sources:

<verbatim>
    fossil clone http://chiselapp.com/user/morgdude/repository/morg \
                 ~/fossil/morg.fossil
    cd ~/morg
    fossil open ~/fossil/morg.fossil
    fossil update rel
</verbatim>

At this point, you should have the Morg sources in <tt>~/morg</tt>. Under
that will be a subdirectory named <tt>py</tt>, which has the main
program, viz., <tt>morg.py</tt>.

That's it: Morg is now installed and you can use it by running
<tt>~/morg/py/morg.py</tt>.

<hr>

<a name="basic-usage-info"></a>
<h3>How do I use Morg?</h3>

At this time, Morg only sports a command-line interface. You can launch
it by running the <tt>morg.py</tt> program. Use the <tt>--help</tt>
option to get usage information. In particular, without any arguments,
Morg will drop you into interactive mode, prompting you to type in
commands until you exit the program.

You can use the <tt>help</tt> command to get a list of available
commands. Passing the name of one of the available commands to the
<tt>help</tt> command will print details about that command.

As these are early days, the only real command you can run is
<tt>new</tt>, which allows you to create new items and properties.

Eventually, Morg will have commands to search the database, edit items,
and to remove items and properties. Eventually, we will also build a
spiffy GUI for the application so you don't have to deal with the cryptic
command-line interface unless you want to run a quick query without
having to fire up the GUI.

<hr>

<a name="dev-questions"></a>
<h2>Design and Implementation</h2>

<a name="why-fossil"></a>

Changes to wiki/home.wiki.

1
2
3
4
5


6
7
8




9
10
11
12
13
14
15
16
17
18
19
20
21
22
..
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
..
54
55
56
57
58
59
60

61
62
63
64
65
66
67
68
69
<title>Home</title>

<h1>About Morg</h1>

Morg is a TODO list manager with the following goals:



  *  <b>Hierarchical Task List</b>: Morg allows tasks to be arranged
     hierarchically, i.e., a task may have subtasks, which, in turn, may




     have further subtasks to any level of nesting.

  *  <b>Arbitrary Tags</b>: Each task may be associated with any number
     of properties such as name, time slot, urgency, importance, and so
     on. Properties may be repeated; thus, for example, a task may, if
     you like, have more than one name, more than one subtask, and even
     more than one parent task.

  *  <b>Versatility</b>: Morg does not adhere to any particular time
     management methodology (or fad). Rather, it implements a sound data
     model and provides functions to manipulate that data and then gets
     out of your way, which means you can adapt it your particular needs
     and style.

................................................................................
     don't want hierarchical lists, you can delete the properties that
     facilitate list nesting.

  *  <b>Portability</b>: Morg stores its data in an SQLite database. On
     laptops and desktops, it is written in Python and, so, will work
     wherever Python is available. It also has an Android app. And,
     finally, it supports syncing across devices, which means you can
     take your TODO list with you anywhere and have updates propagate
     without requiring a bunch of other tools.

  *  <b>Security</b>: Your TODO list can be intensely personal to you. No
     one else should have access to it without your consent. Thus, Morg
     has support for database encryption. Moreover, syncing is
     peer-to-peer, i.e., syncing between your phone and laptop will not
     involve a third-party server.

In short, Morg helps you stay on top of all the things you need to get
done before you end up in the morgue...

<h1>News</h1>

<h3>September 4, 2014</h3>
  *  <em>First milestone: version 0.1.0</em>.<br>
     It doesn't do much, but is a step in the right direction toward the grand
     vision laid out above.<br>
................................................................................


<h1>Documentation</h1>

<h2>For End-users</h2>

  *  [/doc/rel/wiki/faq.wiki|Frequently Asked Questions]


<h2>For Developers</h2>

  *  [/doc/rel/wiki/process.wiki|Branching and Release Policy]

<h1>Legalese</h1>

  *  [/doc/rel/wiki/copyright.wiki|Copyright]
  *  [http://www.gnu.org/licenses/gpl-3.0-standalone.html|License]




|
>
>

<
<
>
>
>
>
|

|
|
|
|
|







 







|
|

|
|
|
|
|


|







 







>









1
2
3
4
5
6
7
8


9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
..
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
..
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
<title>Home</title>

<h1>About Morg</h1>

Morg is a list manager that is geared toward helping you keep track of
the things you need to do (i.e., tasks list) and the things you need to
get (i.e., shopping lists).



Morg has the following design goals:

  *  <b>Hierarchical Lists</b>: Morg allows items to be arranged
     hierarchically, i.e., an item may have subitems, which, in turn, may
     have further subitems to any level of nesting.

  *  <b>Arbitrary Tags</b>: Each item may be associated with any number
     of properties such as name, time slot, priority, price, quantity,
     and so on. Properties may be repeated; thus, for example, an item
     may, if you like, have more than one name, more than one subitem,
     and even more than one parent item.

  *  <b>Versatility</b>: Morg does not adhere to any particular time
     management methodology (or fad). Rather, it implements a sound data
     model and provides functions to manipulate that data and then gets
     out of your way, which means you can adapt it your particular needs
     and style.

................................................................................
     don't want hierarchical lists, you can delete the properties that
     facilitate list nesting.

  *  <b>Portability</b>: Morg stores its data in an SQLite database. On
     laptops and desktops, it is written in Python and, so, will work
     wherever Python is available. It also has an Android app. And,
     finally, it supports syncing across devices, which means you can
     take your TODO and shopping lists with you anywhere and have updates
     propagate without requiring a bunch of other tools.

  *  <b>Security</b>: Your task and shopping list can be intensely
     personal to you. No one else should have access to these things
     without your consent. Thus, Morg has support for database
     encryption. Moreover, syncing is peer-to-peer, i.e., syncing between
     your phone and laptop will not involve a third-party server.

In short, Morg helps you stay on top of all the things you need to get
and/or get done before you end up in the morgue...

<h1>News</h1>

<h3>September 4, 2014</h3>
  *  <em>First milestone: version 0.1.0</em>.<br>
     It doesn't do much, but is a step in the right direction toward the grand
     vision laid out above.<br>
................................................................................


<h1>Documentation</h1>

<h2>For End-users</h2>

  *  [/doc/rel/wiki/faq.wiki|Frequently Asked Questions]
  *  [/doc/rel/wiki/schema.wiki|Morg's Database Schema]

<h2>For Developers</h2>

  *  [/doc/rel/wiki/process.wiki|Branching and Release Policy]

<h1>Legalese</h1>

  *  [/doc/rel/wiki/copyright.wiki|Copyright]
  *  [http://www.gnu.org/licenses/gpl-3.0-standalone.html|License]

Changes to wiki/milestones.wiki.

7
8
9
10
11
12
13
14
15
16
17
18











<b>NOTE:</b> Eventually, as Morg becomes usable (nearing the version 1.0
release), we should be able to track all relevant TODO's and these
milestones using Morg itself, thus, making this page unnecessary.

<h1>Major Milestones</h1>

  *  <b>Version 1.0.0</b>: Full-fledged command-line UI.
  *  <b>Version 2.0.0</b>: Syncing between multiple desktops.
  *  <b>Version 3.0.0</b>: Database encryption.
  *  <b>Version 4.0.0</b>: Android app.
  *  <b>Version 5.0.0</b>: Desktop GUI.

















|
|
|
|
|
>
>
>
>
>
>
>
>
>
>
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

<b>NOTE:</b> Eventually, as Morg becomes usable (nearing the version 1.0
release), we should be able to track all relevant TODO's and these
milestones using Morg itself, thus, making this page unnecessary.

<h1>Major Milestones</h1>

  *  <b>Version 1.0.0:</b> Full-fledged command-line UI.
  *  <b>Version 2.0.0:</b> Syncing between multiple desktops.
  *  <b>Version 3.0.0:</b> Database encryption.
  *  <b>Version 4.0.0:</b> Desktop GUI.
  *  <b>Version 5.0.0:</b> Android app.

<h1>Version 1.0.0 Milestones</h1>

  *  <b>Version 0.1.0:</b> Basic command-line UI (all stubs though).
  *  <b>Version 0.2.0:</b> Functional <tt>new</tt> command.
  *  <b>Version 0.3.0:</b> Functional <tt>ls</tt> command.
  *  <b>Version 0.4.0:</b> Functional <tt>edit</tt> command.
  *  <b>Version 0.5.0:</b> Functional <tt>rm</tt> command.
  *  <b>Version 0.6.0:</b> Support for user-defined commands.
  *  <b>Version 0.7.0:</b> Syntactic sugar (@ and @@) for time specifications.

Changes to wiki/process.wiki.

52
53
54
55
56
57
58
59
  #  Replace all <em>ckout</em> in docs with <em>rel</em>.
  #  Update <em>wiki/changelog.wiki</em> and <em>wiki/relnotes.wiki</em>.
  #  Update <b>News</b> section of <em>wiki/home.wiki</em> plus any other
     docs that need updating.
  #  Update version number in <tt>py/morg.py</tt> and commit --tag as
     vA.B.C (where major number A, minor number B, and patch level C all
     match the new version number assigned in the code).
  #  Push to [http://chiselapp.com|chiselapp].







|
52
53
54
55
56
57
58
59
  #  Replace all <em>ckout</em> in docs with <em>rel</em>.
  #  Update <em>wiki/changelog.wiki</em> and <em>wiki/relnotes.wiki</em>.
  #  Update <b>News</b> section of <em>wiki/home.wiki</em> plus any other
     docs that need updating.
  #  Update version number in <tt>py/morg.py</tt> and commit --tag as
     vA.B.C (where major number A, minor number B, and patch level C all
     match the new version number assigned in the code).
  #  Push to [https://chiselapp.com/user/morgdude/repository/morg|chiselapp].

Added wiki/schema.wiki.











































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
<title>Morg Database Schema</title>

This page describes the different tables that Morg uses in its items
database and the relationships between them.

<h1>morg</h1>

<center>
<table border="1">
  <tr>
    <td> schema_version (integer) </td>
    <td> timestamp (text) </td>
    <td> hostname (text) </td>
    <td> username (text) </td>
    <td> magic_phrase (text) </td>
  </tr>
</table>
</center>

This table is used to identify a Morg database. The Morg application will
not use a preexisting SQLite file passed to it unless that database meets
all of the following critera:

  *  It contains a table named <tt>morg</tt> that has exactly the above
     columns and exactly one row.
  *  The magic phrase matches the expected value.

The schema version is the database version, not the Morg application's
version. This is simply an integer we will bump up every time we make
substantial changes to the data model (e.g., going from a single host to
a database synced across multiple hosts).

The timestamp is simply the creation time for a new database recorded in
the format <tt>yyyy-mm-dd HH:MM:SS</tt>.

The magic phrase is the SHA-1 ID computed by concatenating the other
columns along with a hard-coded string.

Although the above test is easily subverted, it does, nonetheless,
provide a reasonable sanity check that Morg is, in fact, using a valid
Morg database and not futzing around with something else.

<h1>Items</h1>

<h2>item</h2>

<center>
<table border="1">
  <tr> <td> id </td> </tr>
</table>
</center>

This table is simply a list of all the items in the database. Each item
has a corresponding table named <tt>item_NNN</tt>, where <tt>NNN</tt> is
the item ID.

<h2>item_NNN</h2>

<center>
<table border="1">
  <tr> <td> property_id </td> <td> value_id </td> </tr>
</table>
</center>

For each item, we record a collection of properties and their
corresponding values. The <tt>property_id</tt> refers to the row of the
<tt>property</tt> table. The <tt>value_id</tt> refers to the row of the
<tt>property_NNN</tt> table.

An item is allowed to have more than one instance of the same property.
Thus, for example, an item may have multiple names, subitems, parent
items, etc.

<h1>Properties</h1>

<h2>property</h2>

<center>
<table border="1">
  <tr>
    <td> id </td>
    <td> name (text) </td>
    <td> type (text) </td>
    <td> range (text) </td>
  </tr>
</table>
</center>

This table records the name, type, and range of each property that Morg
can associate with an item.

Property names are arbitrary strings assigned by users. Property types,
however, are restricted to one of the following (case-insensitive)
strings:

  *  integer
  *  real
  *  text

The range of a property is optional. If given, Morg will interpret the
range string as follows:

  *  Commas separate discrete allowed values, e.g., a property
     <tt>foo</tt> with type <tt>integer</tt> and range <tt>1,3,5,7</tt>
     can only take on those values.
  *  Double dots specify numeric ranges, e.g., a property <tt>foo</tt> of
     type <tt>real</tt> and range <tt>-1..+1</tt> will be allowed to take
     on values from -1 to +1 (inclusive).

<h2>property_NNN</h2>

<center>
<table border="1">
  <tr>
    <td> id </td>
    <td> item_id </td>
    <td> value </td>
  </tr>
</table>
</center>

Similar to items, for each property, we will have a table
<tt>property_NNN</tt>, where <tt>NNN</tt> is the ID of the property
defined in the <tt>property</tt> table.

In a <tt>property_NNN</tt> table, we record the ID of the item to which
that particular instance of the property belongs along with its value.
The <tt>value</tt> column's type is the same as the one recorded in the
<tt>property</tt> table's <tt>type</tt> column for that property.

<h2>Predefined Properties</h2>

Morg allows users to define whatever properties they would like. However,
in a new database, for the sake of convenience and to be able to provide
some basic functionality out-of-the-box, Morg sets up the following
predefined properties:

<center>
<table border="1">
  <tr> <td> id </td> <td> name </td> <td> type </td> <td> range </td> </tr>
  <tr>
    <td> </td>
    <td> name </td>
    <td> text </td>
    <td> </td>
  </tr>
  <tr>
    <td> </td>
    <td> type </td>
    <td> text </td>
    <td> task, shop </td>
  </tr>
  <tr>
    <td> </td>
    <td> subitem_id </td>
    <td> integer </td>
    <td> </td>
  </tr>
  <tr>
    <td> </td>
    <td> parent_id </td>
    <td> integer </td>
    <td> </td>
  </tr>
  <tr>
    <td> </td>
    <td> day </td>
    <td> integer </td>
    <td> 1 .. 7 </td>
  </tr>
  <tr>
    <td> </td>
    <td> date </td>
    <td> integer </td>
    <td> 1 .. 31 </td>
  </tr>
  <tr>
    <td> </td>
    <td> week </td>
    <td> integer </td>
    <td> 1 .. 5 </td>
  </tr>
  <tr>
    <td> </td>
    <td> month </td>
    <td> integer </td>
    <td> 1 .. 12 </td>
  </tr>
  <tr>
    <td> </td>
    <td> year </td>
    <td> integer </td>
    <td> </td>
  </tr>
  <tr>
    <td> </td>
    <td> start_hour </td>
    <td> integer </td>
    <td> 0 .. 23 </td>
  </tr>
  <tr>
    <td> </td>
    <td> start_minute </td>
    <td> integer </td>
    <td> 0 .. 59 </td>
  </tr>
  <tr>
    <td> </td>
    <td> end_hour </td>
    <td> integer </td>
    <td> 0 .. 23 </td>
  </tr>
  <tr>
    <td> </td>
    <td> end_minute </td>
    <td> integer </td>
    <td> 0 .. 59 </td>
  </tr>
  <tr>
    <td> </td>
    <td> priority </td>
    <td> integer </td>
    <td> </td>
  </tr>
  <tr>
    <td> </td>
    <td> status </td>
    <td> text </td>
    <td> todo, wip, done </td>
  </tr>
  <tr>
    <td> </td>
    <td> store </td>
    <td> text </td>
    <td> </td>
  </tr>
  <tr>
    <td> </td>
    <td> price </td>
    <td> real </td>
    <td> </td>
  </tr>
  <tr>
    <td> </td>
    <td> quantity </td>
    <td> integer </td>
    <td> </td>
  </tr>
  <tr>
    <td> </td>
    <td> notes </td>
    <td> text </td>
    <td> </td>
  </tr>
</table>
</center>

Users are not restricted to associating items with the above properties.
They may augment the above properties with their own. They may also
delete these defaults and define their own properties from scratch if
they so desire.

Changes to wiki/todo.wiki.

2
3
4
5
6
7
8
9
10
11
12
13
14

15
16
17
18
19
20
21
22
23










24
25
26

<b>NOTE:</b> Eventually, as Morg becomes usable (nearing the version 1.0
release), we should be able to track all relevant TODO's using Morg
itself, thus, making this page unnecessary.

<h1>Current TODO List</h1>

To get to version 0.1.0, the following tasks have to be completed:

<h2>DONE</h2>

  *  Implement main() with support for command-line arguments.
  *  Implement logging support.

  *  Add stubs for the various commands.
  *  Execute above stubs via command-line args.
  *  Add support for interactive mode and hook it up to above stubs.
  *  Help should not accept help as an argument.
  *  Help should cache the command list rather than build it each time.
  *  Help should not show EOF.
  *  The example help command should be near the middle of the list.
  *  Interpreter should accept shortest unique command prefix.
  *  Exit commands should accept --help.











<h2>PENDING</h2>








|



<
|
>
|
|
|
|
|
|
|
|
|
>
>
>
>
>
>
>
>
>
>



2
3
4
5
6
7
8
9
10
11
12

13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

<b>NOTE:</b> Eventually, as Morg becomes usable (nearing the version 1.0
release), we should be able to track all relevant TODO's using Morg
itself, thus, making this page unnecessary.

<h1>Current TODO List</h1>

To get to version 0.2.0, the following tasks have to be completed:

<h2>DONE</h2>


  *  Implement <tt>morglib.database</tt> stub.
  *  Instantiate database object in <tt>main()</tt>.
  *  Create <tt>morg</tt> table on database init.
  *  Create <tt>task</tt> table on database init.
  *  Create <tt>property</tt> table on database init.
  *  Add default properties dict.
  *  Create <tt>property_NNN</tt> tables on database init.
  *  Implement sanity check on database initialization.
  *  Update database constructor doc string.
  *  Use the BETWEEN operator in property range constraints.
  *  Implement a watermark class to ease verification and "Morgification."
  *  The watermark table should be created and populated in a transaction.
  *  Update doc strings to reflect recent changes about the watermark.
  *  Pass database to all commands' <tt>__call__</tt> method.
  *  Generalize Morg so it isn't necessarily task-centric.
  *  Implement <tt>morglib.database.add_item()</tt>.
  *  Implement <tt>new</tt> command-line processing.
  *  Hook up <tt>new</tt> command to <tt>add_item()</tt>.
  *  Hook up <tt>new</tt> command to <tt>add_property()</tt>.
  *  Refactor completions code and exception into utils module.
  *  Add some user documentation.

<h2>PENDING</h2>