singer-python 6.1.0__tar.gz → 6.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- singer_python-6.2.0/PKG-INFO +27 -0
- {singer-python-6.1.0 → singer_python-6.2.0}/setup.cfg +1 -1
- {singer-python-6.1.0 → singer_python-6.2.0}/setup.py +1 -1
- {singer-python-6.1.0 → singer_python-6.2.0}/singer/catalog.py +1 -0
- {singer-python-6.1.0 → singer_python-6.2.0}/singer/schema.py +1 -1
- singer_python-6.2.0/singer/schema_generation.py +92 -0
- singer_python-6.2.0/singer_python.egg-info/PKG-INFO +27 -0
- {singer-python-6.1.0 → singer_python-6.2.0}/singer_python.egg-info/SOURCES.txt +2 -0
- {singer-python-6.1.0 → singer_python-6.2.0}/tests/test_catalog.py +3 -3
- {singer-python-6.1.0 → singer_python-6.2.0}/tests/test_exceptions.py +12 -12
- {singer-python-6.1.0 → singer_python-6.2.0}/tests/test_schema.py +14 -14
- singer_python-6.2.0/tests/test_schema_generation.py +76 -0
- {singer-python-6.1.0 → singer_python-6.2.0}/tests/test_transform.py +24 -24
- singer-python-6.1.0/PKG-INFO +0 -9
- singer-python-6.1.0/singer_python.egg-info/PKG-INFO +0 -9
- {singer-python-6.1.0 → singer_python-6.2.0}/LICENSE +0 -0
- {singer-python-6.1.0 → singer_python-6.2.0}/README.md +0 -0
- {singer-python-6.1.0 → singer_python-6.2.0}/singer/__init__.py +0 -0
- {singer-python-6.1.0 → singer_python-6.2.0}/singer/bookmarks.py +0 -0
- {singer-python-6.1.0 → singer_python-6.2.0}/singer/exceptions.py +0 -0
- {singer-python-6.1.0 → singer_python-6.2.0}/singer/logger.py +0 -0
- {singer-python-6.1.0 → singer_python-6.2.0}/singer/logging.conf +0 -0
- {singer-python-6.1.0 → singer_python-6.2.0}/singer/messages.py +0 -0
- {singer-python-6.1.0 → singer_python-6.2.0}/singer/metadata.py +0 -0
- {singer-python-6.1.0 → singer_python-6.2.0}/singer/metrics.py +0 -0
- {singer-python-6.1.0 → singer_python-6.2.0}/singer/requests.py +0 -0
- {singer-python-6.1.0 → singer_python-6.2.0}/singer/statediff.py +0 -0
- {singer-python-6.1.0 → singer_python-6.2.0}/singer/transform.py +0 -0
- {singer-python-6.1.0 → singer_python-6.2.0}/singer/utils.py +0 -0
- {singer-python-6.1.0 → singer_python-6.2.0}/singer_python.egg-info/dependency_links.txt +0 -0
- {singer-python-6.1.0 → singer_python-6.2.0}/singer_python.egg-info/requires.txt +0 -0
- {singer-python-6.1.0 → singer_python-6.2.0}/singer_python.egg-info/top_level.txt +0 -0
- {singer-python-6.1.0 → singer_python-6.2.0}/tests/__init__.py +0 -0
- {singer-python-6.1.0 → singer_python-6.2.0}/tests/test_bookmarks.py +0 -0
- {singer-python-6.1.0 → singer_python-6.2.0}/tests/test_metadata.py +0 -0
- {singer-python-6.1.0 → singer_python-6.2.0}/tests/test_metrics.py +0 -0
- {singer-python-6.1.0 → singer_python-6.2.0}/tests/test_singer.py +0 -0
- {singer-python-6.1.0 → singer_python-6.2.0}/tests/test_statediff.py +0 -0
- {singer-python-6.1.0 → singer_python-6.2.0}/tests/test_utils.py +0 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: singer-python
|
|
3
|
+
Version: 6.2.0
|
|
4
|
+
Summary: Singer.io utility library
|
|
5
|
+
Home-page: http://singer.io
|
|
6
|
+
Author: Stitch
|
|
7
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: pytz>=2018.4
|
|
10
|
+
Requires-Dist: jsonschema==2.*,>=2.6.0
|
|
11
|
+
Requires-Dist: simplejson==3.*,>=3.13.2
|
|
12
|
+
Requires-Dist: python-dateutil==2.*,>=2.7.3
|
|
13
|
+
Requires-Dist: backoff==2.*,>=2.2.1
|
|
14
|
+
Requires-Dist: ciso8601==2.*,>=2.3.1
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pylint; extra == "dev"
|
|
17
|
+
Requires-Dist: ipython; extra == "dev"
|
|
18
|
+
Requires-Dist: ipdb; extra == "dev"
|
|
19
|
+
Requires-Dist: nose; extra == "dev"
|
|
20
|
+
Requires-Dist: singer-tools; extra == "dev"
|
|
21
|
+
Dynamic: author
|
|
22
|
+
Dynamic: classifier
|
|
23
|
+
Dynamic: home-page
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
Dynamic: provides-extra
|
|
26
|
+
Dynamic: requires-dist
|
|
27
|
+
Dynamic: summary
|
|
@@ -20,6 +20,7 @@ def write_catalog(catalog):
|
|
|
20
20
|
# pylint: disable=too-many-instance-attributes
|
|
21
21
|
class CatalogEntry():
|
|
22
22
|
|
|
23
|
+
# pylint: disable=too-many-positional-arguments
|
|
23
24
|
def __init__(self, tap_stream_id=None, stream=None,
|
|
24
25
|
key_properties=None, schema=None, replication_key=None,
|
|
25
26
|
is_view=None, database=None, table=None, row_count=None,
|
|
@@ -31,7 +31,7 @@ class Schema(): # pylint: disable=too-many-instance-attributes
|
|
|
31
31
|
|
|
32
32
|
'''
|
|
33
33
|
|
|
34
|
-
# pylint: disable=too-many-locals
|
|
34
|
+
# pylint: disable=too-many-locals,too-many-positional-arguments
|
|
35
35
|
def __init__(self, type=None, format=None, properties=None, items=None,
|
|
36
36
|
selected=None, inclusion=None, description=None, minimum=None,
|
|
37
37
|
maximum=None, exclusiveMinimum=None, exclusiveMaximum=None,
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import dateutil.parser
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def add_observation(acc, path):
|
|
5
|
+
|
|
6
|
+
node = acc
|
|
7
|
+
for i in range(0, len(path) - 1):
|
|
8
|
+
k = path[i]
|
|
9
|
+
if k not in node:
|
|
10
|
+
node[k] = {}
|
|
11
|
+
node = node[k]
|
|
12
|
+
|
|
13
|
+
node[path[-1]] = True
|
|
14
|
+
|
|
15
|
+
# pylint: disable=too-many-branches
|
|
16
|
+
def add_observations(acc, path, data):
|
|
17
|
+
if isinstance(data, dict):
|
|
18
|
+
for key in data:
|
|
19
|
+
add_observations(acc, path + ["object", key], data[key])
|
|
20
|
+
elif isinstance(data, list):
|
|
21
|
+
for item in data:
|
|
22
|
+
add_observations(acc, path + ["array"], item)
|
|
23
|
+
elif isinstance(data, str):
|
|
24
|
+
# If the string parses as a date, add an observation that its a date
|
|
25
|
+
try:
|
|
26
|
+
data = dateutil.parser.parse(data)
|
|
27
|
+
except (dateutil.parser.ParserError, OverflowError):
|
|
28
|
+
data = None
|
|
29
|
+
if data:
|
|
30
|
+
add_observation(acc, path + ["date"])
|
|
31
|
+
else:
|
|
32
|
+
add_observation(acc, path + ["string"])
|
|
33
|
+
|
|
34
|
+
elif isinstance(data, bool):
|
|
35
|
+
add_observation(acc, path + ["boolean"])
|
|
36
|
+
elif isinstance(data, int):
|
|
37
|
+
add_observation(acc, path + ["integer"])
|
|
38
|
+
elif isinstance(data, float):
|
|
39
|
+
add_observation(acc, path + ["number"])
|
|
40
|
+
elif data is None:
|
|
41
|
+
add_observation(acc, path + ["null"])
|
|
42
|
+
else:
|
|
43
|
+
raise Exception("Unexpected value " + repr(data) + " at path " + repr(path))
|
|
44
|
+
|
|
45
|
+
return acc
|
|
46
|
+
|
|
47
|
+
def to_json_schema(obs):
|
|
48
|
+
result = {'type': ['null']}
|
|
49
|
+
|
|
50
|
+
for key in obs:
|
|
51
|
+
|
|
52
|
+
if key == 'object':
|
|
53
|
+
result['type'] += ['object']
|
|
54
|
+
if 'properties' not in result:
|
|
55
|
+
result['properties'] = {}
|
|
56
|
+
for obj_key in obs['object']:
|
|
57
|
+
result['properties'][obj_key] = to_json_schema(obs['object'][obj_key])
|
|
58
|
+
|
|
59
|
+
elif key == 'array':
|
|
60
|
+
result['type'] += ['array']
|
|
61
|
+
result['items'] = to_json_schema(obs['array'])
|
|
62
|
+
|
|
63
|
+
elif key == 'date':
|
|
64
|
+
result['type'] += ['string']
|
|
65
|
+
result['format'] = 'date-time'
|
|
66
|
+
elif key == 'string':
|
|
67
|
+
result['type'] += ['string']
|
|
68
|
+
|
|
69
|
+
elif key == 'boolean':
|
|
70
|
+
result['type'] += ['boolean']
|
|
71
|
+
|
|
72
|
+
elif key == 'integer':
|
|
73
|
+
result['type'] += ['integer']
|
|
74
|
+
|
|
75
|
+
elif key == 'number':
|
|
76
|
+
# Use type=string, format=singer.decimal
|
|
77
|
+
result['type'] += ['string']
|
|
78
|
+
result['format'] = 'singer.decimal'
|
|
79
|
+
|
|
80
|
+
elif key == 'null':
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
else:
|
|
84
|
+
raise Exception("Unexpected data type " + key)
|
|
85
|
+
|
|
86
|
+
return result
|
|
87
|
+
|
|
88
|
+
def generate_schema(records):
|
|
89
|
+
obs = {}
|
|
90
|
+
for record in records:
|
|
91
|
+
obs = add_observations(obs, [], record)
|
|
92
|
+
return to_json_schema(obs)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: singer-python
|
|
3
|
+
Version: 6.2.0
|
|
4
|
+
Summary: Singer.io utility library
|
|
5
|
+
Home-page: http://singer.io
|
|
6
|
+
Author: Stitch
|
|
7
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: pytz>=2018.4
|
|
10
|
+
Requires-Dist: jsonschema==2.*,>=2.6.0
|
|
11
|
+
Requires-Dist: simplejson==3.*,>=3.13.2
|
|
12
|
+
Requires-Dist: python-dateutil==2.*,>=2.7.3
|
|
13
|
+
Requires-Dist: backoff==2.*,>=2.2.1
|
|
14
|
+
Requires-Dist: ciso8601==2.*,>=2.3.1
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pylint; extra == "dev"
|
|
17
|
+
Requires-Dist: ipython; extra == "dev"
|
|
18
|
+
Requires-Dist: ipdb; extra == "dev"
|
|
19
|
+
Requires-Dist: nose; extra == "dev"
|
|
20
|
+
Requires-Dist: singer-tools; extra == "dev"
|
|
21
|
+
Dynamic: author
|
|
22
|
+
Dynamic: classifier
|
|
23
|
+
Dynamic: home-page
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
Dynamic: provides-extra
|
|
26
|
+
Dynamic: requires-dist
|
|
27
|
+
Dynamic: summary
|
|
@@ -13,6 +13,7 @@ singer/metadata.py
|
|
|
13
13
|
singer/metrics.py
|
|
14
14
|
singer/requests.py
|
|
15
15
|
singer/schema.py
|
|
16
|
+
singer/schema_generation.py
|
|
16
17
|
singer/statediff.py
|
|
17
18
|
singer/transform.py
|
|
18
19
|
singer/utils.py
|
|
@@ -28,6 +29,7 @@ tests/test_exceptions.py
|
|
|
28
29
|
tests/test_metadata.py
|
|
29
30
|
tests/test_metrics.py
|
|
30
31
|
tests/test_schema.py
|
|
32
|
+
tests/test_schema_generation.py
|
|
31
33
|
tests/test_singer.py
|
|
32
34
|
tests/test_statediff.py
|
|
33
35
|
tests/test_transform.py
|
|
@@ -25,7 +25,7 @@ class TestGetSelectedStreams(unittest.TestCase):
|
|
|
25
25
|
CatalogEntry(tap_stream_id='c',schema=Schema(),metadata=[])])
|
|
26
26
|
state = {}
|
|
27
27
|
selected_streams = catalog.get_selected_streams(state)
|
|
28
|
-
self.
|
|
28
|
+
self.assertEqual([e for e in selected_streams],[selected_entry])
|
|
29
29
|
|
|
30
30
|
def test_resumes_currently_syncing_stream(self):
|
|
31
31
|
selected_entry_a = CatalogEntry(tap_stream_id='a',
|
|
@@ -44,7 +44,7 @@ class TestGetSelectedStreams(unittest.TestCase):
|
|
|
44
44
|
selected_entry_c])
|
|
45
45
|
state = {'currently_syncing': 'c'}
|
|
46
46
|
selected_streams = catalog.get_selected_streams(state)
|
|
47
|
-
self.
|
|
47
|
+
self.assertEqual([e for e in selected_streams][0],selected_entry_c)
|
|
48
48
|
|
|
49
49
|
class TestToDictAndFromDict(unittest.TestCase):
|
|
50
50
|
|
|
@@ -141,4 +141,4 @@ class TestGetStream(unittest.TestCase):
|
|
|
141
141
|
CatalogEntry(tap_stream_id='b'),
|
|
142
142
|
CatalogEntry(tap_stream_id='c')])
|
|
143
143
|
entry = catalog.get_stream('b')
|
|
144
|
-
self.
|
|
144
|
+
self.assertEqual('b', entry.tap_stream_id)
|
|
@@ -14,8 +14,8 @@ class TestSingerErrors(unittest.TestCase):
|
|
|
14
14
|
raise SingerError(error_text)
|
|
15
15
|
|
|
16
16
|
expected_text = "SingerError\n" + error_text
|
|
17
|
-
self.
|
|
18
|
-
|
|
17
|
+
self.assertEqual(expected_text,
|
|
18
|
+
str(test_run.exception))
|
|
19
19
|
|
|
20
20
|
def test_SingerConfigurationError_prints_correctly(self):
|
|
21
21
|
error_text = "An error occured"
|
|
@@ -24,8 +24,8 @@ class TestSingerErrors(unittest.TestCase):
|
|
|
24
24
|
raise SingerConfigurationError(error_text)
|
|
25
25
|
|
|
26
26
|
expected_text = "SingerConfigurationError\n" + error_text
|
|
27
|
-
self.
|
|
28
|
-
|
|
27
|
+
self.assertEqual(expected_text,
|
|
28
|
+
str(test_run.exception))
|
|
29
29
|
|
|
30
30
|
def test_SingerDiscoveryError_prints_correctly(self):
|
|
31
31
|
error_text = "An error occured"
|
|
@@ -34,8 +34,8 @@ class TestSingerErrors(unittest.TestCase):
|
|
|
34
34
|
raise SingerDiscoveryError(error_text)
|
|
35
35
|
|
|
36
36
|
expected_text = "SingerDiscoveryError\n" + error_text
|
|
37
|
-
self.
|
|
38
|
-
|
|
37
|
+
self.assertEqual(expected_text,
|
|
38
|
+
str(test_run.exception))
|
|
39
39
|
|
|
40
40
|
def test_SingerSyncError_prints_correctly(self):
|
|
41
41
|
error_text = "An error occured"
|
|
@@ -44,8 +44,8 @@ class TestSingerErrors(unittest.TestCase):
|
|
|
44
44
|
raise SingerSyncError(error_text)
|
|
45
45
|
|
|
46
46
|
expected_text = "SingerSyncError\n" + error_text
|
|
47
|
-
self.
|
|
48
|
-
|
|
47
|
+
self.assertEqual(expected_text,
|
|
48
|
+
str(test_run.exception))
|
|
49
49
|
|
|
50
50
|
def test_SingerRetryableRequestError_prints_correctly(self):
|
|
51
51
|
error_text = "An error occured"
|
|
@@ -54,8 +54,8 @@ class TestSingerErrors(unittest.TestCase):
|
|
|
54
54
|
raise SingerRetryableRequestError(error_text)
|
|
55
55
|
|
|
56
56
|
expected_text = "SingerRetryableRequestError\n" + error_text
|
|
57
|
-
self.
|
|
58
|
-
|
|
57
|
+
self.assertEqual(expected_text,
|
|
58
|
+
str(test_run.exception))
|
|
59
59
|
|
|
60
60
|
def test_SingerError_prints_multiple_lines_correctly(self):
|
|
61
61
|
error_text = "\n".join(["Line 1", "Line 2", "Line 3"])
|
|
@@ -64,5 +64,5 @@ class TestSingerErrors(unittest.TestCase):
|
|
|
64
64
|
raise SingerError(error_text)
|
|
65
65
|
|
|
66
66
|
expected_text = "SingerError\n" + error_text
|
|
67
|
-
self.
|
|
68
|
-
|
|
67
|
+
self.assertEqual(expected_text,
|
|
68
|
+
str(test_run.exception))
|
|
@@ -44,38 +44,38 @@ class TestSchema(unittest.TestCase):
|
|
|
44
44
|
additionalProperties=True)
|
|
45
45
|
|
|
46
46
|
def test_string_to_dict(self):
|
|
47
|
-
self.
|
|
47
|
+
self.assertEqual(self.string_dict, self.string_obj.to_dict())
|
|
48
48
|
|
|
49
49
|
def test_integer_to_dict(self):
|
|
50
|
-
self.
|
|
50
|
+
self.assertEqual(self.integer_dict, self.integer_obj.to_dict())
|
|
51
51
|
|
|
52
52
|
def test_array_to_dict(self):
|
|
53
|
-
self.
|
|
53
|
+
self.assertEqual(self.array_dict, self.array_obj.to_dict())
|
|
54
54
|
|
|
55
55
|
def test_object_to_dict(self):
|
|
56
|
-
self.
|
|
56
|
+
self.assertEqual(self.object_dict, self.object_obj.to_dict())
|
|
57
57
|
|
|
58
58
|
def test_string_from_dict(self):
|
|
59
|
-
self.
|
|
59
|
+
self.assertEqual(self.string_obj, Schema.from_dict(self.string_dict))
|
|
60
60
|
|
|
61
61
|
def test_integer_from_dict(self):
|
|
62
|
-
self.
|
|
62
|
+
self.assertEqual(self.integer_obj, Schema.from_dict(self.integer_dict))
|
|
63
63
|
|
|
64
64
|
def test_array_from_dict(self):
|
|
65
|
-
self.
|
|
65
|
+
self.assertEqual(self.array_obj, Schema.from_dict(self.array_dict))
|
|
66
66
|
|
|
67
67
|
def test_object_from_dict(self):
|
|
68
|
-
self.
|
|
68
|
+
self.assertEqual(self.object_obj, Schema.from_dict(self.object_dict))
|
|
69
69
|
|
|
70
70
|
def test_repr_atomic(self):
|
|
71
|
-
self.
|
|
71
|
+
self.assertEqual(self.string_obj, eval(repr(self.string_obj)))
|
|
72
72
|
|
|
73
73
|
def test_repr_recursive(self):
|
|
74
|
-
self.
|
|
74
|
+
self.assertEqual(self.object_obj, eval(repr(self.object_obj)))
|
|
75
75
|
|
|
76
76
|
def test_object_from_dict_with_defaults(self):
|
|
77
77
|
schema = Schema.from_dict(self.object_dict, inclusion='automatic')
|
|
78
|
-
self.
|
|
79
|
-
|
|
80
|
-
self.
|
|
81
|
-
self.
|
|
78
|
+
self.assertEqual('whatever', schema.inclusion,
|
|
79
|
+
msg='The schema value should override the default')
|
|
80
|
+
self.assertEqual('automatic', schema.properties['a_string'].inclusion)
|
|
81
|
+
self.assertEqual('automatic', schema.properties['an_array'].items.inclusion)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from singer.schema_generation import generate_schema
|
|
3
|
+
|
|
4
|
+
class TestSchemaGeneration(unittest.TestCase):
|
|
5
|
+
def test_simple_schema(self):
|
|
6
|
+
records = [{'a': 1, 'b': 'two', 'c': True, 'dt': '2000-01-01T00:11:22Z'}]
|
|
7
|
+
expected_schema = {
|
|
8
|
+
'type': ['null', 'object'],
|
|
9
|
+
'properties': {
|
|
10
|
+
'a': {'type': ['null', 'integer']},
|
|
11
|
+
'b': {'type': ['null', 'string']},
|
|
12
|
+
'c': {'type': ['null', 'boolean']},
|
|
13
|
+
'dt': {'type': ['null', 'string'], 'format': 'date-time'}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
self.assertEqual(expected_schema, generate_schema(records))
|
|
17
|
+
|
|
18
|
+
def test_mix_n_match_records_schema(self):
|
|
19
|
+
records = [
|
|
20
|
+
{'a': 1, 'b': 'b'},
|
|
21
|
+
{'a': 'two', 'c': 7, 'd': [1, 'two']},
|
|
22
|
+
{'a': True, 'c': 7.7, 'd': {'one': 1, 'two': 'two'}}
|
|
23
|
+
]
|
|
24
|
+
expected_schema = {
|
|
25
|
+
'type': ['null', 'object'],
|
|
26
|
+
'properties': {
|
|
27
|
+
'a': {'type': {'null', 'integer', 'string', 'boolean'}},
|
|
28
|
+
'b': {'type': ['null', 'string']},
|
|
29
|
+
'c': {'type': {'null', 'integer', 'string'}, 'format': 'singer.decimal'},
|
|
30
|
+
'd': {
|
|
31
|
+
'type': {'null', 'array', 'object'},
|
|
32
|
+
'items': {'type': {'null', 'integer', 'string'}},
|
|
33
|
+
'properties': {'one': {'type': ['null', 'integer']},
|
|
34
|
+
'two': {'type': ['null', 'string']}}
|
|
35
|
+
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
actual_schema = generate_schema(records)
|
|
40
|
+
actual_schema['properties']['a']['type'] = set(actual_schema['properties']['a']['type'])
|
|
41
|
+
actual_schema['properties']['c']['type'] = set(actual_schema['properties']['c']['type'])
|
|
42
|
+
actual_schema['properties']['d']['type'] = set(actual_schema['properties']['d']['type'])
|
|
43
|
+
actual_schema['properties']['d']['items']['type'] = set(actual_schema['properties']['d']['items']['type'])
|
|
44
|
+
self.assertEqual(expected_schema, actual_schema)
|
|
45
|
+
|
|
46
|
+
def test_nested_structue_schema(self):
|
|
47
|
+
records = [{'a': {'b': {'c': [{'d': 7}]}, 'e': [[1, 2, 3]]}}]
|
|
48
|
+
expected_schema = {
|
|
49
|
+
'type': ['null', 'object'],
|
|
50
|
+
'properties': {
|
|
51
|
+
'a': {
|
|
52
|
+
'type': ['null', 'object'],
|
|
53
|
+
'properties': {
|
|
54
|
+
'b': {
|
|
55
|
+
'type': ['null', 'object'],
|
|
56
|
+
'properties': {
|
|
57
|
+
'c': {
|
|
58
|
+
'type': ['null', 'array'],
|
|
59
|
+
'items': {
|
|
60
|
+
'type': ['null', 'object'],
|
|
61
|
+
'properties': {'d': {'type': ['null', 'integer']}}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
'e': {
|
|
67
|
+
'type': ['null', 'array'],
|
|
68
|
+
'items': {
|
|
69
|
+
'type': ['null', 'array'],
|
|
70
|
+
'items': {'type': ['null', 'integer']}}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
self.assertEqual(expected_schema, generate_schema(records))
|
|
@@ -264,11 +264,11 @@ class TestTransform(unittest.TestCase):
|
|
|
264
264
|
nan = {'percentage': decimal.Decimal('NaN')}
|
|
265
265
|
snan = {'percentage': decimal.Decimal('sNaN')}
|
|
266
266
|
|
|
267
|
-
self.
|
|
268
|
-
self.
|
|
269
|
-
self.
|
|
270
|
-
self.
|
|
271
|
-
self.
|
|
267
|
+
self.assertEqual(inf, transform(inf, schema))
|
|
268
|
+
self.assertEqual(negative_inf, transform(negative_inf, schema))
|
|
269
|
+
self.assertEqual({'percentage': '1.4142135623730951'}, transform(root2, schema))
|
|
270
|
+
self.assertEqual({'percentage': 'NaN'}, transform(nan, schema))
|
|
271
|
+
self.assertEqual({'percentage': 'NaN'}, transform(snan, schema))
|
|
272
272
|
|
|
273
273
|
|
|
274
274
|
str1 = {'percentage':'0.1'}
|
|
@@ -276,11 +276,11 @@ class TestTransform(unittest.TestCase):
|
|
|
276
276
|
str3 = {'percentage': '1E+13'}
|
|
277
277
|
str4 = {'percentage': '100'}
|
|
278
278
|
str5 = {'percentage': '-100'}
|
|
279
|
-
self.
|
|
280
|
-
self.
|
|
281
|
-
self.
|
|
282
|
-
self.
|
|
283
|
-
self.
|
|
279
|
+
self.assertEqual(str1, transform(str1, schema))
|
|
280
|
+
self.assertEqual({'percentage': '1E-13'}, transform(str2, schema))
|
|
281
|
+
self.assertEqual({'percentage': '1E+13'}, transform(str3, schema))
|
|
282
|
+
self.assertEqual({'percentage': '100'}, transform(str4, schema))
|
|
283
|
+
self.assertEqual({'percentage': '-100'}, transform(str5, schema))
|
|
284
284
|
|
|
285
285
|
float1 = {'percentage': 12.0000000000000000000000000001234556}
|
|
286
286
|
float2 = {'percentage': 0.0123}
|
|
@@ -288,28 +288,28 @@ class TestTransform(unittest.TestCase):
|
|
|
288
288
|
float4 = {'percentage': -100.0123}
|
|
289
289
|
float5 = {'percentage': 0.000001}
|
|
290
290
|
float6 = {'percentage': 0.0000001}
|
|
291
|
-
self.
|
|
292
|
-
self.
|
|
293
|
-
self.
|
|
294
|
-
self.
|
|
295
|
-
self.
|
|
296
|
-
self.
|
|
291
|
+
self.assertEqual({'percentage':'12.0'}, transform(float1, schema))
|
|
292
|
+
self.assertEqual({'percentage':'0.0123'}, transform(float2, schema))
|
|
293
|
+
self.assertEqual({'percentage':'100.0123'}, transform(float3, schema))
|
|
294
|
+
self.assertEqual({'percentage':'-100.0123'}, transform(float4, schema))
|
|
295
|
+
self.assertEqual({'percentage':'0.000001'}, transform(float5, schema))
|
|
296
|
+
self.assertEqual({'percentage':'1E-7'}, transform(float6, schema))
|
|
297
297
|
|
|
298
298
|
int1 = {'percentage': 123}
|
|
299
299
|
int2 = {'percentage': 0}
|
|
300
300
|
int3 = {'percentage': -1000}
|
|
301
|
-
self.
|
|
302
|
-
self.
|
|
303
|
-
self.
|
|
301
|
+
self.assertEqual({'percentage':'123'}, transform(int1, schema))
|
|
302
|
+
self.assertEqual({'percentage':'0'}, transform(int2, schema))
|
|
303
|
+
self.assertEqual({'percentage':'-1000'}, transform(int3, schema))
|
|
304
304
|
|
|
305
305
|
dec1 = {'percentage': decimal.Decimal('1.1010101')}
|
|
306
306
|
dec2 = {'percentage': decimal.Decimal('.111111111111111111111111')}
|
|
307
307
|
dec3 = {'percentage': decimal.Decimal('-.111111111111111111111111')}
|
|
308
308
|
dec4 = {'percentage': decimal.Decimal('100')}
|
|
309
|
-
self.
|
|
310
|
-
self.
|
|
311
|
-
self.
|
|
312
|
-
self.
|
|
309
|
+
self.assertEqual({'percentage':'1.1010101'}, transform(dec1, schema))
|
|
310
|
+
self.assertEqual({'percentage':'0.111111111111111111111111'}, transform(dec2, schema))
|
|
311
|
+
self.assertEqual({'percentage':'-0.111111111111111111111111'}, transform(dec3, schema))
|
|
312
|
+
self.assertEqual({'percentage':'100'}, transform(dec4, schema))
|
|
313
313
|
|
|
314
314
|
bad1 = {'percentage': 'fsdkjl'}
|
|
315
315
|
with self.assertRaises(SchemaMismatch):
|
|
@@ -317,7 +317,7 @@ class TestTransform(unittest.TestCase):
|
|
|
317
317
|
|
|
318
318
|
badnull = {'percentage': None}
|
|
319
319
|
with self.assertRaises(SchemaMismatch):
|
|
320
|
-
self.
|
|
320
|
+
self.assertEqual({'percentage':None}, transform(badnull, schema))
|
|
321
321
|
|
|
322
322
|
class TestTransformsWithMetadata(unittest.TestCase):
|
|
323
323
|
|
singer-python-6.1.0/PKG-INFO
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|