imip-agent

Annotated imipweb/data.py

686:b5bdf9dcad0f
2015-09-04 Paul Boddie Added docstrings.
paul@497 1
#!/usr/bin/env python
paul@497 2
paul@497 3
"""
paul@497 4
Web interface data abstractions.
paul@497 5
paul@497 6
Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk>
paul@497 7
paul@497 8
This program is free software; you can redistribute it and/or modify it under
paul@497 9
the terms of the GNU General Public License as published by the Free Software
paul@497 10
Foundation; either version 3 of the License, or (at your option) any later
paul@497 11
version.
paul@497 12
paul@497 13
This program is distributed in the hope that it will be useful, but WITHOUT
paul@497 14
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
paul@497 15
FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
paul@497 16
details.
paul@497 17
paul@497 18
You should have received a copy of the GNU General Public License along with
paul@497 19
this program.  If not, see <http://www.gnu.org/licenses/>.
paul@497 20
"""
paul@497 21
paul@556 22
from datetime import datetime, timedelta
paul@539 23
from imiptools.dates import end_date_from_calendar, end_date_to_calendar, \
paul@538 24
                            format_datetime, get_datetime, get_end_of_day, \
paul@532 25
                            to_date
paul@621 26
from imiptools.period import RecurringPeriod
paul@497 27
paul@498 28
class PeriodError(Exception):
paul@498 29
    pass
paul@498 30
paul@539 31
class EventPeriod(RecurringPeriod):
paul@497 32
paul@498 33
    """
paul@498 34
    A simple period plus attribute details, compatible with RecurringPeriod, and
paul@498 35
    intended to represent information obtained from an iCalendar resource.
paul@498 36
    """
paul@497 37
paul@541 38
    def __init__(self, start, end, tzid=None, origin=None, start_attr=None, end_attr=None, form_start=None, form_end=None):
paul@528 39
paul@528 40
        """
paul@528 41
        Initialise a period with the given 'start' and 'end' datetimes, together
paul@528 42
        with optional 'start_attr' and 'end_attr' metadata, 'form_start' and
paul@528 43
        'form_end' values provided as textual input, and with an optional
paul@528 44
        'origin' indicating the kind of period this object describes.
paul@528 45
        """
paul@528 46
paul@541 47
        RecurringPeriod.__init__(self, start, end, tzid, origin, start_attr, end_attr)
paul@498 48
        self.form_start = form_start
paul@498 49
        self.form_end = form_end
paul@498 50
paul@498 51
    def as_tuple(self):
paul@541 52
        return self.start, self.end, self.tzid, self.origin, self.start_attr, self.end_attr, self.form_start, self.form_end
paul@498 53
paul@498 54
    def __repr__(self):
paul@541 55
        return "EventPeriod(%r)" % (self.as_tuple(),)
paul@499 56
paul@499 57
    def as_event_period(self):
paul@499 58
        return self
paul@499 59
paul@499 60
    # Form data compatibility methods.
paul@498 61
paul@498 62
    def get_form_start(self):
paul@498 63
        if not self.form_start:
paul@499 64
            self.form_start = self.get_form_date(self.get_start(), self.start_attr)
paul@498 65
        return self.form_start
paul@498 66
paul@498 67
    def get_form_end(self):
paul@498 68
        if not self.form_end:
paul@539 69
            self.form_end = self.get_form_date(end_date_from_calendar(self.get_end()), self.end_attr)
paul@498 70
        return self.form_end
paul@498 71
paul@498 72
    def as_form_period(self):
paul@498 73
        return FormPeriod(
paul@499 74
            self.get_form_start(),
paul@499 75
            self.get_form_end(),
paul@556 76
            isinstance(self.end, datetime) or self.get_start() != self.get_end() - timedelta(1),
paul@532 77
            isinstance(self.start, datetime) or isinstance(self.end, datetime),
paul@541 78
            self.tzid,
paul@532 79
            self.origin
paul@498 80
            )
paul@498 81
paul@498 82
    def get_form_date(self, dt, attr=None):
paul@498 83
        return FormDate(
paul@498 84
            format_datetime(to_date(dt)),
paul@498 85
            isinstance(dt, datetime) and str(dt.hour) or None,
paul@498 86
            isinstance(dt, datetime) and str(dt.minute) or None,
paul@498 87
            isinstance(dt, datetime) and str(dt.second) or None,
paul@498 88
            attr and attr.get("TZID") or None,
paul@498 89
            dt, attr
paul@498 90
            )
paul@498 91
paul@620 92
class FormPeriod(RecurringPeriod):
paul@498 93
paul@498 94
    "A period whose information originates from a form."
