Skip to content

Commit 9ea5ac8

Browse files
author
Tim Visher
committed
Deprecate utils.strptime
Motivation ---------- This appears to have been deprecated for some time and is poorly behaved in the case of fractional seconds. `utils.strptime_to_utc` is preferred anyway. See singer-io#81 Implementation Notes -------------------- - Move to circle 2.0 - Add doctest to the nose runner - Add a docstring and warn call to utils.strptime
1 parent 3ddbcc6 commit 9ea5ac8

10 files changed

Lines changed: 119 additions & 86 deletions

File tree

.circleci/config.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
version: 2
2+
jobs:
3+
build:
4+
docker:
5+
- image: ubuntu:16.04
6+
steps:
7+
- checkout
8+
- run:
9+
name: 'Install python 3.5.2'
10+
command: |
11+
apt update
12+
apt install --yes python3 python3-pip python3-venv
13+
- run:
14+
name: 'Setup virtualenv'
15+
command: |
16+
mkdir -p ~/.virtualenvs
17+
python3 -m venv ~/.virtualenvs/singer-python
18+
source ~/.virtualenvs/singer-python/bin/activate
19+
pip install -U pip setuptools
20+
make install
21+
- run:
22+
name: 'Run tests'
23+
command: |
24+
# Need to re-activate the virtualenv
25+
source ~/.virtualenvs/singer-python/bin/activate
26+
make test

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
check_prereqs:
44
bash -c '[[ -n $$VIRTUAL_ENV ]]'
5-
bash -c '[[ $$(python3 --version) == *3.4.3* ]]'
5+
bash -c '[[ $$(python3 --version) == *3.5.2* ]]'
66

77
install: check_prereqs
88
python3 -m pip install -e '.[dev]'
99

1010
test: install
1111
pylint singer -d missing-docstring,broad-except,bare-except,too-many-return-statements,too-many-branches,too-many-arguments,no-else-return,too-few-public-methods,fixme,protected-access
12-
nosetests -v
12+
nosetests --with-doctest -v

bin/circle_deps

Lines changed: 0 additions & 13 deletions
This file was deleted.

bin/circle_test

Lines changed: 0 additions & 9 deletions
This file was deleted.

circle.yml

Lines changed: 0 additions & 7 deletions
This file was deleted.

