Merge lp:~oubiwann/txaws/416366-remove-epsilon into lp:txaws

Proposed by Duncan McGreggor
Status: Rejected
Rejected by: Duncan McGreggor
Proposed branch: lp:~oubiwann/txaws/416366-remove-epsilon
Merge into: lp:txaws
Diff against target: 687 lines
4 files modified
txaws/storage/client.py (+1/-3)
txaws/storage/tests/test_client.py (+5/-8)
txaws/tests/test_util.py (+109/-2)
txaws/util.py (+475/-3)
To merge this branch: bzr merge lp:~oubiwann/txaws/416366-remove-epsilon
Reviewer Review Type Date Requested Status
Duncan McGreggor Disapprove
Review via email: mp+14447@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Duncan McGreggor (oubiwann) wrote :

I've copied over particular aspects of Divmod's ISO datetime stuff (including some unit tests) for use by txaws.storage. This way, the Epslion package doesn't need to be installed by folks that want to use txaws.storage.

46. By Duncan McGreggor

Fixed pyflakes.

47. By Duncan McGreggor

Fixed some formatting.

48. By Duncan McGreggor

Fixed typo.

Revision history for this message
Duncan McGreggor (oubiwann) wrote :

We're not going to remove Epsilon support.

review: Disapprove

Unmerged revisions

48. By Duncan McGreggor

Fixed typo.

47. By Duncan McGreggor

Fixed some formatting.

46. By Duncan McGreggor

Fixed pyflakes.

45. By Duncan McGreggor

Added unit tests for Time class.

44. By Duncan McGreggor

Removed unnecessary code.

43. By Duncan McGreggor

