|
| 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