paul@498 95
paul@541 96
    def __init__(self, start, end, end_enabled=True, times_enabled=True, tzid=None, origin=None):
paul@498 97
        self.start = start
paul@498 98
        self.end = end
paul@498 99
        self.end_enabled = end_enabled
paul@498 100
        self.times_enabled = times_enabled
paul@541 101
        self.tzid = tzid
paul@499 102
        self.origin = origin
paul@497 103
paul@497 104
    def as_tuple(self):
paul@541 105
        return self.start, self.end, self.end_enabled, self.times_enabled, self.tzid, self.origin
paul@497 106
paul@497 107
    def __repr__(self):
paul@541 108
        return "FormPeriod(%r)" % (self.as_tuple(),)
paul@498 109
paul@499 110
    def as_event_period(self, index=None):
paul@528 111
paul@528 112
        """
paul@528 113
        Return a converted version of this object as an event period suitable
paul@528 114
        for iCalendar usage. If 'index' is indicated, include it in any error
paul@528 115
        raised in the conversion process.
paul@528 116
        """
paul@528 117
paul@528 118
        dtstart, dtstart_attr = self._get_start()
paul@528 119
        if not dtstart:
paul@499 120
            raise PeriodError(*[index is not None and ("dtstart", index) or "dtstart"])
paul@499 121
paul@538 122
        dtend, dtend_attr = self._get_end()
paul@528 123
        if not dtend:
paul@499 124
            raise PeriodError(*[index is not None and ("dtend", index) or "dtend"])
paul@499 125
paul@499 126
        if dtstart > dtend:
paul@499 127
            raise PeriodError(*[
paul@499 128
                index is not None and ("dtstart", index) or "dtstart",
paul@499 129
                index is not None and ("dtend", index) or "dtend"
paul@499 130
                ])
paul@499 131
paul@544 132
        return EventPeriod(dtstart, end_date_to_calendar(dtend), self.tzid, self.origin, dtstart_attr, dtend_attr, self.start, self.end)
paul@499 133
paul@499 134
    # Period data methods.
paul@499 135
paul@498 136
    def get_start(self):
paul@620 137
        return self.start.as_datetime(self.times_enabled)
paul@498 138
paul@498 139
    def get_end(self):
paul@620 140
paul@620 141
        # Handle specified end datetimes.
paul@620 142
paul@620 143
        if self.end_enabled:
paul@620 144
            dtend = self.end.as_datetime(self.times_enabled)
paul@620 145
            if not dtend:
paul@620 146
                return None
paul@620 147
paul@620 148
        # Otherwise, treat the end date as the start date. Datetimes are
paul@620 149
        # handled by making the event occupy the rest of the day.
paul@620 150
paul@620 151
        else:
paul@620 152
            dtstart, dtstart_attr = self.get_start_item()
paul@620 153
            if dtstart:
paul@620 154
                if isinstance(dtstart, datetime):
paul@620 155
                    dtend = get_end_of_day(dtstart, dtstart_attr["TZID"])
paul@620 156
                else:
paul@620 157
                    dtend = dtstart
paul@620 158
            else:
paul@620 159
                return None
paul@620 160
paul@528 161
        return dtend
paul@528 162
paul@620 163
    def get_start_attr(self):
paul@620 164
        return self.start.get_attributes(self.times_enabled)
paul@528 165
paul@620 166
    def get_end_attr(self):
paul@620 167
        return self.end.get_attributes(self.times_enabled)
paul@498 168
paul@499 169
    # Form data methods.
paul@498 170
paul@498 171
    def get_form_start(self):
paul@498 172
        return self.start
paul@498 173
paul@498 174
    def get_form_end(self):
paul@498 175
        return self.end
paul@498 176
paul@498 177
    def as_form_period(self):
paul@498 178
        return self
paul@497 179
paul@498 180
class FormDate:
paul@498 181
paul@498 182
    "Date information originating from form information."
paul@498 183
paul@498 184
    def __init__(self, date=None, hour=None, minute=None, second=None, tzid=None, dt=None, attr=None):
paul@498 185
        self.date = date
paul@498 186
        self.hour = hour
paul@498 187
        self.minute = minute
paul@498 188
        self.second = second
paul@498 189
        self.tzid = tzid
paul@498 190
        self.dt = dt
paul@498 191
        self.attr = attr
paul@498 192
paul@498 193
    def as_tuple(self):
paul@498 194
        return self.date, self.hour, self.minute, self.second, self.tzid, self.dt, self.attr
paul@498 195
paul@498 196
    def __repr__(self):
paul@541 197
        return "FormDate(%r)" % (self.as_tuple(),)
paul@498 198
paul@498 199
    def get_component(self, value):
paul@498 200
        return (value or "").rjust(2, "0")[:2]
