From 655444bcde054d7356588553c5cd4802685ef517 Mon Sep 17 00:00:00 2001 From: Kenny Daniel Date: Mon, 7 Apr 2025 17:20:22 -0700 Subject: [PATCH] Fix continued data pages Parquet allows consecutive pages to continue a previously assembled list. Broke in hyparquet 1.9.0. Added continued_page.parquet test. --- package.json | 2 +- src/assemble.js | 1 + src/column.js | 24 ++++-- test/files/continued_page.json | 102 ++++++++++++++++++++++++ test/files/continued_page.metadata.json | 90 +++++++++++++++++++++ test/files/continued_page.parquet | Bin 0 -> 2838 bytes 6 files changed, 211 insertions(+), 8 deletions(-) create mode 100644 test/files/continued_page.json create mode 100644 test/files/continued_page.metadata.json create mode 100644 test/files/continued_page.parquet diff --git a/package.json b/package.json index 4faaf79..a31248c 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "eslint-plugin-jsdoc": "50.6.9", "hyparquet-compressors": "1.1.1", "typescript": "5.8.3", - "typescript-eslint": "8.29.0", + "typescript-eslint": "8.29.1", "vitest": "3.1.1" } } diff --git a/src/assemble.js b/src/assemble.js index 10b78f4..a93b501 100644 --- a/src/assemble.js +++ b/src/assemble.js @@ -29,6 +29,7 @@ export function assembleLists(output, definitionLevels, repetitionLevels, values if (repetitionLevels[0]) { // continue previous row while (currentDepth < repetitionPath.length - 2 && currentRepLevel < repetitionLevels[0]) { + if (!currentContainer) throw new Error('parquet cannot resume previous page') // go into last list currentContainer = currentContainer.at(-1) containerStack.push(currentContainer) diff --git a/src/column.js b/src/column.js index 3e52940..8f9b952 100644 --- a/src/column.js +++ b/src/column.js @@ -27,14 +27,21 @@ export function readColumn(reader, rowGroupStart, rowGroupEnd, columnMetadata, s // read dictionary if (hasDictionary(columnMetadata)) { - dictionary = readPage(reader, columnMetadata, schemaPath, element, dictionary, 0, options) + dictionary = readPage(reader, columnMetadata, schemaPath, element, dictionary, undefined, 0, options) } while (rowCount < rowGroupEnd) { if (reader.offset >= reader.view.byteLength - 1) break // end of reader - const values = readPage(reader, columnMetadata, schemaPath, element, dictionary, rowGroupStart - rowCount, options) - chunks.push(values) - rowCount += values.length + const lastChunk = chunks.at(-1) + const lastChunkLength = lastChunk ? lastChunk.length : 0 + const values = readPage(reader, columnMetadata, schemaPath, element, dictionary, lastChunk, rowGroupStart - rowCount, options) + if (lastChunk === values) { + // continued from previous page + rowCount += values.length - lastChunkLength + } else { + chunks.push(values) + rowCount += values.length + } } if (isFinite(rowGroupEnd)) { if (rowCount < rowGroupEnd) { @@ -57,11 +64,12 @@ export function readColumn(reader, rowGroupStart, rowGroupEnd, columnMetadata, s * @param {SchemaTree[]} schemaPath * @param {SchemaElement} element * @param {DecodedArray | undefined} dictionary + * @param {DecodedArray | undefined} previousChunk * @param {number} pageStart skip this many rows in the page * @param {ParquetReadOptions} options * @returns {DecodedArray} */ -export function readPage(reader, columnMetadata, schemaPath, element, dictionary, pageStart, { utf8, compressors }) { +export function readPage(reader, columnMetadata, schemaPath, element, dictionary, previousChunk, pageStart, { utf8, compressors }) { const header = parquetHeader(reader) // column header // read compressed_page_size bytes @@ -87,7 +95,8 @@ export function readPage(reader, columnMetadata, schemaPath, element, dictionary // convert types, dereference dictionary, and assemble lists let values = convertWithDictionary(dataPage, dictionary, element, daph.encoding, utf8) if (repetitionLevels.length || definitionLevels?.length) { - return assembleLists([], definitionLevels, repetitionLevels, values, schemaPath) + const output = Array.isArray(previousChunk) ? previousChunk : [] + return assembleLists(output, definitionLevels, repetitionLevels, values, schemaPath) } else { // wrap nested flat data by depth for (let i = 2; i < schemaPath.length; i++) { @@ -112,7 +121,8 @@ export function readPage(reader, columnMetadata, schemaPath, element, dictionary // convert types, dereference dictionary, and assemble lists const values = convertWithDictionary(dataPage, dictionary, element, daph2.encoding, utf8) - return assembleLists([], definitionLevels, repetitionLevels, values, schemaPath) + const output = Array.isArray(previousChunk) ? previousChunk : [] + return assembleLists(output, definitionLevels, repetitionLevels, values, schemaPath) } else if (header.type === 'DICTIONARY_PAGE') { const diph = header.dictionary_page_header if (!diph) throw new Error('parquet dictionary page header is undefined') diff --git a/test/files/continued_page.json b/test/files/continued_page.json new file mode 100644 index 0000000..a4ee195 --- /dev/null +++ b/test/files/continued_page.json @@ -0,0 +1,102 @@ +[ + [[51, 26, 66, 49, 9, 78, 40, 44, 58, 96, 65, 58, 82, 3, 69, 8, 49, 14, 83, 99]], + [[4, 59, 90, 41, 58, 65, 34, 12, 98, 16, 89, 12, 73, 26, 21, 29, 35, 0, 6, 87]], + [[86, 82, 55, 33, 57, 23, 51, 89, 32, 20, 74, 56, 36, 2, 89, 61, 96, 26, 94, 83]], + [[49, 34, 59, 39, 65, 16, 16, 11, 73, 1, 2, 85, 85, 4, 27, 98, 85, 7, 66, 91]], + [[53, 61, 82, 63, 39, 88, 42, 10, 12, 76, 40, 14, 8, 8, 18, 64, 70, 65, 11, 13]], + [[91, 52, 56, 80, 53, 66, 30, 7, 41, 9, 64, 55, 11, 10, 75, 78, 29, 4, 79, 31]], + [[28, 46, 60, 94, 20, 34, 37, 92, 90, 29, 3, 55, 59, 53, 37, 54, 72, 21, 65, 54]], + [[68, 9, 11, 70, 1, 37, 63, 1, 73, 12, 29, 75, 76, 74, 74, 22, 15, 14, 55, 97]], + [[28, 75, 60, 30, 88, 13, 32, 62, 11, 34, 17, 39, 23, 85, 66, 32, 59, 58, 39, 30]], + [[5, 69, 73, 28, 50, 95, 91, 57, 24, 49, 95, 85, 44, 17, 70, 47, 51, 49, 37, 13]], + [[73, 83, 25, 26, 94, 65, 60, 89, 94, 85, 24, 29, 21, 16, 76, 90, 82, 31, 39, 54]], + [[66, 47, 91, 36, 96, 84, 91, 99, 99, 84, 8, 7, 25, 14, 58, 89, 31, 88, 30, 29]], + [[87, 68, 27, 37, 49, 40, 88, 77, 6, 94, 97, 25, 10, 19, 79, 14, 38, 39, 30, 61]], + [[34, 86, 68, 90, 96, 45, 72, 71, 16, 20, 86, 41, 7, 86, 60, 88, 55, 20, 30, 49]], + [[97, 88, 46, 82, 17, 11, 55, 3, 0, 82, 42, 4, 94, 29, 31, 30, 14, 80, 65, 35]], + [[68, 22, 11, 73, 29, 83, 7, 98, 41, 76, 26, 18, 69, 31, 77, 95, 66, 2, 53, 57]], + [[85, 67, 15, 8, 80, 60, 83, 8, 7, 34, 26, 97, 92, 34, 10, 60, 88, 96, 92, 5]], + [[22, 66, 71, 99, 74, 9, 88, 89, 75, 98, 82, 3, 85, 40, 76, 13, 23, 63, 44, 20]], + [[22, 36, 39, 4, 79, 86, 1, 29, 26, 96, 84, 1, 57, 24, 97, 13, 80, 64, 28, 8]], + [[27, 44, 43, 35, 69, 20, 51, 37, 24, 28, 67, 69, 33, 12, 78, 40, 35, 42, 17, 8]], + [[63, 92, 92, 40, 9, 7, 47, 90, 49, 94, 15, 89, 41, 13, 65, 30, 79, 93, 28, 84]], + [[45, 72, 97, 97, 84, 68, 33, 46, 37, 24, 0, 39, 18, 65, 75, 59, 61, 20, 10, 23]], + [[11, 64, 9, 13, 62, 93, 87, 25, 4, 55, 64, 78, 73, 71, 68, 85, 51, 44, 88, 43]], + [[53, 0, 15, 0, 19, 91, 69, 72, 96, 36, 44, 97, 78, 61, 64, 47, 4, 95, 61, 37]], + [[7, 75, 66, 97, 99, 77, 4, 82, 84, 58, 77, 81, 33, 35, 94, 6, 68, 6, 27, 77]], + [[99, 45, 87, 89, 10, 51, 20, 52, 18, 2, 46, 42, 41, 5, 92, 25, 52, 3, 70, 55]], + [[34, 21, 52, 1, 57, 64, 25, 35, 85, 29, 70, 40, 74, 84, 63, 40, 7, 2, 25, 1]], + [[21, 73, 54, 89, 47, 74, 4, 15, 19, 0, 41, 2, 21, 7, 15, 89, 32, 75, 14, 42]], + [[26, 64, 90, 30, 49, 68, 40, 13, 2, 58, 40, 1, 80, 68, 88, 73, 49, 59, 44, 35]], + [[52, 21, 88, 18, 56, 1, 87, 92, 11, 99, 89, 26, 62, 40, 74, 12, 89, 19, 89, 89]], + [[35, 21, 81, 72, 30, 84, 8, 38, 54, 98, 31, 54, 15, 57, 44, 89, 77, 5, 51, 44]], + [[24, 91, 69, 90, 80, 54, 66, 55, 39, 71, 57, 93, 4, 49, 1, 66, 43, 43, 28, 88]], + [[67, 34, 9, 62, 86, 57, 42, 37, 45, 16, 29, 3, 33, 0, 85, 77, 38, 87, 16, 36]], + [[61, 75, 12, 27, 12, 92, 54, 49, 71, 0, 15, 57, 56, 82, 98, 0, 93, 63, 44, 96]], + [[88, 97, 27, 80, 55, 90, 59, 9, 56, 72, 68, 50, 11, 3, 36, 35, 5, 38, 79, 22]], + [[63, 91, 22, 68, 55, 59, 71, 40, 97, 82, 31, 43, 49, 39, 39, 41, 29, 19, 63, 18]], + [[87, 23, 27, 27, 47, 60, 85, 0, 62, 50, 6, 45, 85, 39, 51, 27, 80, 72, 72, 24]], + [[16, 21, 95, 95, 34, 11, 9, 59, 10, 91, 84, 80, 48, 84, 89, 9, 39, 9, 75, 14]], + [[74, 33, 41, 88, 72, 6, 83, 9, 90, 67, 60, 63, 15, 29, 88, 96, 17, 32, 44, 81]], + [[60, 46, 56, 67, 12, 14, 89, 73, 88, 50, 82, 9, 65, 45, 14, 23, 91, 97, 83, 3]], + [[26, 93, 41, 94, 44, 3, 41, 28, 2, 35, 41, 92, 95, 91, 83, 13, 71, 89, 68, 92]], + [[28, 76, 32, 50, 63, 39, 48, 21, 86, 76, 14, 31, 43, 31, 89, 67, 88, 1, 35, 33]], + [[78, 89, 25, 79, 44, 76, 46, 93, 76, 96, 4, 9, 87, 63, 4, 8, 92, 92, 35, 80]], + [[75, 12, 70, 31, 86, 84, 41, 20, 64, 77, 9, 88, 91, 90, 62, 6, 57, 71, 2, 40]], + [[51, 74, 96, 39, 94, 82, 80, 13, 22, 55, 72, 43, 45, 6, 80, 2, 43, 73, 28, 19]], + [[11, 31, 42, 82, 39, 1, 79, 91, 6, 24, 77, 3, 81, 29, 44, 11, 1, 47, 97, 77]], + [[69, 92, 10, 17, 95, 81, 92, 6, 22, 25, 50, 2, 14, 11, 64, 67, 85, 28, 44, 45]], + [[58, 79, 35, 24, 18, 40, 37, 6, 8, 73, 81, 55, 66, 53, 41, 89, 80, 18, 5, 73]], + [[15, 50, 77, 36, 22, 87, 56, 47, 53, 81, 50, 45, 28, 51, 90, 31, 42, 80, 23, 73]], + [[71, 13, 82, 68, 38, 23, 34, 44, 22, 74, 54, 31, 62, 6, 88, 69, 62, 79, 79, 11]], + [[41, 88, 57, 52, 60, 6, 74, 48, 87, 54, 9, 40, 12, 36, 96, 27, 85, 56, 54, 70]], + [[95, 74, 10, 44, 28, 54, 64, 92, 13, 37, 57, 33, 31, 63, 74, 50, 37, 10, 28, 33]], + [[83, 80, 20, 23, 64, 62, 50, 62, 57, 50, 25, 52, 36, 14, 82, 58, 40, 15, 48, 36]], + [[81, 29, 90, 55, 36, 33, 75, 70, 1, 29, 79, 87, 38, 1, 10, 60, 84, 42, 78, 27]], + [[43, 80, 52, 33, 72, 16, 56, 12, 95, 12, 62, 46, 41, 66, 13, 96, 88, 56, 71, 91]], + [[17, 6, 31, 18, 51, 10, 0, 97, 16, 49, 56, 58, 59, 58, 43, 42, 20, 73, 92, 45]], + [[45, 70, 74, 9, 6, 25, 14, 10, 86, 18, 83, 76, 55, 22, 65, 55, 47, 98, 51, 28]], + [[46, 22, 13, 82, 46, 90, 73, 75, 81, 85, 97, 89, 85, 66, 77, 99, 29, 98, 53, 54]], + [[62, 53, 47, 64, 71, 92, 38, 79, 14, 22, 27, 21, 60, 76, 86, 8, 68, 99, 1, 48]], + [[66, 58, 54, 7, 50, 65, 45, 9, 79, 43, 23, 43, 90, 0, 0, 45, 66, 84, 12, 9]], + [[3, 74, 7, 13, 14, 33, 66, 81, 97, 84, 29, 54, 9, 52, 76, 37, 29, 69, 30, 35]], + [[57, 96, 5, 1, 90, 91, 51, 14, 14, 67, 35, 67, 70, 24, 40, 92, 76, 19, 87, 51]], + [[76, 1, 72, 18, 80, 93, 7, 55, 56, 5, 93, 67, 92, 62, 64, 34, 4, 97, 35, 52]], + [[45, 87, 44, 47, 34, 4, 70, 41, 94, 14, 63, 49, 74, 65, 83, 43, 27, 34, 10, 30]], + [[81, 8, 96, 50, 13, 15, 5, 50, 70, 32, 91, 46, 49, 45, 72, 6, 50, 57, 12, 70]], + [[94, 58, 73, 81, 88, 18, 17, 43, 17, 49, 56, 2, 85, 70, 26, 38, 57, 66, 50, 83]], + [[73, 93, 10, 11, 63, 14, 92, 0, 43, 33, 40, 98, 2, 89, 6, 45, 57, 4, 69, 54]], + [[15, 48, 68, 71, 81, 60, 25, 80, 45, 56, 88, 2, 55, 29, 44, 63, 24, 87, 20, 21]], + [[27, 92, 47, 33, 86, 62, 87, 21, 81, 87, 77, 16, 89, 31, 22, 17, 58, 87, 26, 76]], + [[98, 1, 15, 82, 94, 60, 96, 59, 51, 42, 17, 17, 9, 64, 97, 49, 33, 14, 97, 26]], + [[67, 19, 85, 34, 37, 50, 87, 31, 27, 0, 52, 78, 97, 53, 31, 44, 48, 48, 69, 79]], + [[56, 77, 92, 9, 62, 63, 16, 36, 99, 34, 16, 45, 78, 57, 70, 41, 18, 76, 54, 84]], + [[60, 62, 47, 63, 29, 41, 34, 57, 92, 19, 26, 18, 38, 80, 54, 20, 25, 40, 46, 94]], + [[18, 37, 72, 63, 74, 40, 11, 66, 6, 27, 25, 61, 23, 55, 99, 56, 63, 5, 32, 12]], + [[38, 34, 6, 57, 48, 30, 65, 25, 15, 70, 99, 51, 62, 96, 84, 30, 69, 70, 72, 8]], + [[9, 89, 28, 92, 30, 98, 19, 98, 65, 65, 65, 64, 32, 95, 96, 54, 12, 93, 23, 34]], + [[2, 13, 51, 32, 2, 10, 43, 14, 41, 50, 72, 89, 35, 75, 31, 57, 50, 26, 84, 83]], + [[42, 2, 80, 0, 4, 39, 84, 54, 6, 77, 99, 95, 59, 69, 94, 79, 91, 98, 24, 4]], + [[50, 10, 43, 34, 8, 28, 40, 58, 94, 40, 26, 60, 65, 35, 12, 21, 40, 27, 66, 57]], + [[75, 39, 53, 71, 99, 53, 31, 60, 6, 1, 0, 63, 50, 89, 38, 83, 48, 6, 87, 26]], + [[83, 64, 33, 80, 83, 69, 50, 55, 87, 10, 27, 25, 79, 96, 53, 88, 38, 25, 45, 98]], + [[76, 25, 80, 77, 16, 86, 25, 17, 53, 26, 3, 23, 33, 71, 52, 18, 12, 69, 1, 91]], + [[78, 38, 68, 13, 24, 86, 96, 78, 44, 91, 14, 27, 13, 37, 38, 18, 5, 33, 0, 36]], + [[59, 68, 26, 82, 87, 72, 79, 17, 59, 84, 45, 36, 41, 75, 94, 54, 82, 78, 69, 41]], + [[93, 87, 63, 56, 36, 20, 94, 5, 95, 4, 36, 67, 37, 33, 33, 19, 2, 16, 26, 56]], + [[56, 70, 93, 46, 67, 69, 67, 76, 16, 43, 30, 13, 30, 14, 38, 8, 21, 88, 46, 57]], + [[9, 13, 31, 52, 73, 63, 7, 70, 16, 93, 17, 67, 95, 47, 32, 54, 73, 50, 29, 17]], + [[22, 88, 8, 40, 70, 45, 33, 63, 97, 3, 36, 96, 13, 29, 90, 33, 43, 41, 44, 39]], + [[39, 47, 78, 66, 42, 74, 96, 87, 31, 83, 61, 80, 29, 58, 31, 59, 11, 5, 99, 92]], + [[93, 79, 79, 65, 43, 48, 98, 25, 69, 67, 17, 27, 63, 64, 23, 11, 50, 73, 77, 95]], + [[6, 75, 2, 12, 63, 95, 35, 30, 65, 28, 7, 60, 54, 63, 47, 15, 30, 24, 67, 91]], + [[73, 20, 57, 68, 78, 31, 66, 73, 83, 57, 81, 80, 47, 8, 67, 91, 66, 62, 48, 76]], + [[32, 90, 82, 48, 33, 41, 50, 19, 98, 66, 98, 70, 79, 78, 69, 6, 56, 89, 60, 83]], + [[45, 45, 29, 51, 71, 70, 58, 93, 63, 26, 43, 70, 58, 69, 54, 80, 89, 2, 67, 68]], + [[7, 11, 62, 82, 60, 15, 50, 32, 80, 6, 49, 27, 27, 57, 35, 93, 30, 29, 85, 50]], + [[70, 79, 43, 4, 13, 94, 65, 9, 62, 43, 61, 78, 3, 29, 50, 29, 44, 88, 36, 83]], + [[38, 77, 47, 58, 27, 60, 22, 20, 17, 3, 43, 35, 24, 11, 91, 82, 94, 71, 81, 38]], + [[72, 89, 30, 97, 62, 69, 62, 74, 93, 44, 41, 4, 6, 56, 64, 6, 87, 17, 79, 40]], + [[22, 51, 59, 35, 72, 73, 9, 55, 74, 54, 5, 90, 56, 78, 66, 95, 52, 44, 19, 41]], + [[23, 66, 21, 40, 13, 15, 53, 83, 76, 48, 89, 0, 80, 11, 50, 90, 68, 75, 98, 36]] +] diff --git a/test/files/continued_page.metadata.json b/test/files/continued_page.metadata.json new file mode 100644 index 0000000..b916034 --- /dev/null +++ b/test/files/continued_page.metadata.json @@ -0,0 +1,90 @@ +{ + "version": 2, + "schema": [ + { + "repetition_type": "REQUIRED", + "name": "schema", + "num_children": 1 + }, + { + "repetition_type": "OPTIONAL", + "name": "int_list", + "num_children": 1, + "converted_type": "LIST", + "logical_type": { + "type": "LIST" + } + }, + { + "repetition_type": "REPEATED", + "name": "list", + "num_children": 1 + }, + { + "type": "INT32", + "repetition_type": "OPTIONAL", + "name": "element" + } + ], + "num_rows": 100, + "row_groups": [ + { + "columns": [ + { + "file_offset": 0, + "meta_data": { + "type": "INT32", + "encodings": [ + "PLAIN", + "RLE", + "RLE_DICTIONARY" + ], + "path_in_schema": [ + "int_list", + "list", + "element" + ], + "codec": "SNAPPY", + "num_values": 2000, + "total_uncompressed_size": 2692, + "total_compressed_size": 2338, + "data_page_offset": 426, + "dictionary_page_offset": 4, + "statistics": { + "max": 99, + "min": 0, + "null_count": 0, + "max_value": 99, + "min_value": 0 + }, + "encoding_stats": [ + { + "page_type": "DICTIONARY_PAGE", + "encoding": "PLAIN", + "count": 1 + }, + { + "page_type": "DATA_PAGE", + "encoding": "RLE_DICTIONARY", + "count": 2 + } + ] + } + } + ], + "total_byte_size": 2692, + "num_rows": 100, + "file_offset": 4, + "total_compressed_size": 2338, + "ordinal": 0 + } + ], + "key_value_metadata": [ + { + "key": "ARROW:schema", + "value": "/////7AAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAEAAAAzP///wAAAQwUAAAAJAAAAAQAAAABAAAALAAAAAgAAABpbnRfbGlzdAAAAAAEAAQABAAAABAAFAAIAAYABwAMAAAAEAAQAAAAAAABAhAAAAAgAAAABAAAAAAAAAAEAAAAaXRlbQAAAAAIAAwACAAHAAgAAAAAAAABIAAAAA==" + } + ], + "created_by": "parquet-cpp-arrow version 19.0.1", + "metadata_length": 488 +} diff --git a/test/files/continued_page.parquet b/test/files/continued_page.parquet new file mode 100644 index 0000000000000000000000000000000000000000..47f32b4a8017503406c2e199ad3874059dc56c3d GIT binary patch literal 2838 zcma*pc~nzp769;ULYQ90(t-k zwgWSu4Pt={*a~6*59k63fZvVy0DnLMLLdd^Kn%iw1z-UI@B}s>4!D9quno|GAxHw= zzz7h5Ge`gouo*Cc41|CUz!o@w0N@Mk!7dOE907`;H;m8;13s#xqEwWGl2{Ca<^?k} zZy2_<`>!4Z9+Fvi;imN`1NSEZS^uDf)haCmEeI>ATH1$|%0wSKhCz7={z{^dRjh5j z@wAjjE?qt{o*`&J&Odsq6g7{O%ban-O|syglx2d_MK=u|op+m_@V7XsuW?L_JLhQ@ z3-w)cBNuLkV7dXT;0cXrZXz9R>B2tC(vKLQ`h_m77?*zBI5?d<=*lKjBIk$&A2s*# z66b=uf1WnlCSnQ2NXNBjHP=gWR*Va*xcJa_hWQNVp_l15*wVZA#3JJnkD_g^QQe}$ z1Lvlk-c@>0j5(Puhs-_`52(k3*lw*VpT=EWt^1LYZ@DGIT~70IQPZ65^HbLj#e0rq zt3=NwUFM4>l%rj>;g`F9b(Jf%3>5$L+&aIr_;d`ru{7#uUVCrtwI;-LDww{bBuet? zx=+ZY#O!&Q@URD>JC?%UU)N64OMT-{H*kzvN%HDG_HE6?_xeJ*>*yTc<^GAs{eQ?} zSPvrin|kLInftkCm>%MnSke_suEpa6T94HQ9WjAART=nj-<9UMETQ?aQ`|cq*Ts^3 zZ~1KD1FkypqKn-?YvSswlY_OG1Rp5XsNi5>@prGzTc^u`@X<(T9M~B;T_Iv zv;P2>hS^S^S(J6s8A;8Z^5RDq%6{3XZ^4Zqeoes0CpN!&{gaxen#QUFuxrau|x48k(mo;+xqUGvU=cEscwE!RdpbcNI6+B%O1K} zQbN5E#0qZRT@s#{+vTUy>pIDFS=!vc}GL;cY{J^Ls0%Zw|e z?kwigT^#+UsE5)Tx)Zg>Liu&SDSx}={HUplQ+UDCm-cwJJOCf-x8ZS!TUbZlvVQv| zSEruc1Zib|W9Fkj9L~fYUsg*Ts=}$4jBIF0Xq-+tp>>qlWHXSx!zRu7o+>)zxd9~g z9V+^g!bcko|DUA(oXi=7?B(Dft=CifH335ZsSGu)Db{p%NMN&fRF9>XyUvkJi6kkozY+U?R=#B(iS2yMR=`NI8~Gd80dc`8y2yPq@Wv)f{v6O^$usbxc#LEZ;;TN7(>Ln`GBo1m1p zEVnO{tzBogFUGXoT}-1ixxceqXI)w5zEtDd!u4n^mnI&q@G=pkCgaC~T1(7xeF}ZF zezmUvZm?r!y6shF(d%QCfR>aNgnS*t6fhYmW(@*rWWKA_$Ke zni>&1zsJ>*AZL^OzV)ge+3(rZ+w zNPJpfm45ugXns9T@9?1Z{ec0txFo^odjHhH(_O2vU!FT@mKCrvyhZ<(;5kWtf%cU_ zDd7?;{_<|DMr}KuBA~zZx0VGQ2pDNPc-w$9k+*ilWE!dU!A9Po9*_- z7a^~LL1V!flTojLzB${f#g}@-J2M`o*gJ)>V z74kjuy}1Z|498?KS+)oZ#WP(H5|!!xpV@0O{u+tm>5Ufjavpuy+IEIyOa5Q~@Jv3` zZLpE1FrcEE8bC8~3};J**-#uYSqyzFA|fo>WBt^OH@MB3%MH_9;cG31i7_l66QyJR z7?zItrmsasA`A=BJo#c+kme5kzwXfU(}dQVLMg15554gG?V4b)rdeMareK&v)4;m0 zLU!`rh_vJYMPce%Ofx@Rv+mmBusN*10~6)1@4NQp`Zy7mvG%ZbvbFK+@wNSvVj>jD z>%Ro%=WEUzxW3=oAko^PF)uHK!O2d_+4sIY*C8c4+aW0@=e>MRo;+uN=6icNPFn8<;@xK8kbgr!c literal 0 HcmV?d00001