Removed the dependenty upon Epsilon by copying the extime module into the txAWS
util module.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'txaws/storage/client.py'
2--- txaws/storage/client.py 2009-09-05 01:13:43 +0000
3+++ txaws/storage/client.py 2009-11-05 04:20:23 +0000
4@@ -7,13 +7,11 @@
5 functionality in this wrapper.
6 """
7
8-from epsilon.extime import Time
9-
10 from twisted.web.client import getPage
11 from twisted.web.http import datetimeToString
12
13 from txaws.service import AWSServiceEndpoint
14-from txaws.util import XML, calculate_md5
15+from txaws.util import Time, XML, calculate_md5
16
17
18 class S3Request(object):
19
20=== modified file 'txaws/storage/tests/test_client.py'
21--- txaws/storage/tests/test_client.py 2009-09-05 01:13:43 +0000
22+++ txaws/storage/tests/test_client.py 2009-11-05 04:20:23 +0000
23@@ -1,15 +1,12 @@
24 from datetime import datetime
25
26-from epsilon.extime import Time
27-
28 from twisted.internet.defer import succeed
29
30 from txaws.credentials import AWSCredentials
31 from txaws.service import AWSServiceEndpoint
32 from txaws.storage.client import S3, S3Request
33 from txaws.testing.base import TXAWSTestCase
34-from txaws.util import calculate_md5
35-
36+from txaws.util import FixedOffset, calculate_md5
37
38
39 class StubbedS3Request(S3Request):
40@@ -219,14 +216,14 @@
41 self.assertEqual(req.object_name, None)
42
43 def _check_result(buckets):
44+ time1 = datetime(2006, 2, 3, 16, 45, 9, 0, FixedOffset(0, 0))
45+ time2 = datetime(2006, 2, 3, 16, 41, 58, 0, FixedOffset(0, 0))
46 self.assertEqual(
47 list(buckets),
48 [{"name": u"quotes",
49- "created": Time.fromDatetime(
50- datetime(2006, 2, 3, 16, 45, 9))},
51+ "created": time1},
52 {"name": u"samples",
53- "created": Time.fromDatetime(
54- datetime(2006, 2, 3, 16, 41, 58))}])
55+ "created": time2}])
56 return d.addCallback(_check_result)
57
58 def test_create_bucket(self):
59
60=== modified file 'txaws/tests/test_util.py'
61--- txaws/tests/test_util.py 2009-09-05 00:26:12 +0000
62+++ txaws/tests/test_util.py 2009-11-05 04:20:23 +0000
63@@ -1,8 +1,9 @@
64+from datetime import datetime, timedelta, tzinfo
65 from urlparse import urlparse
66
67-from twisted.trial.unittest import TestCase
68+from twisted.trial.unittest import FailTest, TestCase
69
70-from txaws.util import hmac_sha1, iso8601time, parse
71+from txaws.util import FixedOffset, Time, hmac_sha1, iso8601time, parse
72
73 class MiscellaneousTests(TestCase):
74
75@@ -70,3 +71,109 @@
76 self.assertTrue(isinstance(scheme, str))
77 self.assertTrue(isinstance(host, str))
78 self.assertTrue(isinstance(path, str))
79+
80+
81+class TestTime(TestCase):
82+ class MST(tzinfo):
83+ def tzname(self, dt):
84+ return 'MST'
85+ def utcoffset(self, dt):
86+ return timedelta(hours = -7)
87+ def dst(self, dt):
88+ return timedelta(0)
89+
90+ class CET(tzinfo):
91+ def tzname(self, dt):
92+ return 'MST'
93+ def utcoffset(self, dt):
94+ return timedelta(hours = 1)
95+ def dst(self, dt):
96+ return timedelta(0)
97+
98+ reference = datetime(2004, 12, 6, 14, 15, 16)
99+ awareReference = datetime(
100+ 2004, 12, 6, 14, 15, 16, tzinfo=FixedOffset(0, 0))
101+
102+ def _createReference(self, reference=None):
103+ """
104+ Return a reference instance.
105+ """
106+ return Time.fromDatetime(reference or self.reference)
107+
108+ def test_insignificantTimezones(self):
109+ """
110+ Timezones should be insignificant when the resolution is >= 1 day
111+ """
112+ def testEqual(creator, input):
113+ self.assertEquals(
114+ creator(input), creator(input, tzinfo=self.MST()))
115+
116+ def testNotEqual(creator, input):
117+ self.assertNotEquals(
118+ creator(input), creator(input, tzinfo=self.MST()))
119+
120+ testEqual(Time.fromISO8601TimeAndDate, '2005')
121+ testEqual(Time.fromISO8601TimeAndDate, '2005-02')
122+ testEqual(Time.fromISO8601TimeAndDate, '2005-02-10')
123+
124+ testNotEqual(Time.fromISO8601TimeAndDate, '2005-02-10T12')
125+ testNotEqual(Time.fromISO8601TimeAndDate, '2005-02-10T12:10')
126+ testNotEqual(Time.fromISO8601TimeAndDate, '2005-02-10T12:10:03')
127+
128+ def test_fromISO8601DateAndTime(self):
129+ self.assertRaises(ValueError, Time.fromISO8601TimeAndDate, '2005-W53' )
130+ self.assertRaises(ValueError, Time.fromISO8601TimeAndDate, '2004-367' )
131+ try:
132+ Time.fromISO8601TimeAndDate('2004-366')
133+ except ValueError:
134+ raise FailTest, 'leap years should have 366 days'
135+
136+ try:
137+ Time.fromISO8601TimeAndDate('2004-123T14-0600')
138+ Time.fromISO8601TimeAndDate('2004-123T14:13-0600')
139+ Time.fromISO8601TimeAndDate('2004-123T14:13:51-0600')
140+ except ValueError:
141+ raise FailTest('timezone should be allowed if time with'
142+ '*any* resolution is specified')
143+
144+ def test_fromDatetime(self):
145+ self.assertEquals(
146+ repr(Time.fromDatetime(datetime(2004, 12, 6, 14, 15, 16))),
147+ "Time(2004, 12, 6, 14, 15, 16, 0, 341, 0)")
148+ self.assertEquals(
149+ repr(Time.fromDatetime(datetime(
150+ 2004, 12, 6, 7, 15, 16, tzinfo=self.MST()))),
151+ "Time(2004, 12, 6, 14, 15, 16, 0, 341, 0)")
152+ self.assertEquals(
153+ repr(Time.fromDatetime(datetime(
154+ 2004, 12, 6, 15, 15, 16, tzinfo=self.CET()))),
155+ "Time(2004, 12, 6, 14, 15, 16, 0, 341, 0)")
156+
157+ def test_asDatetime(self):
158+ self.assertEquals(
159+ self._createReference().asDatetime(), self.awareReference)
160+ self.assertEquals(
161+ self._createReference().asDatetime(tzinfo=self.MST()),
162+ self.awareReference )
163+ self.assertEquals(
164+ self._createReference().asDatetime(tzinfo=self.CET()),
165+ self.awareReference)
166+
167+ def test_arithmetic(self):
168+ """
169+ Verify that L{timedelta} objects can be added to and subtracted from
170+ L{Time} instances and that L{Time} instances can be subtracted from
171+ each other.
172+ """
173+ time1 = Time.fromISO8601TimeAndDate('2004-12-03T14:15:16')
174+ time2 = Time.fromISO8601TimeAndDate('2004-12-09T14:15:16')
175+ offset = timedelta(days=6)
176+
177+ # Supported operations
178+ self.assertEqual(time1 + offset, time2)
179+ self.assertEqual(time2 - offset, time1)
180+ self.assertEqual(time2 - time1, offset)
181+
182+ # Make sure unsupported types give back a TypeError
183+ self.assertRaises(TypeError, lambda: time1 + 1)
184+ self.assertRaises(TypeError, lambda: time1 - 1)
185
186=== modified file 'txaws/util.py'
187--- txaws/util.py 2009-10-21 19:40:18 +0000
188+++ txaws/util.py 2009-11-05 04:20:23 +0000
189@@ -4,11 +4,13 @@
190 services.
191 """
192
193+import datetime
194+import hmac
195+import re
196+import time
197 from base64 import b64encode
198 from hashlib import sha1, md5, sha256
199-import hmac
200 from urlparse import urlparse, urlunparse
201-import time
202
203 # Import XMLTreeBuilder from somewhere; here in one place to prevent
204 # duplication.
205@@ -18,7 +20,8 @@
206 from elementtree.ElementTree import XMLTreeBuilder
207
208
209-__all__ = ["hmac_sha1", "hmac_sha256", "iso8601time", "calculate_md5", "XML"]
210+__all__ = [
211+ "hmac_sha1", "hmac_sha256", "iso8601time", "calculate_md5", "Time", "XML"]
212
213
214 def calculate_md5(data):
215@@ -94,3 +97,472 @@
216 if path == "":
217 path = "/"
218 return scheme, host, port, path
219+
220+
221+def _timedeltaToSignHrMin(offset):
222+ """
223+ Return a (sign, hour, minute) triple for the offset described by timedelta.
224+
225+ sign is a string, either "+" or "-". In the case of 0 offset, sign is "+".
226+ """
227+ minutes = round((offset.days * 3600000000 * 24
228+ + offset.seconds * 1000000
229+ + offset.microseconds)
230+ / 60000000.0)
231+ if minutes < 0:
232+ sign = '-'
233+ minutes = -minutes
234+ else:
235+ sign = '+'
236+ return (sign, minutes // 60, minutes % 60)
237+
238+
239+def _timedeltaToSeconds(offset):
240+ """
241+ Convert a datetime.timedelta instance to simply a number of seconds.
242+
243+ For example, you can specify purely second intervals with timedelta's
244+ constructor:
245+
246+ >>> td = datetime.timedelta(seconds=99999999)
247+
248+ but then you can't get them out again:
249+
250+ >>> td.seconds
251+ 35199
252+
253+ This allows you to:
254+
255+ >>> import txaws.util
256+ >>> txaws.util._timedeltaToSeconds(td)
257+ 99999999.0
258+
259+ @param offset: a L{datetime.timedelta} representing an interval that we
260+ wish to know the total number of seconds for.
261+
262+ @return: a number of seconds
263+ @rtype: float
264+ """
265+ return ((offset.days * 60*60*24) +
266+ (offset.seconds) +
267+ (offset.microseconds * 1e-6))
268+
269+
270+class FixedOffset(datetime.tzinfo):
271+ _zeroOffset = datetime.timedelta()
272+
273+ def __init__(self, hours, minutes):
274+ self.offset = datetime.timedelta(minutes = hours * 60 + minutes)
275+
276+ def utcoffset(self, dt):
277+ return self.offset
278+
279+ def tzname(self, dt):
280+ return _timedeltaToSignHrMin(self.offset)
281+
282+ def dst(self, tz):
283+ return self._zeroOffset
284+
285+ def __repr__(self):
286+ return '<%s.%s object at 0x%x offset %r>' % (
287+ self.__module__, type(self).__name__, id(self), self.offset)
288+
289+
290+class Time(object):
291+ """An object representing a well defined instant in time.
292+
293+ A Time object unambiguously addresses some time, independent of timezones,
294+ contorted base-60 counting schemes, leap seconds, and the effects of
295+ general relativity. It provides methods for returning a representation of
296+ this time in various ways that a human or a programmer might find more
297+ useful in various applications.
298+
299+ Every Time instance has an attribute 'resolution'. This can be ignored, or
300+ the instance can be considered to address a span of time. This resolution
301+ is determined by the value used to initalize the instance, or the
302+ resolution of the internal representation, whichever is greater. It is
303+ mostly useful when using input formats that allow the specification of
304+ whole days or weeks. For example, ISO 8601 allows one to state a time as,
305+ "2005-W03", meaning "the third week of 2005". In this case the resolution
306+ is set to one week. Other formats are considered to express only an instant
307+ in time, such as a POSIX timestamp, because the resolution of the time is
308+ limited only by the hardware's representation of a real number.
309+
310+ Timezones are significant only for instances with a resolution greater than
311+ one day. When the timezone is insignificant, the result of methods like
312+ asISO8601TimeAndDate is the same for any given tzinfo parameter. Sort order
313+ is determined by the start of the period in UTC. For example, "today" sorts
314+ after "midnight today, central Europe", and before "midnight today, US
315+ Eastern". For applications that need to store a mix of timezone dependent
316+ and independent instances, it may be wise to store them separately, since
317+ the time between the start and end of today in the local timezone may not
318+ include the start of today in UTC, and thus not independent instances
319+ addressing the whole day. In other words, the desired sort order (the one
320+ where just "Monday" sorts before any more precise time in "Monday", and
321+ after any in "Sunday") of Time instances is dependant on the timezone
322+ context.
323+
324+ Date arithmetic and boolean operations operate on instants in time, not
325+ periods. In this case, the start of the period is used as the value, and
326+ the result has a resolution of 0.
327+
328+ For containment tests with the 'in' operator, the period addressed by the
329+ instance is used.
330+
331+ The methods beginning with 'from' are constructors to create instances from
332+ various formats. Some of them are textual formats, and others are other
333+ time types commonly found in Python code.
334+
335+ Likewise, methods beginning with 'as' return the represented time in
336+ various formats. Some of these methods should try to reflect the resolution
337+ of the instance. However, they don't yet.
338+
339+ For formats with both a constructor and a formatter, d == fromFu(d.asFu())
340+
341+ @type resolution: datetime.timedelta
342+ @ivar resolution: the length of the period to which this instance could
343+ refer. For example, "Today, 13:38" could refer to any time between 13:38
344+ until but not including 13:39. In this case resolution would be
345+ timedelta(minutes=1).
346+ """
347+
348+ # the instance variable _time is the internal representation of time. It
349+ # is a naive datetime object which is always UTC. A UTC tzinfo would be
350+ # great, if one existed, and anyway it complicates pickling.
351+
352+ rfc2822Weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
353+
354+ rfc2822Months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug',
355+ 'Sep', 'Oct', 'Nov', 'Dec']
356+
357+ resolution = datetime.timedelta.resolution
358+
359+ def __init__(self):
360+ """Return a new Time instance representing the time now.
361+
362+ See also the fromFu methods to create new instances from other types of
363+ initializers.
364+ """
365+ self._time = datetime.datetime.utcnow()
366+
367+ iso8601pattern = re.compile(r"""
368+ ^ (?P<year> \d{4})
369+ (
370+ # a year may optionally be followed by one of:
371+ # - a month
372+ # - a week
373+ # - a specific day, and an optional time
374+ # a specific day is one of:
375+ # - a month and day
376+ # - week and weekday
377+ # - a day of the year
378+ (
379+ -? (?P<month1> \d{2})
380+ |
381+ -? W (?P<week1> \d{2})
382+ |
383+ (
384+ -? (?P<month2> \d{2})
385+ -? (?P<day> \d{2})
386+ |
387+ -? W (?P<week2> \d{2})
388+ -? (?P<weekday> \d)
389+ |
390+ -? (?P<dayofyear> \d{3})
391+ )
392+ (
393+ T (?P<hour> \d{2})
394+ (
395+ :? (?P<minute> \d{2})
396+ (
397+ :? (?P<second> \d{2})
398+ (
399+ [\.,] (?P<fractionalsec> \d+)
400+ )?
401+ )?
402+ )?
403+ (
404+ (?P<zulu> Z)
405+ |
406+ (?P<tzhour> [+\-]\d{2})
407+ (
408+ :? (?P<tzmin> \d{2})
409+ )?
410+ )?
411+ )?
412+ )?
413+ )? $""", re.VERBOSE)
414+
415+ def fromISO8601TimeAndDate(klass, iso8601string, tzinfo=None):
416+ """Return a new Time instance from a string formated as in ISO 8601.
417+
418+ If the given string contains no timezone, it is assumed to be in the
419+ timezone specified by the parameter `tzinfo`, or UTC if tzinfo is None.
420+ An input string with an explicit timezone will always override tzinfo.
421+
422+ If the given iso8601string does not contain all parts of the time, they
423+ will default to 0 in the timezone given by `tzinfo`.
424+
425+ WARNING: this function is incomplete. ISO is dumb and their standards
426+ are not free. Only a subset of all valid ISO 8601 dates are parsed,
427+ because I can't find a formal description of the format. However,
428+ common ones should work.
429+ """
430+
431+ def calculateTimezone():
432+ if groups['zulu'] == 'Z':
433+ return FixedOffset(0, 0)
434+ else:
435+ tzhour = groups.pop('tzhour')
436+ tzmin = groups.pop('tzmin')
437+ if tzhour is not None:
438+ return FixedOffset(int(tzhour), int(tzmin or 0))
439+ return tzinfo or FixedOffset(0, 0)
440+
441+ def coerceGroups():
442+ groups['month'] = groups['month1'] or groups['month2']
443+ groups['week'] = groups['week1'] or groups['week2']
444+ # don't include fractional seconds, because it's not an integer.
445+ defaultTo0 = ['hour', 'minute', 'second']
446+ defaultTo1 = ['month', 'day', 'week', 'weekday', 'dayofyear']
447+ if groups['fractionalsec'] is None:
448+ groups['fractionalsec'] = '0'
449+ for key in defaultTo0:
450+ if groups[key] is None:
451+ groups[key] = 0
452+ for key in defaultTo1:
453+ if groups[key] is None:
454+ groups[key] = 1
455+ groups['fractionalsec'] = float('.'+groups['fractionalsec'])
456+ for key in defaultTo0 + defaultTo1 + ['year']:
457+ groups[key] = int(groups[key])
458+
459+ for group, min, max in [
460+ # some years have only 52 weeks
461+ ('week', 1, 53),
462+ ('weekday', 1, 7),
463+ ('month', 1, 12),
464+ ('day', 1, 31),
465+ ('hour', 0, 24),
466+ ('minute', 0, 59),
467+
468+ # Sometime in the 22nd century AD, two leap seconds will be
469+ # required every year. In the 25th century AD, four every
470+ # year. We'll ignore that for now though because it would be
471+ # tricky to get right and we certainly don't need it for our
472+ # target applications. In other words, post-singularity
473+ # Martian users, please do not rely on this code for
474+ # compatibility with Greater Galactic Protectorate of Earth
475+ # date/time formatting! Apologies, but no library I know of in
476+ # Python is sufficient for processing their dates and times
477+ # without ADA bindings to get the radiation-safety zone counter
478+ # correct. -glyph
479+
480+ ('second', 0, 61),
481+ # don't forget leap years
482+ ('dayofyear', 1, 366)]:
483+ if not min <= groups[group] <= max:
484+ raise ValueError, '%s must be in %i..%i' % (group, min, max)
485+
486+ def determineResolution():
487+ if match.group('fractionalsec') is not None:
488+ return max(datetime.timedelta.resolution,
489+ datetime.timedelta(
490+ microseconds=1 * 10 ** -len(
491+ match.group('fractionalsec')) * 1000000))
492+
493+ for testGroup, resolution in [
494+ ('second', datetime.timedelta(seconds=1)),
495+ ('minute', datetime.timedelta(minutes=1)),
496+ ('hour', datetime.timedelta(hours=1)),
497+ ('weekday', datetime.timedelta(days=1)),
498+ ('dayofyear', datetime.timedelta(days=1)),
499+ ('day', datetime.timedelta(days=1)),
500+ ('week1', datetime.timedelta(weeks=1)),
501+ ('week2', datetime.timedelta(weeks=1))]:
502+ if match.group(testGroup) is not None:
503+ return resolution
504+
505+ if match.group('month1') is not None \
506+ or match.group('month2') is not None:
507+ if self._time.month == 12:
508+ return datetime.timedelta(days=31)
509+ nextMonth = self._time.replace(month=self._time.month+1)
510+ return nextMonth - self._time
511+ else:
512+ nextYear = self._time.replace(year=self._time.year+1)
513+ return nextYear - self._time
514+
515+ def calculateDtime(tzinfo):
516+ """Calculate a datetime for the start of the addressed period."""
517+
518+ if match.group('week1') is not None \
519+ or match.group('week2') is not None:
520+ if not 0 < groups['week'] <= 53:
521+ raise ValueError(
522+ 'week must be in 1..53 (was %i)' % (groups['week'],))
523+ dtime = datetime.datetime(
524+ groups['year'],
525+ 1,
526+ 4,
527+ groups['hour'],
528+ groups['minute'],
529+ groups['second'],
530+ int(round(groups['fractionalsec'] * 1000000)),
531+ tzinfo=tzinfo
532+ )
533+ dtime -= datetime.timedelta(days = dtime.weekday())
534+ dtime += datetime.timedelta(
535+ days = (groups['week']-1) * 7 + groups['weekday'] - 1)
536+ if dtime.isocalendar() != (
537+ groups['year'], groups['week'], groups['weekday']):
538+ # actually the problem could be an error in my logic, but
539+ # nothing should cause this but requesting week 53 of a
540+ # year with 52 weeks.
541+ raise ValueError('year %04i has no week %02i' %
542+ (groups['year'], groups['week']))
543+ return dtime
544+
545+ if match.group('dayofyear') is not None:
546+ dtime = datetime.datetime(
547+ groups['year'],
548+ 1,
549+ 1,
550+ groups['hour'],
551+ groups['minute'],
552+ groups['second'],
553+ int(round(groups['fractionalsec'] * 1000000)),
554+ tzinfo=tzinfo
555+ )
556+ dtime += datetime.timedelta(days=groups['dayofyear']-1)
557+ if dtime.year != groups['year']:
558+ raise ValueError(
559+ 'year %04i has no day of year %03i' %
560+ (groups['year'], groups['dayofyear']))
561+ return dtime
562+
563+ else:
564+ return datetime.datetime(
565+ groups['year'],
566+ groups['month'],
567+ groups['day'],
568+ groups['hour'],
569+ groups['minute'],
570+ groups['second'],
571+ int(round(groups['fractionalsec'] * 1000000)),
572+ tzinfo=tzinfo
573+ )
574+
575+ match = klass.iso8601pattern.match(iso8601string)
576+ if match is None:
577+ raise ValueError(
578+ '%r could not be parsed as an ISO 8601 date and time' %
579+ (iso8601string,))
580+
581+ groups = match.groupdict()
582+ coerceGroups()
583+ if match.group('hour') is not None:
584+ timezone = calculateTimezone()
585+ else:
586+ timezone = None
587+ self = klass.fromDatetime(calculateDtime(timezone))
588+ self.resolution = determineResolution()
589+ return self.asDatetime()
590+
591+ fromISO8601TimeAndDate = classmethod(fromISO8601TimeAndDate)
592+
593+ def fromDatetime(klass, dtime):
594+ """Return a new Time instance from a datetime.datetime instance.
595+
596+ If the datetime instance does not have an associated timezone, it is
597+ assumed to be UTC.
598+ """
599+ self = klass.__new__(klass)
600+ if dtime.tzinfo is not None:
601+ self._time = dtime.astimezone(FixedOffset(0, 0)).replace(tzinfo=None)
602+ else:
603+ self._time = dtime
604+ self.resolution = datetime.timedelta.resolution
605+ return self
606+
607+ fromDatetime = classmethod(fromDatetime)
608+
609+ def asDatetime(self, tzinfo=None):
610+ """Return this time as an aware datetime.datetime instance.
611+
612+ The returned datetime object has the specified tzinfo, or a tzinfo
613+ describing UTC if the tzinfo parameter is None.
614+ """
615+ if tzinfo is None:
616+ tzinfo = FixedOffset(0, 0)
617+
618+ if not self.isTimezoneDependent():
619+ return self._time.replace(tzinfo=tzinfo)
620+ else:
621+ return self._time.replace(tzinfo=FixedOffset(0, 0)).astimezone(tzinfo)
622+
623+ def isTimezoneDependent(self):
624+ """Return True iff timezone is relevant for this instance.
625+
626+ Timezone is only relevent for instances with a resolution better than
627+ one day.
628+ """
629+ return self.resolution < datetime.timedelta(days=1)
630+
631+ def __cmp__(self, other):
632+ if not isinstance(other, Time):
633+ raise TypeError("Cannot meaningfully compare %r with %r" % (self, other))
634+ return cmp(self._time, other._time)
635+
636+ def __eq__(self, other):
637+ if isinstance(other, Time):
638+ return cmp(self._time, other._time) == 0
639+ return False
640+
641+ def __ne__(self, other):
642+ return not (self == other)
643+
644+ def __repr__(self):
645+ return "%s%s" % (
646+ self.__class__.__name__, self.asDatetime().timetuple())
647+
648+ def __contains__(self, other):
649+ """Test if another Time instance is entirely within the period addressed by this one."""
650+ if not isinstance(other, Time):
651+ raise TypeError(
652+ '%r is not a Time instance; can not test for containment'
653+ % (other,))
654+ if other._time < self._time:
655+ return False
656+ if self._time + self.resolution < other._time + other.resolution:
657+ return False
658+ return True
659+
660+ def __add__(self, addend):
661+ if not isinstance(addend, datetime.timedelta):
662+ raise TypeError, 'expected a datetime.timedelta instance'
663+ return Time.fromDatetime(self._time + addend)
664+
665+ def __sub__(self, subtrahend):
666+ """
667+ Implement subtraction of an interval or another time from this one.
668+
669+ @type subtrahend: L{datetime.timedelta} or L{Time}
670+
671+ @param subtrahend: The object to be subtracted from this one.
672+
673+ @rtype: L{datetime.timedelta} or L{Time}
674+
675+ @return: If C{subtrahend} is a L{datetime.timedelta}, the result is
676+ a L{Time} instance which is offset from this one by that amount. If
677+ C{subtrahend} is a L{Time}, the result is a L{datetime.timedelta}
678+ instance which gives the difference between it and this L{Time}
679+ instance.
680+ """
681+ if isinstance(subtrahend, datetime.timedelta):
682+ return Time.fromDatetime(self._time - subtrahend)
683+
684+ if isinstance(subtrahend, Time):
685+ return self.asDatetime() - subtrahend.asDatetime()
686+
687+ return NotImplemented

Subscribers

People subscribed via source and target branches

to all changes: