Skip to content

Commit cd406b8

Browse files
committed
initial commit
0 parents  commit cd406b8

8 files changed

+620
-0
lines changed

.gitignore

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
venv/
2+
*.pyc
3+
.vscode/
4+
.DS_Store
5+
/dist/
6+
/*.egg-info/

LICENSE.txt

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2020, crouchcd
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# pyion2json
2+
3+
Convert an [Amazon Ion](http://amzn.github.io/ion-docs/) document(s) to JSON
4+
5+
## Install
6+
7+
```
8+
pip install pyion2json
9+
```
10+
11+
## Usage
12+
13+
### Convert individual Ion values
14+
15+
```
16+
import json
17+
import amazon.ion.simpleion as ion
18+
from pyion2json import ion_to_json
19+
20+
json_doc = ion_to_json(
21+
ion.loads('{ first: "Tom" , last: "Riddle" }')
22+
)
23+
json.dumps(json_doc, indent=' ')
24+
25+
```
26+
27+
> Outputs:
28+
29+
```
30+
{
31+
"first": "Tom",
32+
"last": "Riddle"
33+
}
34+
```
35+
36+
### Convert a cursor from QLDB
37+
38+
```
39+
from pyion2json import ion_cursor_to_json
40+
41+
with create_qldb_session() as qldb_session:
42+
qldb_cursor = qldb_session.execute_statement('SELECT first,last FROM Users')
43+
json_rows = ion_cursor_to_json(qldb_cursor)
44+
json.dumps(json_rows, indent=' ')
45+
46+
```
47+
48+
> Outputs:
49+
50+
```
51+
[
52+
{
53+
"first": "Harry",
54+
"last": "Potter"
55+
},
56+
{
57+
"first": "Tom",
58+
"last": "Riddle"
59+
}
60+
]
61+
```
62+
63+
## TODO:
64+
65+
1. Verify BLOB conversion meets expectations
66+
2. Verify CLOB conversion meets expectations

pyion2json/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from .pyion2json import ion_to_json
2+
from .pyion2json import ion_cursor_to_json

pyion2json/pyion2json.py

+150
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import amazon.ion.simpleion as ion
2+
from base64 import b64encode as base64encode
3+
4+
5+
def _is_ion_null(ion_val):
6+
return isinstance(ion_val, ion.IonPyNull)
7+
8+
9+
def _is_ion_int(ion_val):
10+
return isinstance(ion_val, ion.IonPyInt) and ion_val.ion_type == ion.IonType.INT
11+
12+
13+
def _is_ion_bool(ion_val):
14+
return isinstance(ion_val, ion.IonPyInt) or isinstance(ion_val, ion.IonPyBool) and ion_val.ion_type == ion.IonType.BOOL
15+
16+
17+
def _is_ion_float(ion_val):
18+
return isinstance(ion_val, ion.IonPyFloat)
19+
20+
21+
def _is_ion_decimal(ion_val):
22+
return isinstance(ion_val, ion.IonPyDecimal)
23+
24+
25+
def _is_ion_timestamp(ion_val):
26+
return isinstance(ion_val, ion.IonPyTimestamp)
27+
28+
29+
def _is_ion_symbol(ion_val):
30+
return isinstance(ion_val, ion.IonPySymbol)
31+
32+
33+
def _is_ion_string(ion_val):
34+
return isinstance(ion_val, ion.IonPyText)
35+
36+
37+
def _is_ion_clob(ion_val):
38+
return isinstance(ion_val, ion.IonPyBytes) and ion_val.ion_type == ion.IonType.CLOB
39+
40+
41+
def _is_ion_blob(ion_val):
42+
return isinstance(ion_val, ion.IonPyBytes) and ion_val.ion_type == ion.IonType.BLOB
43+
44+
45+
def _is_ion_struct(ion_val):
46+
return isinstance(ion_val, ion.IonPyDict)
47+
48+
49+
def _is_ion_list(ion_val):
50+
return isinstance(ion_val, ion.IonPyList)
51+
52+
53+
def _default_blob_decoder(blob):
54+
# TODO: verify this is handles all cases
55+
return base64encode(blob).decode('utf-8')
56+
57+
58+
def _default_clob_decoder(clob):
59+
# TODO: verify this is handles all cases
60+
return clob.decode('utf-8')
61+
62+
63+
def ion_to_json(ion_val, blob_decoder=None, clob_decoder=None):
64+
""" Down-convert an Ion value into a type that can be serialized by `json.dump`.
65+
The official rules to perform the conversion are located
66+
at: http://amzn.github.io/ion-docs/guides/cookbook.html#down-converting-to-json.
67+
68+
:param ion_val: An Amazon Ion value
69+
:type ion_val: `IonType(<ANY>)`
70+
71+
:param blob_decoder: Function that will be used to decode blobs
72+
:type blob_decoder: `function(blob) -> str`, optional
73+
74+
:param clob_decoder: Function that will be used to decode clobs
75+
:type clob_decoder: `function(clob) -> str`, optional
76+
77+
:raises `Exception`: If the type of `ion_val` isn't handled
78+
79+
:return: JSON serializable type converted from ion_val
80+
:rtype: `any`
81+
"""
82+
if _is_ion_null(ion_val):
83+
# Nulls of any type are converted to JSON null
84+
return None
85+
elif _is_ion_int(ion_val):
86+
# Arbitrary precision integers are printed as JSON number with precision preserved
87+
return ion_val
88+
elif _is_ion_bool(ion_val):
89+
# Convert BOOL to JSON True/False
90+
return True if ion_val == 1 else False
91+
elif _is_ion_float(ion_val):
92+
# Floats are printed as JSON number with nan and +-inf converted to JSON null
93+
if 'inf' in str(ion_val) or 'nan' in str(ion_val):
94+
return None
95+
return ion_val
96+
elif _is_ion_decimal(ion_val):
97+
# Decimals are printed as JSON number with precision preserved
98+
return float(ion_val)
99+
elif _is_ion_timestamp(ion_val):
100+
# Timestamps are printed as JSON string
101+
return str(ion_val)
102+
elif _is_ion_symbol(ion_val):
103+
# Symbols are printed as JSON string
104+
return ion_val.text
105+
elif _is_ion_string(ion_val):
106+
# Strings are printed as JSON string
107+
return str(ion_val)
108+
elif _is_ion_clob(ion_val):
109+
# Clobs are ASCII-encoded for characters between 32 (0x20) and 126 (0x7e),
110+
# inclusive. Characters from 0 (0x00) to 31 (0x1f) and from 127 (0x7f) to
111+
# 255 (0xff) are escaped as Unicode code points U+00XX
112+
if clob_decoder:
113+
return clob_decoder(ion_val)
114+
return _default_clob_decoder(ion_val)
115+
elif _is_ion_blob(ion_val):
116+
# Blobs are printed as Base64-encoded JSON strings
117+
if blob_decoder:
118+
return blob_decoder(ion_val)
119+
return _default_blob_decoder(ion_val)
120+
elif _is_ion_struct(ion_val):
121+
# Structs are printed as JSON object
122+
return {key: ion_to_json(ion_val[key]) for key in ion_val.keys()}
123+
elif _is_ion_list(ion_val):
124+
# Lists are printed as JSON array
125+
# S-expressions are printed as JSON array
126+
return list(map(ion_to_json, ion_val))
127+
raise Exception(f'Unhandled conversion for {type(ion_val)}')
128+
129+
130+
def ion_cursor_to_json(ion_cursor, blob_decoder=None, clob_decoder=None):
131+
""" Down-convert each row of an iterable of Ion objects
132+
into a JSON serializable list
133+
134+
:param ion_cursor: The Ion iterable (e.g. cursor from QLDB)
135+
:type ion_cursor: `iterable`
136+
137+
:param blob_decoder: Function that will be used to decode blobs
138+
:type blob_decoder: `function(blob) -> str`, optional
139+
140+
:param clob_decoder: Function that will be used to decode clobs
141+
:type clob_decoder: `function(clob) -> str`, optional
142+
143+
:return: A JSON list
144+
:rtype: `list(<JSON>)`
145+
"""
146+
return list(map(
147+
lambda row: ion_to_json(row, blob_decoder=blob_decoder,
148+
clob_decoder=clob_decoder),
149+
ion_cursor
150+
))

0 commit comments

Comments
 (0)