paul@498 201
paul@498 202
    def get_hour(self):
paul@498 203
        return self.get_component(self.hour)
paul@498 204
paul@498 205
    def get_minute(self):
paul@498 206
        return self.get_component(self.minute)
paul@498 207
paul@498 208
    def get_second(self):
paul@498 209
        return self.get_component(self.second)
paul@498 210
paul@498 211
    def get_date_string(self):
paul@498 212
        return self.date or ""
paul@498 213
paul@498 214
    def get_datetime_string(self):
paul@498 215
        if not self.date:
paul@498 216
            return ""
paul@498 217
paul@498 218
        hour = self.hour; minute = self.minute; second = self.second
paul@498 219
paul@498 220
        if hour or minute or second:
paul@498 221
            time = "T%s%s%s" % tuple(map(self.get_component, (hour, minute, second)))
paul@498 222
        else:
paul@498 223
            time = ""
paul@498 224
            
paul@498 225
        return "%s%s" % (self.date, time)
paul@498 226
paul@498 227
    def get_tzid(self):
paul@498 228
        return self.tzid
paul@498 229
paul@528 230
    def as_datetime(self, with_time=True):
paul@498 231
paul@528 232
        "Return a datetime for this object."
paul@498 233
paul@498 234
        # Return any original datetime details.
paul@498 235
paul@498 236
        if self.dt:
paul@528 237
            return self.dt
paul@498 238
paul@528 239
        # Otherwise, construct a datetime.
paul@498 240
paul@528 241
        s, attr = self.as_datetime_item(with_time)
paul@528 242
        if s:
paul@528 243
            return get_datetime(s, attr)
paul@498 244
        else:
paul@528 245
            return None
paul@528 246
paul@528 247
    def as_datetime_item(self, with_time=True):
paul@498 248
paul@528 249
        """
paul@528 250
        Return a (datetime string, attr) tuple for the datetime information
paul@528 251
        provided by this object, where both tuple elements will be None if no
paul@528 252
        suitable date or datetime information exists.
paul@528 253
        """
paul@498 254
paul@528 255
        s = None
paul@528 256
        if with_time:
paul@528 257
            s = self.get_datetime_string()
paul@528 258
            attr = self.get_attributes(True)
paul@528 259
        if not s:
paul@528 260
            s = self.get_date_string()
paul@528 261
            attr = self.get_attributes(False)
paul@528 262
        if not s:
paul@528 263
            return None, None
paul@528 264
        return s, attr
paul@498 265
paul@528 266
    def get_attributes(self, with_time=True):
paul@528 267
paul@528 268
        "Return attributes for the date or datetime represented by this object."
paul@498 269
paul@528 270
        if with_time:
paul@528 271
            return {"TZID" : self.get_tzid(), "VALUE" : "DATE-TIME"}
paul@528 272
        else:
paul@528 273
            return {"VALUE" : "DATE"}
paul@498 274
paul@499 275
def event_period_from_period(period):
paul@624 276
paul@624 277
    """
paul@624 278
    Convert a 'period' to one suitable for use in an iCalendar representation.
paul@624 279
    In an "event period" representation, the end day of any date-level event is
paul@624 280
    encoded as the "day after" the last day actually involved in the event.
paul@624 281
    """
paul@624 282
paul@499 283
    if isinstance(period, EventPeriod):
paul@499 284
        return period
paul@499 285
    elif isinstance(period, FormPeriod):
paul@499 286
        return period.as_event_period()
paul@499 287
    else:
paul@528 288
        dtstart, dtstart_attr = period.get_start_item()
paul@528 289
        dtend, dtend_attr = period.get_end_item()
paul@539 290
        if not isinstance(period, RecurringPeriod):
paul@539 291
            dtend = end_date_to_calendar(dtend)
paul@541 292
        return EventPeriod(dtstart, dtend, period.tzid, period.origin, dtstart_attr, dtend_attr)
paul@499 293
paul@499 294
def form_period_from_period(period):
paul@624 295
paul@624 296
    """
paul@624 297
    Convert a 'period' into a representation usable in a user-editable form.
paul@624 298
    In a "form period" representation, the end day of any date-level event is
paul@624 299
    presented in a "natural" form, not the iCalendar "day after" form.
paul@624 300
    """
paul@624 301
paul@499 302
    if isinstance(period, EventPeriod):
paul@499 303
        return period.as_form_period()
paul@499 304
    elif isinstance(period, FormPeriod):
paul@499 305
        return period
paul@499 306
    else:
paul@499 307
        return event_period_from_period(period).as_form_period()
paul@499 308
paul@497 309
# vim: tabstop=4 expandtab shiftwidth=4