Merge lp:~oubiwann/txaws/416366-remove-epsilon into lp:txaws
- 416366-remove-epsilon
- Merge into trunk
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 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Duncan McGreggor | Disapprove | ||
Review via email: mp+14447@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
Revision history for this message
Duncan McGreggor (oubiwann) wrote : | # |
- 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 |
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.