setup.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
extras_require={
2020
'dev': [
2121
'pylint',
22+
'ipython',
23+
'ipdb',
2224
'nose',
2325
'singer-tools'
2426
]

singer/messages.py

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@ class RecordMessage(Message):
3535
number. Note that this feature is experimental and most Taps and
3636
Targets should not need to use versioned streams.
3737
38-
>>> msg = singer.RecordMessage(
39-
>>> stream='users',
40-
>>> record={'id': 1, 'name': 'Mary'})
38+
msg = singer.RecordMessage(
39+
stream='users',
40+
record={'id': 1, 'name': 'Mary'})
4141
4242
'''
4343

@@ -76,15 +76,15 @@ class SchemaMessage(Message):
7676
* schema (dict) - The JSON schema.
7777
* key_properties (list of strings) - List of primary key properties.
7878
79-
>>> msg = singer.SchemaMessage(
80-
>>> stream='users',
81-
>>> schema={'type': 'object',
82-
>>> 'properties': {
83-
>>> 'id': {'type': 'integer'},
84-
>>> 'name': {'type': 'string'}
85-
>>> }
86-
>>> },
87-
>>> key_properties=['id'])
79+
msg = singer.SchemaMessage(
80+
stream='users',
81+
schema={'type': 'object',
82+
'properties': {
83+
'id': {'type': 'integer'},
84+
'name': {'type': 'string'}
85+
}
86+
},
87+
key_properties=['id'])
8888
8989
'''
9090
def __init__(self, stream, schema, key_properties, bookmark_properties=None):
@@ -118,8 +118,8 @@ class StateMessage(Message):
118118
119119
* value (dict) - The value of the state.
120120
121-
>>> msg = singer.StateMessage(
122-
>>> value={'users': '2017-06-19T00:00:00'})
121+
msg = singer.StateMessage(
122+
value={'users': '2017-06-19T00:00:00'})
123123
124124
'''
125125
def __init__(self, value):
@@ -148,9 +148,9 @@ class ActivateVersionMessage(Message):
148148
not need to use the "version" field of "RECORD" messages or the
149149
"ACTIVATE_VERSION" message at all.
150150
151-
>>> msg = singer.ActivateVersionMessage(
152-
>>> stream='users',
153-
>>> version=2)
151+
msg = singer.ActivateVersionMessage(
152+
stream='users',
153+
version=2)
154154
155155
'''
156156
def __init__(self, stream, version):
@@ -221,7 +221,7 @@ def write_message(message):
221221
def write_record(stream_name, record, stream_alias=None, time_extracted=None):
222222
"""Write a single record for the given stream.
223223
224-
>>> write_record("users", {"id": 2, "email": "mike@stitchdata.com"})
224+
write_record("users", {"id": 2, "email": "mike@stitchdata.com"})
225225
"""
226226
write_message(RecordMessage(stream=(stream_alias or stream_name),
227227
record=record,
@@ -231,9 +231,9 @@ def write_record(stream_name, record, stream_alias=None, time_extracted=None):
231231
def write_records(stream_name, records):
232232
"""Write a list of records for the given stream.
233233
234-
>>> chris = {"id": 1, "email": "chris@stitchdata.com"}
235-
>>> mike = {"id": 2, "email": "mike@stitchdata.com"}
236-
>>> write_records("users", [chris, mike])
234+
chris = {"id": 1, "email": "chris@stitchdata.com"}
235+
mike = {"id": 2, "email": "mike@stitchdata.com"}
236+
write_records("users", [chris, mike])
237237
"""
238238
for record in records:
239239
write_record(stream_name, record)
@@ -242,10 +242,10 @@ def write_records(stream_name, records):
242242
def write_schema(stream_name, schema, key_properties, bookmark_properties=None, stream_alias=None):
243243
"""Write a schema message.
244244
245-
>>> stream = 'test'
246-
>>> schema = {'properties': {'id': {'type': 'integer'}, 'email': {'type': 'string'}}} # nopep8
247-
>>> key_properties = ['id']
248-
>>> write_schema(stream, schema, key_properties)
245+
stream = 'test'
246+
schema = {'properties': {'id': {'type': 'integer'}, 'email': {'type': 'string'}}} # nopep8
247+
key_properties = ['id']
248+
write_schema(stream, schema, key_properties)
249249
"""
250250
if isinstance(key_properties, (str, bytes)):
251251
key_properties = [key_properties]
@@ -263,16 +263,16 @@ def write_schema(stream_name, schema, key_properties, bookmark_properties=None,
263263
def write_state(value):
264264
"""Write a state message.
265265
266-
>>> write_state({'last_updated_at': '2017-02-14T09:21:00'})
266+
write_state({'last_updated_at': '2017-02-14T09:21:00'})
267267
"""
268268
write_message(StateMessage(value=value))
269269

270270

271271
def write_version(stream_name, version):
272272
"""Write an activate version message.
273273
274-
>>> stream = 'test'
275-
>>> version = int(time.time())
276-
>>> write_version(stream, version)
274+
stream = 'test'
275+
version = int(time.time())
276+
write_version(stream, version)
277277
"""
278278
write_message(ActivateVersionMessage(stream_name, version))

singer/metrics.py

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@
1010
since the last time it reported. For example, to increment a record count
1111
for records from a "users" endpoint, you could do:
1212
13-
>>> with Counter('record_count', {'endpoint': 'users'}) as counter:
14-
>>> for record in my_records:
15-
>>> # Do stuff...
16-
>>> counter.increment()
13+
with Counter('record_count', {'endpoint': 'users'}) as counter:
14+
for record in my_records:
15+
# Do stuff...
16+
counter.increment()
1717
1818
Timer is class that allows you to track the timing of operations. Like
1919
Counter, you initialize it as a context manager, with a metric name and a
@@ -22,8 +22,8 @@
2222
automatically include a tag called "status" that is set to "failed" if an
2323
Exception was raised, or "succeeded" otherwise.
2424
25-
>>> with Timer('http_request_duration', {'endpoint': 'users'}):
26-
>>> # Make a request, do some things
25+
with Timer('http_request_duration', {'endpoint': 'users'}):
26+
# Make a request, do some things
2727
2828
In order to encourage consistent metric and tag names, this module
2929
provides several functions for creating Counters and Timers for very
@@ -95,10 +95,10 @@ class Counter():
9595
exits. The only thing you need to do is initialize the Counter and
9696
then call increment().
9797
98-
>>> with singer.metrics.Counter('record_count', {'endpoint': 'users'}) as counter:
99-
>>> for user in get_users(...):
100-
>>> # Print out the user
101-
>>> counter.increment()
98+
with singer.metrics.Counter('record_count', {'endpoint': 'users'}) as counter:
99+
for user in get_users(...):
100+
# Print out the user
101+
counter.increment()
102102
103103
This would print a metric like this:
104104
@@ -154,8 +154,8 @@ class Timer(): # pylint: disable=too-few-public-methods
154154
context exits with an Exception or "success" if it exits cleanly. You
155155
can override this by setting timer.status within the context.
156156
157-
>>> with singer.metrics.Timer('request_duration', {'endpoint': 'users'}):
158-
>>> # Do some stuff
157+
with singer.metrics.Timer('request_duration', {'endpoint': 'users'}):
158+
# Do some stuff
159159
160160
This produces a metric like this:
161161
@@ -196,10 +196,10 @@ def __exit__(self, exc_type, exc_value, traceback):
196196
def record_counter(endpoint=None, log_interval=DEFAULT_LOG_INTERVAL):
197197
'''Use for counting records retrieved from the source.
198198
199-
>>> with singer.metrics.record_counter(endpoint="users") as counter:
200-
>>> for record in my_records:
201-
>>> # Do something with the record
202-
>>> counter.increment()
199+
with singer.metrics.record_counter(endpoint="users") as counter:
200+
for record in my_records:
201+
# Do something with the record
202+
counter.increment()
203203
'''
204204
tags = {}
205205
if endpoint:
@@ -210,8 +210,8 @@ def record_counter(endpoint=None, log_interval=DEFAULT_LOG_INTERVAL):
210210
def http_request_timer(endpoint):
211211
'''Use for timing HTTP requests to an endpoint
212212
213-
>>> with singer.metrics.http_request_timer("users") as timer:
214-
>>> # Make a request
213+
with singer.metrics.http_request_timer("users") as timer:
214+
# Make a request
215215
'''
216216
tags = {}
217217
if endpoint:
@@ -222,8 +222,8 @@ def http_request_timer(endpoint):
222222
def job_timer(job_type=None):
223223
'''Use for timing asynchronous jobs
224224
225-
>>> with singer.metrics.job_timer(job_type="users") as timer:
226-
>>> # Make a request
225+
with singer.metrics.job_timer(job_type="users") as timer:
226+
# Make a request
227227
'''
228228
tags = {}
229229
if job_type:

singer/utils.py

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import functools
55
import json
66
import time
7+
from warnings import warn
8+
79
import dateutil.parser
810
import pytz
911
import backoff as backoff_module
@@ -25,10 +27,35 @@ def strptime_with_tz(dtime):
2527
return d_object
2628

2729
def strptime(dtime):
28-
try:
29-
return datetime.datetime.strptime(dtime, DATETIME_FMT)
30-
except Exception:
31-
return datetime.datetime.strptime(dtime, DATETIME_PARSE)
30+
"""DEPRECATED Use strptime_to_utc instead.
31+
32+
Parse DTIME according to DATETIME_PARSE without TZ safety.
33+
34+
>>> strptime("2018-01-01T00:00:00Z")
35+
datetime.datetime(2018, 1, 1, 0, 0)
36+
37+
Requires the Z TZ signifier
38+
>>> strptime("2018-01-01T00:00:00")
39+
Traceback (most recent call last):
40+
...
41+
ValueError: time data '2018-01-01T00:00:00' does not match format '%Y-%m-%dT%H:%M:%SZ'
42+
43+
Can't parse non-UTC DTs
44+
>>> strptime("2018-01-01T00:00:00-04:00")
45+
Traceback (most recent call last):
46+
...
47+
ValueError: time data '2018-01-01T00:00:00-04:00' does not match format '%Y-%m-%dT%H:%M:%SZ'
48+
49+
Does not support fractional seconds
50+
>>> strptime("2018-01-01T00:00:00.000000Z")
51+
Traceback (most recent call last):
52+
...
53+
ValueError: time data '2018-01-01T00:00:00.000000Z' does not match format '%Y-%m-%dT%H:%M:%SZ'
54+
"""
55+
56+
warn("Use strptime_to_utc instead", DeprecationWarning, stacklevel=2)
57+
58+
return datetime.datetime.strptime(dtime, DATETIME_PARSE)
3259

3360
def strptime_to_utc(dtimestr):
3461
d_object = dateutil.parser.parse(dtimestr)

tests/test_utils.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
import unittest
22
from datetime import datetime as dt
3-
from datetime import timezone as tz
3+
import pytz
44
import logging
55
import singer.utils as u
66

77

88
class TestFormat(unittest.TestCase):
99
def test_small_years(self):
10-
self.assertEqual(u.strftime(dt(90, 1, 1, tzinfo=tz.utc)),
10+
self.assertEqual(u.strftime(dt(90, 1, 1, tzinfo=pytz.UTC)),
1111
"0090-01-01T00:00:00.000000Z")
1212

13+
def test_round_trip(self):
14+
now = dt.utcnow().replace(tzinfo=pytz.UTC)
15+
dtime = u.strftime(now)
16+
pdtime = u.strptime_to_utc(dtime)
17+
fdtime = u.strftime(pdtime)
18+
self.assertEqual(dtime, fdtime)
19+
1320

1421
class TestHandleException(unittest.TestCase):
1522
def setUp(self):

0 commit comments

Comments
 (0)