Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python argparse: Create timedelta object from argument?

I am attempting to use argparse to convert an argument into a timedelta object. My program reads in strings supplied by the user and converts them to various datetime objects for later usage. I cannot get the filter_length argument to process correctly though. My code:

import datetime
import time
import argparse

def mkdate(datestring):
    return datetime.datetime.strptime(datestring, '%Y-%m-%d').date()

def mktime(timestring):
    return datetime.datetime.strptime(timestring, '%I:%M%p').time()

def mkdelta(deltatuple):
    return datetime.timedelta(deltatuple)

parser = argparse.ArgumentParser()
parser.add_argument('start_date', type=mkdate, nargs=1)
parser.add_argument('start_time', type=mktime, nargs=1, )
parser.add_argument('filter_length', type=mkdelta, nargs=1, default=datetime.timedelta(1))#default filter length is 1 day.

I run the program, passing 1 as the timedelta value (I only want it to be one day):

> python program.py 2012-09-16 11:00am 1

But I get the following error:

>>> program.py: error: argument filter_length: invalid mkdelta value: '1'

I don't understand why the value is invalid. If I call the mkdelta function on its own, like this:

mkdelta(1)
print mkdelta(1)

It returns:

datetime.timedelta(1)
1 day, 0:00:00

This is exactly the value that I'm looking for. Can someone help me figure out how to do this conversion properly using argparse?

like image 925
schemanic Avatar asked Sep 17 '12 15:09

schemanic


3 Answers

Notice the quotes around '1' in your error message? You pass a string to mkdelta, whereas in your test code, you pass an integer.

like image 199
Hans Then Avatar answered Nov 10 '22 23:11

Hans Then


Your function doesn't handle a string argument, which is what argparse is handing it; call int() on it:

def mkdelta(deltatuple):
    return datetime.timedelta(int(deltatuple))

If you need to support more than days, you'll have to find a way to parse the argument passed in into timedelta arguments.

You could, for example, support d, h, m or s postfixes to denote days, hours, minutes or seconds:

_units = dict(d=60*60*24, h=60*60, m=60, s=1)
def mkdelta(deltavalue):
    seconds = 0
    defaultunit = unit = _units['d']  # default to days
    value = ''
    for ch in list(str(deltavalue).strip()):
        if ch.isdigit():
            value += ch
            continue
        if ch in _units:
            unit = _units[ch]
            if value:
                seconds += unit * int(value)
                value = ''
                unit = defaultunit
            continue
        if ch in ' \t':
            # skip whitespace
            continue
        raise ValueError('Invalid time delta: %s' % deltavalue)
    if value:
        seconds = unit * int(value)
    return datetime.timedelta(seconds=seconds)

Now your mkdelta method accepts more complete deltas, and even integers still:

>>> mkdelta('1d')
datetime.timedelta(1)
>>> mkdelta('10s')
datetime.timedelta(0, 10)
>>> mkdelta('5d 10h 3m 10s')
datetime.timedelta(5, 36190)
>>> mkdelta(5)
datetime.timedelta(5)
>>> mkdelta('1')
datetime.timedelta(1)

The default unit is days.

like image 28
Martijn Pieters Avatar answered Nov 10 '22 23:11

Martijn Pieters


You could use a custom action to collect all the remaining args and parse them into a timedelta.

This will allow you to write CLI commands such as

% test.py 2012-09-16 11:00am 2 3 4 5
datetime.timedelta(2, 3, 5004)   # args.filter_length

You could also provide optional arguments for --days, --seconds, etc, so you can write CLI commands such as

% test.py 2012-09-16 11:00am --weeks 6 --days 0
datetime.timedelta(42)           # args.filter_length

% test.py 2012-09-16 11:00am --weeks 6.5 --days 0
datetime.timedelta(45, 43200)

import datetime as dt
import argparse

def mkdate(datestring):
    return dt.datetime.strptime(datestring, '%Y-%m-%d').date()

def mktime(timestring):
    return dt.datetime.strptime(timestring, '%I:%M%p').time()

class TimeDeltaAction(argparse.Action):
    def __call__(self, parser, args, values, option_string = None):
        # print '{n} {v} {o}'.format(n = args, v = values, o = option_string)
        setattr(args, self.dest, dt.timedelta(*map(float, values)))

parser = argparse.ArgumentParser()
parser.add_argument('start_date', type = mkdate)
parser.add_argument('start_time', type = mktime)
parser.add_argument('--days', type = float, default = 1)
parser.add_argument('--seconds', type = float, default = 0)
parser.add_argument('--microseconds', type = float, default = 0)
parser.add_argument('--milliseconds', type = float, default = 0)
parser.add_argument('--minutes', type = float, default = 0)
parser.add_argument('--hours', type = float, default = 0)
parser.add_argument('--weeks', type = float, default = 0)
parser.add_argument('filter_length', nargs = '*', action = TimeDeltaAction)

args = parser.parse_args()
if not args.filter_length:
    args.filter_length = dt.timedelta(
        args.days, args.seconds, args.microseconds, args.milliseconds,
        args.minutes, args.hours, args.weeks)
print(repr(args.filter_length))
like image 30
unutbu Avatar answered Nov 10 '22 23:11

unutbu