From 0f1798854997363380a05f6be2f83597acf465ba Mon Sep 17 00:00:00 2001 From: Sebastian Smiley Date: Fri, 23 Jun 2023 16:13:09 -0400 Subject: [PATCH 1/3] Avro files Additional info in _sdc columns header_skip, footer_skip, override_headers --- README.md | 17 +++-- meltano.yml | 9 +++ poetry.lock | 44 ++++++++---- pyproject.toml | 1 + tap_file/client.py | 44 ++++++++++++ tap_file/streams.py | 169 +++++++++++++++++++++++++++++++++++++++----- tap_file/tap.py | 70 +++++++++++++++--- 7 files changed, 306 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 9d4474d..1fba14d 100644 --- a/README.md +++ b/README.md @@ -24,15 +24,20 @@ pipx install git+https://github.com/MeltanoLabs/tap-file.git | protocol | True | None | The protocol to use to retrieve data. One of `file` or `s3`. | | filepath | True | None | The path to obtain files from. Example: `/foo/bar`. Or, for `protocol==s3`, use `s3-bucket-name` instead. | | file_regex | False | None | A regex pattern to only include certain files. Example: `.*\.csv`. | -| file_type | False | detect | Can be any of `csv`, `tsv`, `json`, `avro`, or `detect`. Indicates how to determine a file's type. If set to `detect`, file names containing a matching extension will be read as that type and other files will not be read. If set to a file type, *all* files will be read as that type. | -| compression | False | detect | The encoding to use to decompress data. One of `zip`, `bz2`, `gzip`, `lzma`, `xz`, `none`, or `detect`. | -| delimiter | False | detect | The character used to separate records in a CSV/TSV. Can be any character or the special value `detect`. If a character is provided, all CSV and TSV files will use that value. `detect` will use `,` for CSV files and `\t` for TSV files. | +| file_type | False | delimited | Can be any of `delimited`, `jsonl`, or `avro`. Indicates the type of file to sync, where `delimited` is for CSV/TSV files and similar. Note that *all* files will be read as that type, regardless of file extension. To only read from files with a matching file extension, appropriately configure `file_regex`. | +| compression | False | detect | The encoding to use to decompress data. One of `zip`, `bz2`, `gzip`, `lzma`, `xz`, `none`, or `detect`. If set to `none` or any encoding, that setting will be applied to *all* files, regardless of file extension. If set to `detect`, encodings will be applied based on file extension. | +| additional_info | False | 1 | If `True`, each row in tap's output will have two additional columns: `_sdc_file_name` and `_sdc_line_number`. If `False`, these columns will not be present. | +| delimiter | False | detect | The character used to separate records in a delimited file. Can be any character or the special value `detect`. If a character is provided, all delimited files will use that value. `detect` will use `,` for `.csv` files, `\t` for `.tsv` files, and fail if other file types are present. | | quote_character | False | " | The character used to indicate when a record in a CSV contains a delimiter character. | +| header_skip | False | 0 | The number of initial rows to skip at the beginning of each delimited file. | +| footer_skip | False | 0 | The number of initial rows to skip at the end of each delimited file. | +| override_headers | False | None | An array of headers used to override the default column names in delimited files, allowing for headerless files to be correctly read. | | jsonl_sampling_strategy | False | first | The strategy determining how to read the keys in a JSONL file. Must be one of `first` or `all`. Currently, only `first` is supported, which will assume that the first record in a file is representative of all keys. | -| jsonl_type_coercion_strategy| False | any | The strategy determining how to construct the schema for JSONL files when the types represented are ambiguous. Must be one of `any`, `string`, or `blob`. `any` will provide a generic schema for all keys, allowing them to be any valid JSON type. `string` will require all keys to be strings and will convert other values accordingly. `blob` will deliver each JSONL row as a JSON object with no internal schema. Currently, only `any` and `string` are supported. | +| jsonl_type_coercion_strategy| False | any | The strategy determining how to construct the schema for JSONL files when the types represented are ambiguous. Must be one of `any`, `string`, or `envelope`. `any` will provide a generic schema for all keys, allowing them to be any valid JSON type. `string` will require all keys to be strings and will convert other values accordingly. `envelope` will deliver each JSONL row as a JSON object with no internal schema. | +| avro_type_coercion_strategy | False | convert | The strategy determining how to construct the schema for Avro files when conversion between schema types is ambiguous. Must be one of `convert` or `envelope`. `convert` will attempt to convert from Avro Schema to JSON Schema and will fail if a type can't be easily coerced. `envelope` will wrap each record in an object without providing an internal schema for the record. | | s3_anonymous_connection | False | 0 | Whether to use an anonymous S3 connection, without any credentials. Ignored if `protocol!=s3`. | -| AWS_ACCESS_KEY_ID | False | $AWS_ACCESS_KEY_ID | The access key to use when authenticating to S3. Ignored if `protocol!=s3` or `s3_anonymous_connection=True`. Defaults to the value of the environment variable of the same name. | -| AWS_SECRET_ACCESS_KEY | False | $AWS_SECRET_ACCESS_KEY | The access key secret to use when authenticating to S3. Ignored if `protocol!=s3` or `s3_anonymous_connection=True`. Defaults to the value of the environment variable of the same name. | +| AWS_ACCESS_KEY_ID | False | None | The access key to use when authenticating to S3. Ignored if `protocol!=s3` or `s3_anonymous_connection=True`. Defaults to the value of the environment variable of the same name. | +| AWS_SECRET_ACCESS_KEY | False | None | The access key secret to use when authenticating to S3. Ignored if `protocol!=s3` or `s3_anonymous_connection=True`. Defaults to the value of the environment variable of the same name. | | caching_strategy | False | once | *DEVELOPERS ONLY* The caching method to use when `protocol!=file`. One of `none`, `once`, or `persistent`. `none` does not use caching at all. `once` (the default) will cache all files for the duration of the tap's invocation, then discard them upon completion. `peristent` will allow caches to persist between invocations of the tap, storing them in your OS's temp directory. It is recommended that you do not modify this setting. | | stream_maps | False | None | Config object for stream maps capability. For more information check out [Stream Maps](https://sdk.meltano.com/en/latest/stream_maps.html). | | stream_map_config | False | None | User-defined config values to be used within map expressions. | diff --git a/meltano.yml b/meltano.yml index 672278b..35c3996 100644 --- a/meltano.yml +++ b/meltano.yml @@ -22,10 +22,19 @@ plugins: - name: file_regex - name: file_type - name: compression + - name: additional_info + kind: boolean - name: delimiter - name: quote_character + - name: header_skip + kind: integer + - name: footer_skip + kind: integer + - name: override_headers + kind: array - name: jsonl_sampling_strategy - name: jsonl_type_coercion_strategy + - name: avro_type_coercion_strategy - name: s3_anonymous_connection - name: AWS_ACCESS_KEY_ID kind: password diff --git a/poetry.lock b/poetry.lock index fb56b1a..8cb9eec 100644 --- a/poetry.lock +++ b/poetry.lock @@ -204,6 +204,21 @@ docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib- tests = ["attrs[tests-no-zope]", "zope-interface"] tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +[[package]] +name = "avro" +version = "1.11.1" +description = "Avro is a serialization and RPC framework." +category = "main" +optional = true +python-versions = ">=3.6" +files = [ + {file = "avro-1.11.1.tar.gz", hash = "sha256:f123623ecc648d0e20ce14f8ed85162140c13cc4b108865d1b2529fbfa06c008"}, +] + +[package.extras] +snappy = ["python-snappy"] +zstandard = ["zstandard"] + [[package]] name = "backoff" version = "2.2.1" @@ -1106,14 +1121,14 @@ files = [ [[package]] name = "platformdirs" -version = "3.5.3" +version = "3.8.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "platformdirs-3.5.3-py3-none-any.whl", hash = "sha256:0ade98a4895e87dc51d47151f7d2ec290365a585151d97b4d8d6312ed6132fed"}, - {file = "platformdirs-3.5.3.tar.gz", hash = "sha256:e48fabd87db8f3a7df7150a4a5ea22c546ee8bc39bc2473244730d4b56d2cc4e"}, + {file = "platformdirs-3.8.0-py3-none-any.whl", hash = "sha256:ca9ed98ce73076ba72e092b23d3c93ea6c4e186b3f1c3dad6edd98ff6ffcca2e"}, + {file = "platformdirs-3.8.0.tar.gz", hash = "sha256:b0cabcb11063d21a0b261d557acb0a9d2126350e63b70cdf7db6347baea456dc"}, ] [package.extras] @@ -1122,14 +1137,14 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest- [[package]] name = "pluggy" -version = "1.0.0" +version = "1.2.0" description = "plugin and hook calling mechanisms for python" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, + {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, + {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, ] [package.extras] @@ -1217,14 +1232,14 @@ files = [ [[package]] name = "pytest" -version = "7.3.2" +version = "7.4.0" description = "pytest: simple powerful testing with Python" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.3.2-py3-none-any.whl", hash = "sha256:cdcbd012c9312258922f8cd3f1b62a6580fdced17db6014896053d47cddf9295"}, - {file = "pytest-7.3.2.tar.gz", hash = "sha256:ee990a3cc55ba808b80795a79944756f315c67c12b56abd3ac993a7b8c17030b"}, + {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, + {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, ] [package.dependencies] @@ -1447,14 +1462,14 @@ crt = ["botocore[crt] (>=1.20.29,<2.0a.0)"] [[package]] name = "setuptools" -version = "67.8.0" +version = "68.0.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "setuptools-67.8.0-py3-none-any.whl", hash = "sha256:5df61bf30bb10c6f756eb19e7c9f3b473051f48db77fddbe06ff2ca307df9a6f"}, - {file = "setuptools-67.8.0.tar.gz", hash = "sha256:62642358adc77ffa87233bc4d2354c4b2682d214048f500964dbe760ccedf102"}, + {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, + {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"}, ] [package.extras] @@ -1916,9 +1931,10 @@ docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [extras] +avro = ["avro"] s3 = ["fs-s3fs", "s3fs"] [metadata] lock-version = "2.0" python-versions = "<3.12,>=3.8" -content-hash = "be520c6704788a13c5c23cfff41e5d10fba2841ff7f86a7cd9a3542344ab5b55" +content-hash = "c44b45e7a12437695e49e8b4ebab6f6d24277f99a9ce408cf9afb27f5cbef02b" diff --git a/pyproject.toml b/pyproject.toml index 18c5709..0e6ead7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,7 @@ singer-sdk = { version="^0.28.0" } fsspec = "^2023.5.0" s3fs = { version = "^2023.5.0", optional = true} fs-s3fs = { version = "^1.1.1", optional = true} +avro = "^1.1.11" [tool.poetry.extras] s3=["s3fs", "fs-s3fs"] diff --git a/tap_file/client.py b/tap_file/client.py index 30473a5..e86d9cf 100644 --- a/tap_file/client.py +++ b/tap_file/client.py @@ -30,6 +30,38 @@ def filesystem(self) -> fsspec.AbstractFileSystem: """ return FilesystemManager(self.config, self.logger).get_filesystem() + @cached_property + def schema(self) -> dict: + """Orchestrates schema creation for all streams. + + Returns: + A schema constructed using the get_properties() method of whichever stream + is currently in use. + """ + properties = self.get_properties() + additional_info = self.config["additional_info"] + if additional_info: + properties.update({"_sdc_file_name": {"type": ["string"]}}) + properties.update({"_sdc_line_number": {"type": ["integer"]}}) + return {"properties": properties} + + def add_additional_info(self, row: dict, file_name: str, line_number: int) -> dict: + """Adds _sdc-prefixed additional columns to a row, dependent on config. + + Args: + row: The row to add info to. + file_name: The name of the file that the row came from. + line_number: The line number of the row within its file. + + Returns: + A dictionary representing a row containing additional information columns. + """ + additional_info = self.config["additional_info"] + if additional_info: + row.update({"_sdc_file_name": file_name}) + row.update({"_sdc_line_number": line_number}) + return row + def get_files(self) -> Generator[str, None, None]: """Gets file names to be synced. @@ -72,6 +104,18 @@ def get_rows(self) -> Generator[dict[str | Any, str | Any], None, None]: msg = "get_rows must be implemented by subclass." raise NotImplementedError(msg) + def get_properties(self) -> dict: + """Gets properties for the purpose of schema generation. + + Raises: + NotImplementedError: This must be implemented by a subclass. + + Returns: + A dictionary representing a series of properties for schema generation. + """ + msg = "get_properties must be implemented by subclass." + raise NotImplementedError(msg) + def get_compression(self, file: str) -> str | None: # noqa: PLR0911 """Determines what compression encoding is appropraite for a given file. diff --git a/tap_file/streams.py b/tap_file/streams.py index 7df3b4d..52271e8 100644 --- a/tap_file/streams.py +++ b/tap_file/streams.py @@ -5,9 +5,13 @@ import csv import json import re -from functools import cached_property from typing import Any, Generator +import avro +import avro.datafile +import avro.io +import avro.schema + from tap_file.client import FileStream @@ -20,30 +24,51 @@ def get_rows(self) -> Generator[dict[str | Any, str | Any], None, None]: Yields: A dictionary containing information about a row in a *SV. """ - for reader in self._get_readers(): + header_skip = self.config["header_skip"] + footer_skip = self.config["footer_skip"] + + for reader_dict in self._get_readers(): + reader = reader_dict["reader"] + if footer_skip != 0: + reader = list(reader) + total_lines = len(reader) + line_number = 1 for row in reader: - yield row + if line_number > header_skip and ( + True + if footer_skip == 0 + else line_number <= total_lines - footer_skip + ): + yield self.add_additional_info( + row, + reader_dict["file_name"], + line_number, + ) + line_number += 1 - @cached_property - def schema(self) -> dict[str, dict]: - """Create a schema for a *SV file. + def get_properties(self) -> dict: + """Get a list of properties for a *SV file, to be used in creating a schema. Each column in the *SV will have its own entry in the schema. All entries will be of the form: `'FIELD_NAME': {'type': ['null', 'string']}` Returns: - A schema representing a *SV. + A list of properties representing a *SV file. """ properties = {} - for reader in self._get_readers(): + for reader_dict in self._get_readers(): + reader = reader_dict["reader"] for field in reader.fieldnames: properties.update({field: {"type": ["null", "string"]}}) - return {"properties": properties} + return properties - def _get_readers(self) -> Generator[csv.DictReader[str], None, None]: + def _get_readers( + self, + ) -> Generator[dict[str, str | csv.DictReader[str]], None, None]: quote_character: str = self.config["quote_character"] + override_headers: list | None = self.config.get("override_headers", None) for file in self.get_files(): if self.config["delimiter"] == "detect": @@ -66,7 +91,15 @@ def _get_readers(self) -> Generator[csv.DictReader[str], None, None]: mode="rt", compression=self.get_compression(file=file), ) as f: - yield csv.DictReader(f, delimiter=delimiter, quotechar=quote_character) + yield { + "reader": csv.DictReader( + f, + delimiter=delimiter, + quotechar=quote_character, + fieldnames=override_headers, + ), + "file_name": file, + } class JSONLStream(FileStream): @@ -84,23 +117,28 @@ def get_rows(self) -> Generator[dict[str, Any], None, None]: mode="rt", compression=self.get_compression(file=file), ) as f: + line_number = 1 for row in f: - yield self._pre_process(json.loads(row)) + yield self.add_additional_info( + self._pre_process(json.loads(row)), + file, + line_number, + ) + line_number += 1 - @cached_property - def schema(self) -> dict[str, dict]: - """Create a schema for a JSONL file. + def get_properties(self) -> dict: + """Get a list of properties for a JSONL file, to be used in creating a schema. The format of the schema will depend on the jsonl_type_coercion_strategy config option, but will always be a dictionary of field names and associated types. Returns: - A schema representing a JSONL file. + A list of properties representing a JSONL file. """ properties = {} for field in self._get_fields(): properties.update(self._get_property(field=field)) - return {"properties": properties} + return properties def _get_property(self, field: str) -> dict[str, dict[str, list[str]]]: strategy = self.config["jsonl_type_coercion_strategy"] @@ -120,7 +158,7 @@ def _get_property(self, field: str) -> dict[str, dict[str, list[str]]]: } if strategy == "string": return {field: {"type": ["null", "string"]}} - if strategy == "blob": + if strategy == "envelope": return {field: {"type": ["null", "object"]}} msg = f"The coercion strategy '{strategy}' is not valid." raise ValueError(msg) @@ -147,7 +185,100 @@ def _pre_process(self, row: dict[str, Any]) -> dict[str, Any]: for entry in row: row[entry] = str(row[entry]) return row - if strategy == "blob": + if strategy == "envelope": return {"record": row} msg = f"The coercion strategy '{strategy}' is not valid." raise ValueError(msg) + + +class AvroStream(FileStream): + """Stream for reading Avro files.""" + + def get_rows(self) -> Generator[dict[str, Any], None, None]: + """Retrive all rows from all Avro files. + + Yields: + A dictionary containing information about a row in a Avro file. + """ + for reader_dict in self._get_readers(): + reader = reader_dict["reader"] + line_number = 1 + for row in reader: + yield self.add_additional_info( + self._pre_process(row), + reader_dict["file_name"], + line_number, + ) + line_number += 1 + + def get_properties(self) -> dict: + """Get a list of properties for an Avro file, to be used in creating a schema. + + Returns: + A list of properties representing an Avro file. + """ + properties = {} + for field in self._get_fields(): + properties.update(self._get_property(field)) + return properties + + def _get_fields(self) -> Generator[dict | str, None, None]: + strategy = self.config["avro_type_coercion_strategy"] + if strategy == "convert": + for reader_dict in self._get_readers(): + reader = reader_dict["reader"] + for field in json.loads(reader.schema)["fields"]: + yield field + return + if strategy == "envelope": + yield "record" + return + msg = f"The coercion strategy '{strategy}' is not valid." + raise ValueError(msg) + + def _get_property(self, field: dict | str) -> dict[str, dict[str, list[str]]]: + strategy = self.config["avro_type_coercion_strategy"] + if strategy == "convert": + return {field["name"]: {"type": [self._type_convert(field["type"])]}} + if strategy == "envelope": + return {field: {"type": ["null", "object"]}} + msg = f"The coercion strategy '{strategy}' is not valid." + raise ValueError(msg) + + def _type_convert(self, field_type: str) -> str: + if field_type in {"null", "boolean", "string"}: + return field_type + if field_type in {"int", "long"}: + return "integer" + if field_type in {"float", "double"}: + return "number" + if field_type == "bytes": + return "string" + if field_type == {"record", "enum", "array", "map", "union", "fixed"}: + msg = f"The field type '{field_type} has not been implemented." + raise NotImplementedError(msg) + msg = f"An invalid field type of '{field_type}' was detected." + raise RuntimeError(msg) + + def _pre_process(self, row: dict[str, Any]) -> dict[str, Any]: + strategy = self.config["avro_type_coercion_strategy"] + if strategy == "convert": + return row + if strategy == "envelope": + return {"record": row} + msg = f"The coercion strategy '{strategy}' is not valid." + raise ValueError(msg) + + def _get_readers( + self, + ) -> Generator[dict[str, str | avro.datafile.DataFileReader], None, None]: + for file in self.get_files(): + with self.filesystem.open( + path=file, + mode="rb", + compression=self.get_compression(file=file), + ) as f: + yield { + "reader": avro.datafile.DataFileReader(f, avro.io.DatumReader()), + "file_name": file, + } diff --git a/tap_file/tap.py b/tap_file/tap.py index 5d620ea..50a0570 100644 --- a/tap_file/tap.py +++ b/tap_file/tap.py @@ -71,6 +71,16 @@ class TapFile(Tap): "file extension." ), ), + th.Property( + "additional_info", + th.BooleanType, + default=True, + description=( + "If `True`, each row in tap's output will have two additional columns: " + "`_sdc_file_name` and `_sdc_line_number`. If `False`, these columns " + "will not be present." + ), + ), th.Property( "delimiter", th.StringType, @@ -92,6 +102,32 @@ class TapFile(Tap): "delimiter character." ), ), + th.Property( + "header_skip", + th.IntegerType, + default=0, + description=( + "The number of initial rows to skip at the beginning of each delimited " + "file." + ), + ), + th.Property( + "footer_skip", + th.IntegerType, + default=0, + description=( + "The number of initial rows to skip at the end of each delimited file." + ), + ), + th.Property( + "override_headers", + th.ArrayType(th.StringType), + description=( + "An optional array of headers used to override the default column " + "name in delimited files, allowing for headerless files to be " + "correctly read." + ), + ), th.Property( "jsonl_sampling_strategy", th.StringType, @@ -107,16 +143,30 @@ class TapFile(Tap): th.Property( "jsonl_type_coercion_strategy", th.StringType, - allowed_values=["any", "string", "blob"], + allowed_values=["any", "string", "envelope"], default="any", description=( "The strategy determining how to construct the schema for JSONL files " "when the types represented are ambiguous. Must be one of `any`, " - "`string`, or `blob`. `any` will provide a generic schema for all " + "`string`, or `envelope`. `any` will provide a generic schema for all " "keys, allowing them to be any valid JSON type. `string` will require " "all keys to be strings and will convert other values accordingly. " - "`blob` will deliver each JSONL row as a JSON object with no internal " - "schema. Currently, only `any` and `string` are supported." + "`envelope` will deliver each JSONL row as a JSON object with no " + "internal schema." + ), + ), + th.Property( + "avro_type_coercion_strategy", + th.StringType, + allowed_values=["convert", "envelope"], + default="convert", + description=( + "The strategy determining how to construct the schema for Avro files " + "when conversion between schema types is ambiguous. Must be one of " + "`convert` or `envelope`. `convert` will attempt to convert from Avro " + "Schema to JSON Schema and will fail if a type can't be easily " + "coerced. `envelope` will wrap each record in an object without " + "providing an internal schema for the record." ), ), th.Property( @@ -178,12 +228,14 @@ def discover_streams(self) -> list[streams.FileStream]: if file_type == "jsonl": return [streams.JSONLStream(self, name=name)] if file_type == "avro": - msg = "avro has not yet been implemented." - raise NotImplementedError(msg) - if file_type in {"csv", "tsv"}: - msg = f"{file_type} is not a valid 'file_type'. Did you mean 'delimited'?" + return [streams.AvroStream(self, name=name)] + if file_type in {"csv", "tsv", "txt"}: + msg = f"'{file_type}' is not a valid file_type. Did you mean 'delimited'?" + raise ValueError(msg) + if file_type in {"json", "ndjson"}: + msg = f"'{file_type}' is not a valid file_type. Did you mean 'jsonl'?" raise ValueError(msg) - msg = f"{file_type} is not a valid 'file_type'." + msg = f"'{file_type}' is not a valid file_type." raise ValueError(msg) From 306461d0cfb4e7eeda1ddc1c0a550a733d4d79e6 Mon Sep 17 00:00:00 2001 From: Sebastian Smiley Date: Fri, 23 Jun 2023 16:32:04 -0400 Subject: [PATCH 2/3] Updates from self code review. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1fba14d..c1740c5 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,8 @@ pipx install git+https://github.com/MeltanoLabs/tap-file.git | jsonl_type_coercion_strategy| False | any | The strategy determining how to construct the schema for JSONL files when the types represented are ambiguous. Must be one of `any`, `string`, or `envelope`. `any` will provide a generic schema for all keys, allowing them to be any valid JSON type. `string` will require all keys to be strings and will convert other values accordingly. `envelope` will deliver each JSONL row as a JSON object with no internal schema. | | avro_type_coercion_strategy | False | convert | The strategy determining how to construct the schema for Avro files when conversion between schema types is ambiguous. Must be one of `convert` or `envelope`. `convert` will attempt to convert from Avro Schema to JSON Schema and will fail if a type can't be easily coerced. `envelope` will wrap each record in an object without providing an internal schema for the record. | | s3_anonymous_connection | False | 0 | Whether to use an anonymous S3 connection, without any credentials. Ignored if `protocol!=s3`. | -| AWS_ACCESS_KEY_ID | False | None | The access key to use when authenticating to S3. Ignored if `protocol!=s3` or `s3_anonymous_connection=True`. Defaults to the value of the environment variable of the same name. | -| AWS_SECRET_ACCESS_KEY | False | None | The access key secret to use when authenticating to S3. Ignored if `protocol!=s3` or `s3_anonymous_connection=True`. Defaults to the value of the environment variable of the same name. | +| AWS_ACCESS_KEY_ID | False | $AWS_ACCESS_KEY_ID | The access key to use when authenticating to S3. Ignored if `protocol!=s3` or `s3_anonymous_connection=True`. Defaults to the value of the environment variable of the same name. | +| AWS_SECRET_ACCESS_KEY | False | $AWS_SECRET_ACCESS_KEY | The access key secret to use when authenticating to S3. Ignored if `protocol!=s3` or `s3_anonymous_connection=True`. Defaults to the value of the environment variable of the same name. | | caching_strategy | False | once | *DEVELOPERS ONLY* The caching method to use when `protocol!=file`. One of `none`, `once`, or `persistent`. `none` does not use caching at all. `once` (the default) will cache all files for the duration of the tap's invocation, then discard them upon completion. `peristent` will allow caches to persist between invocations of the tap, storing them in your OS's temp directory. It is recommended that you do not modify this setting. | | stream_maps | False | None | Config object for stream maps capability. For more information check out [Stream Maps](https://sdk.meltano.com/en/latest/stream_maps.html). | | stream_map_config | False | None | User-defined config values to be used within map expressions. | From 856dc4151d4130280a0423fd224fd681b9176754 Mon Sep 17 00:00:00 2001 From: Sebastian Smiley Date: Tue, 27 Jun 2023 12:37:12 -0400 Subject: [PATCH 3/3] Add tests Refactor header_skip and footer_skip to process skipping before parsing. --- README.md | 10 +- meltano.yml | 10 +- tap_file/streams.py | 107 +-- tap_file/tap.py | 14 +- tests/data/athletes.avro | Bin 0 -> 3702 bytes tests/data/cats.csv | 10 + tests/data/employees.jsonl | 100 +++ tests/data/fruit_records.csv | 1001 +++++++++++++++++++++++++++++ tests/data/fruit_records.csv.bz2 | Bin 0 -> 12105 bytes tests/data/fruit_records.csv.gz | Bin 0 -> 17363 bytes tests/data/fruit_records.csv.lzma | Bin 0 -> 14746 bytes tests/data/fruit_records.csv.xz | Bin 0 -> 14792 bytes tests/data/fruit_records.zip | Bin 0 -> 35862 bytes tests/data/small_fruit.csv | 6 + tests/test_core.py | 151 ++++- 15 files changed, 1338 insertions(+), 71 deletions(-) create mode 100644 tests/data/athletes.avro create mode 100644 tests/data/cats.csv create mode 100644 tests/data/employees.jsonl create mode 100644 tests/data/fruit_records.csv create mode 100644 tests/data/fruit_records.csv.bz2 create mode 100644 tests/data/fruit_records.csv.gz create mode 100644 tests/data/fruit_records.csv.lzma create mode 100644 tests/data/fruit_records.csv.xz create mode 100644 tests/data/fruit_records.zip create mode 100644 tests/data/small_fruit.csv diff --git a/README.md b/README.md index c1740c5..ecb6476 100644 --- a/README.md +++ b/README.md @@ -27,11 +27,11 @@ pipx install git+https://github.com/MeltanoLabs/tap-file.git | file_type | False | delimited | Can be any of `delimited`, `jsonl`, or `avro`. Indicates the type of file to sync, where `delimited` is for CSV/TSV files and similar. Note that *all* files will be read as that type, regardless of file extension. To only read from files with a matching file extension, appropriately configure `file_regex`. | | compression | False | detect | The encoding to use to decompress data. One of `zip`, `bz2`, `gzip`, `lzma`, `xz`, `none`, or `detect`. If set to `none` or any encoding, that setting will be applied to *all* files, regardless of file extension. If set to `detect`, encodings will be applied based on file extension. | | additional_info | False | 1 | If `True`, each row in tap's output will have two additional columns: `_sdc_file_name` and `_sdc_line_number`. If `False`, these columns will not be present. | -| delimiter | False | detect | The character used to separate records in a delimited file. Can be any character or the special value `detect`. If a character is provided, all delimited files will use that value. `detect` will use `,` for `.csv` files, `\t` for `.tsv` files, and fail if other file types are present. | -| quote_character | False | " | The character used to indicate when a record in a CSV contains a delimiter character. | -| header_skip | False | 0 | The number of initial rows to skip at the beginning of each delimited file. | -| footer_skip | False | 0 | The number of initial rows to skip at the end of each delimited file. | -| override_headers | False | None | An array of headers used to override the default column names in delimited files, allowing for headerless files to be correctly read. | +| delimited_delimiter | False | detect | The character used to separate records in a delimited file. Can be any character or the special value `detect`. If a character is provided, all delimited files will use that value. `detect` will use `,` for `.csv` files, `\t` for `.tsv` files, and fail if other file types are present. | +| delimited_quote_character | False | " | The character used to indicate when a record in a delimited file contains a delimiter character. | +| delimited_header_skip | False | 0 | The number of initial rows to skip at the beginning of each delimited file. | +| delimited_footer_skip | False | 0 | The number of initial rows to skip at the end of each delimited file. | +| delimited_override_headers | False | None | An optional array of headers used to override the default column name in delimited files, allowing for headerless files to be correctly read. | | jsonl_sampling_strategy | False | first | The strategy determining how to read the keys in a JSONL file. Must be one of `first` or `all`. Currently, only `first` is supported, which will assume that the first record in a file is representative of all keys. | | jsonl_type_coercion_strategy| False | any | The strategy determining how to construct the schema for JSONL files when the types represented are ambiguous. Must be one of `any`, `string`, or `envelope`. `any` will provide a generic schema for all keys, allowing them to be any valid JSON type. `string` will require all keys to be strings and will convert other values accordingly. `envelope` will deliver each JSONL row as a JSON object with no internal schema. | | avro_type_coercion_strategy | False | convert | The strategy determining how to construct the schema for Avro files when conversion between schema types is ambiguous. Must be one of `convert` or `envelope`. `convert` will attempt to convert from Avro Schema to JSON Schema and will fail if a type can't be easily coerced. `envelope` will wrap each record in an object without providing an internal schema for the record. | diff --git a/meltano.yml b/meltano.yml index 35c3996..a940cef 100644 --- a/meltano.yml +++ b/meltano.yml @@ -24,13 +24,13 @@ plugins: - name: compression - name: additional_info kind: boolean - - name: delimiter - - name: quote_character - - name: header_skip + - name: delimited_delimiter + - name: delimited_quote_character + - name: delimited_header_skip kind: integer - - name: footer_skip + - name: delimited_footer_skip kind: integer - - name: override_headers + - name: delimited_override_headers kind: array - name: jsonl_sampling_strategy - name: jsonl_type_coercion_strategy diff --git a/tap_file/streams.py b/tap_file/streams.py index 52271e8..cb0eeda 100644 --- a/tap_file/streams.py +++ b/tap_file/streams.py @@ -24,26 +24,15 @@ def get_rows(self) -> Generator[dict[str | Any, str | Any], None, None]: Yields: A dictionary containing information about a row in a *SV. """ - header_skip = self.config["header_skip"] - footer_skip = self.config["footer_skip"] - - for reader_dict in self._get_readers(): + for reader_dict in self._get_reader_dicts(): reader = reader_dict["reader"] - if footer_skip != 0: - reader = list(reader) - total_lines = len(reader) line_number = 1 for row in reader: - if line_number > header_skip and ( - True - if footer_skip == 0 - else line_number <= total_lines - footer_skip - ): - yield self.add_additional_info( - row, - reader_dict["file_name"], - line_number, - ) + yield self.add_additional_info( + row, + reader_dict["file_name"], + line_number, + ) line_number += 1 def get_properties(self) -> dict: @@ -57,49 +46,71 @@ def get_properties(self) -> dict: """ properties = {} - for reader_dict in self._get_readers(): + for reader_dict in self._get_reader_dicts(): reader = reader_dict["reader"] + if reader.fieldnames is None: + msg = ( + "Column names could not be read because they don't exist. Try " + "manually specifying them using 'delimited_override_headers'." + ) + raise RuntimeError(msg) for field in reader.fieldnames: properties.update({field: {"type": ["null", "string"]}}) return properties - def _get_readers( + def _get_reader_dicts( self, ) -> Generator[dict[str, str | csv.DictReader[str]], None, None]: - quote_character: str = self.config["quote_character"] - override_headers: list | None = self.config.get("override_headers", None) + quote_character: str = self.config["delimited_quote_character"] + override_headers: list | None = self.config.get( + "delimited_override_headers", + None, + ) for file in self.get_files(): - if self.config["delimiter"] == "detect": + if self.config["delimited_delimiter"] == "detect": if re.match(".*\\.csv.*", file): delimiter = "," elif re.match(".*\\.tsv.*", file): delimiter = "\t" else: msg = ( - "Configuration option 'delimiter' is set to 'detect' but a " - "non-csv non-tsv file is present. Please manually specify " - "'delimiter'." + "Configuration option 'delimited_delimiter' is set to 'detect' " + "but a non-csv non-tsv file is present. Please manually " + "specify 'delimited_delimiter'." ) raise RuntimeError(msg) else: - delimiter = self.config["delimiter"] + delimiter = self.config["delimited_delimiter"] + + yield { + "reader": csv.DictReader( + f=self._skip_rows(file), + delimiter=delimiter, + quotechar=quote_character, + fieldnames=override_headers, + ), + "file_name": file, + } - with self.filesystem.open( - path=file, - mode="rt", - compression=self.get_compression(file=file), - ) as f: - yield { - "reader": csv.DictReader( - f, - delimiter=delimiter, - quotechar=quote_character, - fieldnames=override_headers, - ), - "file_name": file, - } + def _skip_rows(self, file: str) -> list[str]: + with self.filesystem.open( + path=file, + mode="rt", + compression=self.get_compression(file=file), + ) as f: + file_list = [] + file_list.extend(f) + for _ in range(self.config["delimited_header_skip"]): + if len(file_list) == 0: + return file_list + file_list.pop(0) + for _ in range(self.config["delimited_footer_skip"]): + if len(file_list) == 0: + return file_list + file_list.pop() + return file_list class JSONLStream(FileStream): @@ -200,7 +211,7 @@ def get_rows(self) -> Generator[dict[str, Any], None, None]: Yields: A dictionary containing information about a row in a Avro file. """ - for reader_dict in self._get_readers(): + for reader_dict in self._get_reader_dicts(): reader = reader_dict["reader"] line_number = 1 for row in reader: @@ -225,7 +236,7 @@ def get_properties(self) -> dict: def _get_fields(self) -> Generator[dict | str, None, None]: strategy = self.config["avro_type_coercion_strategy"] if strategy == "convert": - for reader_dict in self._get_readers(): + for reader_dict in self._get_reader_dicts(): reader = reader_dict["reader"] for field in json.loads(reader.schema)["fields"]: yield field @@ -246,6 +257,9 @@ def _get_property(self, field: dict | str) -> dict[str, dict[str, list[str]]]: raise ValueError(msg) def _type_convert(self, field_type: str) -> str: + if type(field_type) != str: + msg = f"The field type '{field_type}' has not been implemented." + raise NotImplementedError(msg) if field_type in {"null", "boolean", "string"}: return field_type if field_type in {"int", "long"}: @@ -254,11 +268,8 @@ def _type_convert(self, field_type: str) -> str: return "number" if field_type == "bytes": return "string" - if field_type == {"record", "enum", "array", "map", "union", "fixed"}: - msg = f"The field type '{field_type} has not been implemented." - raise NotImplementedError(msg) - msg = f"An invalid field type of '{field_type}' was detected." - raise RuntimeError(msg) + msg = f"The field type '{field_type} has not been implemented." + raise NotImplementedError(msg) def _pre_process(self, row: dict[str, Any]) -> dict[str, Any]: strategy = self.config["avro_type_coercion_strategy"] @@ -269,7 +280,7 @@ def _pre_process(self, row: dict[str, Any]) -> dict[str, Any]: msg = f"The coercion strategy '{strategy}' is not valid." raise ValueError(msg) - def _get_readers( + def _get_reader_dicts( self, ) -> Generator[dict[str, str | avro.datafile.DataFileReader], None, None]: for file in self.get_files(): diff --git a/tap_file/tap.py b/tap_file/tap.py index 50a0570..1a8933d 100644 --- a/tap_file/tap.py +++ b/tap_file/tap.py @@ -82,7 +82,7 @@ class TapFile(Tap): ), ), th.Property( - "delimiter", + "delimited_delimiter", th.StringType, default="detect", description=( @@ -94,16 +94,16 @@ class TapFile(Tap): ), ), th.Property( - "quote_character", + "delimited_quote_character", th.StringType, default='"', description=( - "The character used to indicate when a record in a CSV contains a " - "delimiter character." + "The character used to indicate when a record in a delimited file " + "contains a delimiter character." ), ), th.Property( - "header_skip", + "delimited_header_skip", th.IntegerType, default=0, description=( @@ -112,7 +112,7 @@ class TapFile(Tap): ), ), th.Property( - "footer_skip", + "delimited_footer_skip", th.IntegerType, default=0, description=( @@ -120,7 +120,7 @@ class TapFile(Tap): ), ), th.Property( - "override_headers", + "delimited_override_headers", th.ArrayType(th.StringType), description=( "An optional array of headers used to override the default column " diff --git a/tests/data/athletes.avro b/tests/data/athletes.avro new file mode 100644 index 0000000000000000000000000000000000000000..4885e5daa03636b5154d41ee0ad33d9e2abccb8a GIT binary patch literal 3702 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 literal 0 HcmV?d00001 diff --git a/tests/data/fruit_records.csv.gz b/tests/data/fruit_records.csv.gz new file mode 100644 index 0000000000000000000000000000000000000000..022614aed7ba6f689c29a81671a8ba72d0d21f59 GIT binary patch literal 17363 zcmV(L^TE5yvlzO>hes z3z&fiGnB!vcdo3i>dNXYK<5RrW$&K8WaVY$%9Zy&-~9dQ<^K6^50?+uoBw{fJUrh& zf4=$nbpPi1w~yDSzr8%%Ki@n*K40GdjsMG=PcI)ozQ3pc3;*K!KOgU(F3`RViD9{>Kg$EW+Z_Ybek&ByEI&AXeJZp>}=H^#bKn{WQ` z_~H8L0eDTOV(niMPQk z7f5$${Q0-u!EWf9>7t-{Jjzdi}?TH@7$T&Whjv@!{vkhwCT$eYko1 zboqFtr=#D~-?!y<{_E+^>^J`W^m4uV@t@b9uMe-{&ByzPYyIQV+_>9vV_l+u&wrin z-khI2-TXu^@ZoY}%1yb=;l|{2Yr_2K_RfUiumAb_^ztgVpQi05-xg1AE!=9~EZybv zA1?Kg*}iT9-Fd>_#=qLFuU;IzunR2j@flw}+y;AN({}5%+o0DUhi9m_VN8395b*8& zjmzdXDL5by8L@9y8d;VqxTM873V zQZKOGIbjOlJiWb=Yoz9-p3H-d^6nvZh;e+~`Pn z`hHs`Lpa|9xdT7m!uOZQPp{~ItACyThltl_0-Y<(gmU*LyD|4$aP0vi9cs9gt(@&3 zbR*^m_zwyL*Pmbi_)Jco?!w=Epttx4XHMRXzC|gwalS;lbDKER^yXjLFn&h%-@hZH(=_0N-gW!o{)oL|0^L7{#;-yC5;LWkVK zYmvQ7eu%w*H@o4``|}9tCQ?Mwj*sbSN&R^ZWn^T>0nQl**x%&M-#XR$jl8&Fuv^YWpdY|N>)G&FD5Mdjy5gok^d|({zIZnhWRTxrsGhQT6 z-i?!G0vYUlKyp9Z=l2nslcb!--%{*ylypSR*5l+P#Z}yN*dx4*Ba;vF_cbzk+~0_A zQ6C8$C4Z(+0;Ofa;Z7VlE(C(G&9dRpnj61`TP=Zecj5f^n`u&zxJ?e;LJL&%8b{mg zNlQr0&ysn#TZ&rn7)DJWkKB!~@xvqiS(I?0875t9gGHKfJ{K$h!{?3$B)q7oAPb45lik)qzHYI{Nu7*@X9GQrfX_Q^*)?DcY%y zu<)BPE$VT3JIDE;V^yCg;R?tcs5+z~<9vxJ8y+sv%4(!PmfV$^3FDijbLfnrC-ca@ z$ZJ`H3{E>(QU5Bb?qu5U>j5iRBf1TQ(at`GsDN1=XbBadwjZHUS;1ODdUkS zCG#fX)67XoQJriK2ZDxznS31*I~{f!59c@UsW`#~Mhuc}A>X3X+9=P}-fEb#m}(oj z+a(;tMp+@no2A-*B(2h`f;AH6JV6$f1#Wef|A=3a$Q>ui~zkd7p{_z9Af-=@Tbks8LKT=w(!f;W# zhyDIr#O8XP?mV7glQemSA1*IXRB^q0dR1hd!>Ea6ELz1q>}1h@y}v#`Tt2)i4TZ$p zzoi1+Sq@NNO+5BTXdyfE8^jGPf@XmO-U7KNkKEy)OeIIB|VK!cjFiQ=I?Lckqv)(rj&!cNXKK5ix;l3v=i;` z&e7IvqOkEmO^|Gm9iQ6*n=r-(L~phOS13~vYSeT-NZ{bF*N5w`FW2|D2X|b7;}m6H zUPs@I%;sytWXS4AIGw^ieaua1s))Wqg)Yn{>lq5MNT#E4uqYKv9Z&Ng9v^;wdAfX} z>vU_R2LNv1OV;c4C@;~<3PUB>_!z042}AzfXjbJVn%F*Q9vw6~OhAJQ6}|eysA0lv z!Z?Y}*2n3_41Gr&krm zw0trE2O-P!oAMBOf|kDh_4?-B>wmv~eEI49{TsT9>v7Ytky6Q5PM}VafnvN1FPjY! z-3IN8ocd0+;c1MpQYrv9c&cFVubNScxI5lSJg^;^cq^8ZPj5f}T0NF*-(>U?De^^1&Ga{-L@#4XM;tvFgw=aDMf&~WHCPe86HYyu zs81Ok6dg8P7#O#4_I5CPO+y#)TIq{pBI4=wf9~PF=)Y*-y|1v4K_FVhK_wwFt&g+6 zQ9vpb2NRll3lLNo>Tk#mKfR!_d8C&N1yDG^KN>J(Wg|qiM?-4~p==~@^?Hm1_|FPHzi|HmE`MO%ly44pPlE>%z4gG7ns!{q)-D86po zbB?O9 z2yj$OQTF6d;6zqP42H_C5y!(pKjuJ&0-AO6`*U|Pggjk2>os0@PJ_zc zaV@LW^|_Z2^(E|a#5bQw#bZWT_rWdd@=aDxyTkJxNY5aXP#x(QYC2`CMum`#?z1MTNS10BxEQtRs~}az4=f;tsdOICY=ytOf7m}m6h$y9 z7-_dloMG9Df z@zU8EjnnQR^y&wNP`I@?%|)$$rmn5<^5KArEN3=o?x6Xqrrn~+v@?2+)(+ADSQXL$ zHJ8{#zWd$dr^jC?3kV%T2eGRlSgFlN`uVxZWrC>46EIsU?Yz~GCBB75%xS%y+H!~e)IV0;r{ZPe*gLp*QZ~vZy$fDul4YhWS9w;i>QsYcsDc7cEgA=(?keIMPwq7 z#5v@QNx+ZSCty9Mb$cqHO(l?5&D&@ogOU9m5WQ~6Wd`r`Oo&eCD(=P+TL)(3O8rMY zA_@o4#4&PnbDUxsazix>k;QtugoxktLK3@PDyD*FrhcLlwzC%F%|Q4e?_XR1cBWMy zvVr*d^6=sEL|5n>HjA@{2eJBtSV@-|65TjPa(cb|O{_KWetKF~)L<4Rl61q#Hhp20 z`!NIE!xv85(JqRpQ9WhvzOI$%pd zC%8D-N*X^+GuV1;VlAok11!A2@nOb-$ZgDAjym-3WyReBbrOBazzwOujeoV;^*wK@ zSGhbrUSI$56MQsYORL{4!*yJsonyZ6Xy4yIzk8`RlAEO(?tPD)9XYOmeFx5A;ZDeLXtx%_XImJe)+l1fVCDR;^W*!+U#|6y z_xaiTAnw2EfYjO2h?eo;bU&x|OHAt56D>pOEh8T)bD!#SoB_3QC7bG(57(#1NBY|I2KN*Rj+)Z;XjeoB`2agyU0QP; z1a=2n*O{RiI#|^U_@A%uA74JwDK6b$!CL{mQWCOewP~Ux6qJ*8x}HTYVK%1+i9Ef7 z!JU;x8s^3iuwhCxlUBY(J%Dy)=VJ%OiobYi$lK9Q_{oDH&DqYI#Pnl+5^uO;V6YB>`E#F=~y+JBM$?X7j zfq}`PTTT3(d=n#;eLE^@gI+z@pWB&fAft9W;FgMw{ZwT{Toi(2S84veztizp`tK$@ zHZY}$igK%9Lz2z}WXEoz#~zV8(5Z%9Eo#BBe);od#|`Z^=BR)LnD&qvmDOS4g^kieXNo&k~GwvV82PU zKJC=z5D~T2hZ()bf8Kxi>GIP*uXG)erDgyY^fYm^dg48?|N7ClrdRwC$vZt%G-eS5 zHWjT72BqUVSBY<>J>V)P=J`v z1tmVOc2ce6(J_WXvDPDZQCmR*7plOPwIcN_XqqMrNI0F@ctCveVH%%~Ir((Zom&+9 z`kA-Fg*tAW(zy#~kj=@*sLUPCf2|XE6b1(!^EAyD`D`Pv&bl7#EfOEhe5u8##yGKk zPzdLYP92EC`sl7K1K4TFDIsP)S!M(()*$VvxgFPeoxoY?FH=APe4{eplyBe~X9;*S zL;g1EfLxZq&;PrA;CFZUO&6-;wV|B2S1M)5I>8W0I1PymfHJkYgP%20rgg^@FWwyG zBJL16ZF}d4XROE& zQ{iP@u?~E1QntpYGpYpZEd?TF>)d^d-F0 z;ovC8lcrEJ_K+CQrJ%Ki)%?MTF0>2IUs^qgiw zYvMfUlcu@c&url#x4z3(8OFAF9f_XW%>6wc&7+7AOJ@%AtZ5PL#w7WV&zJX9RiAoC z43%08`Ku|NS)BGbqVi0HLzcT`oh_@!j%W|xy)gwYCv74`OI%{NNjQ8*)uU52GmHki z**+QM*kRLK^iky@YB^VIWTcFcMP*|0Mem7DUtoB9B7@|>g<1b_Vy%Qbg=K7GqZu>0 z8!*a~5Igk>MZi1_w6w8hjtSQQ*Vu0hgjgc5$BB%MhvYbIpp+gJm!7;XdVn@h-NGA15V+V`M0oIshKGtY;=sO%LVB5LKm&@7lazX!SiYp7EkjQrTl%TW%h$S-UWm=o`*g2^8pHWJ!n5Fx--+8ssG3Qq*HoTBccY?cSiLrovP6K-~Bo zMb5rvfE$hik=9pRjtskPoK6SE;Zcftdv>5RgQK4}x0#_CZEmfh;Bg)@m@3Obhi0== ztH5xhF*?0t>}2}h)Kth}H-x;7ZJMHs#LY?TkJpE5eU8I_a>thjT z1$m&dfQ-QS)e#4M%D`=+6v8MsxgLw6{e(~}m6Y2CmM02+%7w{1bv}%ls^kwB0OYRq zfhypDAq95W2kYJsW?*6FBRm8W(ZpD(|qvysUo_TA;z%Rk=LV`^H3WEqyU^_Z?2i}4(j zoNe`yLu(S!2cEg%DxD~=lMH7MgdH9jQ~WnwFFmBbgbJj~n#xHp`iSI3&tT(v&EJY4VT_hTC!042=A$DmtBkkB=wNp6@U;t9eG(waFF-KBZ9 z5m$G}!=Yi#k5T5HLB3PU5$BpsB8;=Fm&@lw?GV%$p%>>`w#m9Kbgrxm3t6|R&HJb8 z^SkR)W&1g2#|dAGKw&X66sHd`9C7Ey5XO&>vdk&`a*2aAlXSu)O|H;pE_$CfN#bu) zjdU(ZD9wQDf}zw?2#{q&{~RKa2Tu6hO(;4nIOp*D>2yg*2*p~T>wOTXMk3duIn$TP zNf>KYm>@{^(C3|w@Op%a zS^#rgX`h-}!oO6047um#7=SGs4AhMdE<-!c$^o0z-3VLuyoRTZg6fgM?uDQ3bIymmTh=$)YO8PVl5iMODTIk@cLVU%Y zhpepX^`kkyDH{}05Tm?P(Fz%JWI0cSQ6h#1qSxv7L2D(s-VTcF?IP0%&f0}@PS|x; zdw(*3^F<$rqB8#D3y|R1zI$WH!%GD)sk1Jh95n`?i+xPp)*0LsR7(5|pj94-i=rZD zBk-Ap)xWMc+F!(ImJBo(G{?$9Aw%-Xt9q)NhKJDVr0C&YQimUB;eck`NoX3@Se{~8 z#d7&ZT7s5@tRc$l&DT_06nX^9<(Vcr(UabF-I|(j<9a! zQBGtwOwgsy(M+zMH*bsHbDc4{LCe4;(Myl0`7yK}HxTnQgNy3h?KP0AqU(k7wW8%J zlLzZBq!BhTW6QK?dLMR1^=4-Ec;t?rPy}}hdTdeGDw-uL|MK#1UnP=zM<~>~HaV+a zA*mC~ZV;5qJUc|$bMm?iR)y_c7P2%|x(lN=`Z$buia{yTz(MmBFw`akn z1MjwS)Etcb%VMDQMs@)fR}Q+SWnF&mn*-fk*&{1YF zRthFgQHIxrxLhg6Nw0!W1^~SFF{DBS16O`)_>n;i1i{qdkj*Dt(7-P5ub(K5zrQ@Z z{tKOp`{&1}&vYf>IgvU-pnnUxY2a~A@4|0g&I_HT$R|x@LmA1wJxtsma&rLjkU;;7 z!s@^MuX{RckeuIoU^jtV(tg!UnUa)`m-qB*x%YylBjIyY1?xMVl;pQ~1VYC;xe>_b z7aJ-^dF8hj%w4IO3D(!B>a;~01N2)HS#UhvlzCNz+A*DE5t_35){1lZTU0_mE~rFC zvchWq>`gaIHY7SbQ9V;}qW$dmZ$z${QdRv6n^iVJ&6AiNbn!-1Lk`6{q?R+|tSZe> zevS`3yz5P0!&2%{wKLm!kIYP9+N%I>Iz5gZ6qHBKHQmG~-*YP3qa)e(=J1ZZ#fGAfvqs;$ zVN@wsKdb^`G1SWCRx|?7Kec^q5?yKxhQviyJ$ctA@fsCqtt>{Y)g5VG&id4lZJ8};l~Xl_{SJ=%KIy~y zR2W3Hx}JTKEzSNMq7@m@isYp_s((8(O1x`k=|b&N=crPq=H*z;r-)TRb#g02T^ccI zgahBFd;%KX2Haa+US$d2de^+7iB&VFIGyMuncwmny~+Wt2WaxyxoH@acZYmC_b#!P zMjvmSdfR&n``!J++n*ml(1k{e#JmdkK@EqzFVVo|33rWMWYLbr%ulu$4){6?yg$%j zk=$}BEQxq3Q8Z)fTATO~1#p``cM$y~D0+&`rhVYe@|Z%82{Q+H;xz6lzr%XEc|D1=y2ln&^vg# zyx%8?2V#s5oxK4}2B?a$=DfthNP=f;G5cc%2#6t|X*|)X^)any>gm#uv!{m+8 zv$G(9m1gHb@;CV1L>B5-i-cM0V`>Zf&?RA+{#6kHHAS1ueGt0vLAX*lb8wClQ=(V1 zAQF-F;i$#IS*k;bdgDU&-J5=k4;{laxq{QiY-jS7n?SR{UPE6z5cl0fd6BJ7SZX8p z_$X9i9oj(Au=@RJ)t%ANZ6AlZ3$`sAyqfZPnV@|8@)K^L&zT3_VN1EMKUbel{Z_ol_l-Y&w9DOdRl!}T(bZFJto{5|?e2v%%aUj-U#3018Uf?{>+M;R{ zYz|f(K`FGIxrspv$n3}Lkdfn2SRVP{xO8gU%L3@}snZgDJPM%5{vvVxxDt^Q2-9B7 z#V&r}>q{WNe%9l%)0ueu^n7{!?;wZzV_gsk1PO%hly!A6qYJM2Bs=6FT z2G`+>rejd802`op90L3FFNtaH&(wTyl5#Sbzd$d3iwehm$^iGsdNh9u(<9fDoKCMp zj&UI7Q5eX*d!74c)nQVf`l2-kX?CqeRT1pmq4a%ykgMWGJjA1)tuc(XR*gTpJ$XOn z*=%~+i))ObwULf}mx*Iu40I?$Lf)%mbzTIHT@=ITmV0JS*fMi#bXw=+aDRJ+*yBIw z5(l;(e8nxDU3|@HDo9`zhguwt3F}`Lmlqu{DfqPae0**)vFtFgD>fH|<`uC+))Cns zpi2VO6-z)tawb(MMQNUQ5O8H0a@5s#CJI7dyyJ5-OU53z8-sf|R})r?Te&wOU4`r* z(`(KvsU%`Irz3|3T>TChzYqF%m}Y}_@G2jpy{s35-UQW$+Wy652~gx>N`Uha0p+@A z^*wfDs69MiMnIh@9hgh?Rw;6=I;QQ*F>Vfnu-tjlby1{`8ahwDj0tCsZAf`R$k65k zd>4dc3>AeyJeTTGwn005TE^ zAqSw*R#x6#Tj1;aHX&GKMqQaVoq@Mn+Tu%#;+d7OSz}mZg4jC^g$~E_5@qWfKWR-&8D3e-M5>_gOBj=^~LC4Hb`_GA^4y7bOEso(Cl<8?3`U(L@%5| zZ}}8noIs^@vCEc`Ofq}RCbY&+U7Jcd3e{t}Y1ru+|MBZj*PW6RTH^<9n_17^Z94gR zlEJO~AsAG_6FXfnE{xWtgg)GU}H$4Pk; z92UZzOKQ;+{{Hd|23)Ym8a707@Ci3$R;tU9!MJ{mW8{aqp-_!1i-ukr#kL@-x9RE(GkV+zTfvUJ_6!Gv zMSXEwtXQ&BH=^M-!49VC_m)OlNDfZb$ewNmo2L1{z?+VP0cUBh_SH^Zu-%tBej|z3 zyQ%WKQ{vgbOdq>&P&VML8(*Av7QS4Hzu7u(mlsZSMG^{qmk^>E5LeC`7s5WHaf~k1 z^+mtD5zVtyl0Lw)>?;K4h9MMA+?{l>=T!OlRnLUNPI=M&I{aRRN7I zZk;tgbQqQj6{O(~j9afs2y2?G!Oxgxs8b6H5kd-%w1iD|h{9Ia`WO}|`kKr0ALNuzMYi)yQzi_EFlQ+l0Qx4Mo}UOM@uM} zx)9q}wOmy{>fD44K@>6U zFq6#U$ceAgfZGtQq&={ll{E{o@A>5K?`3EdS>!6bZD29RJ_Ac4O^8FaRP~t+7qVYp z^D!JixqB#v8Xh6@a-tv%yL#q4RCbKu(s;Per1J*DQ=tFqrj8K%z#C7)V(z~Ro2x-+ zsR9#XK4@(PsX#Un$;_8gi_U)vy`hwRRkXn}w8cP?A`Tvoep1*4d3@40qRz@!doGpC zi16}a_vp{BSneZedr(8NhjdJ;DDy(uVpCIrbV$$eQdsiYy58Y78UEnal1NQ4e#8?V zj)zrL!_~l>B)40mG&Rfs&-2I3clv;GNL|Fsa2t#cAsFQU3fcBIFHirV+j#xE%O^5U zv4SYuT>!LLqZL2`An@`qfBsxwN!$*j4N$KH zD7=9K@ul&z7Wej9xVVX}nWa|J03KCfAjxz@*UXJuLseN$&OzAD!oR7nN|yJvB97ZY zdP)#g!d%@Ml?P@d zoQ|P*2RoV@`v4NlaH7YT&%rlL35|sOq7iT^lRdFj4IsB}{YkeHIgJ!}g=b#-*?=pnukt8ypw5-bsbfFQ-?c}wn`{*E3VMgFURpfO@`+j^9IY_i2$ukO zYLSEvp;%_7kyOJ{c*+obzI} zd8|QR!%l^B?O$f)RZ6`-d6fMt6`%(cc2KDOr83>okJi zUS^o43+w*x&FZ9@q8XKI25CxTyhlCXh!rWPuX@(}d!5F2BFhH+ZC@J*+P(t_pL7&ETu9CXj^7m2!2J z=S7DdO1ul6m*%*Aqnf2EcF_oRunkkNLI?5}inD>c-=zCJSFQv6+F^p_v?{c6q#$%%qi`8Refkis^ zCBpU|Lf-%U_zNB;PlwI12gR}zMJ7k)f)#m)egIBgT@L0}QpMuD_ZDXC-73anFr|jf zdPUuq$id^oGsLf-C>{<&Oo(GpCGm>PrIXmh&gPFnxX)TYMTkDT^GZd#EN^3Q^N}%i z=%<`>znQPqC?qlXF)!w^0-JTQuAq`GVrcae>lU%#Pd*Fn*lBUvt|1ab2T*x|!B)5Y zsw%2MFS>@YCCDDJ;+l?o99D5EyF%YSh$`LJ&xKf99znTS4IN$z8`uq|@d>}idRm~+yjiU>TeMlLtc7wf=1_wr+ zg7WHNi=t{uD5uf+_gvr1eAX z7yI}!YT8)!IkbQ~GgFSQBjoKY;;wUb6_rkiTii+NQ}qVq8W0Sb?5^F4RLCPMeSi6x z{uEuL@c=SHF7zU@IeXQpQXyh$_$kw+@b}XhA5g23W1PvR%1^h*0_W6clC7-d_LxS? zEKEBqWTA`5hSw*Oz{7_n**y(BKazCn!X&=ao+F9QBEl?>U!Hgv!fQ;p&oSC$ZB<_e z1J0b)c7@j%pQFz#;dd4_z$U3#% zYWlP>cXfHzLM?Rxfx1j=8boU7TkeICVLD5~#+9;|r_#Bu@#s2rTIgAsDyh_hm%o;R`TgfFAfY@_Tm)hX*xCy5ktzV;YG0d%XV2c&+_(lOIK z@~5ZE6?bre_bihcxi_p!&uN^&Sca8wiMO%WvQACC&qVfEdLYM1z&?#wbjMom6n0n~ zT1(u$1Xn>3-z&;C;dltuv94M|;5Mtg@JxA(vXz943ZKtyI@fNdeGaBnx)v=`GsmF1 zn~U3`ub-c`kwjr^B-^Pt(_H)k)fVNao^~p1&BK_V1>ee)pwmYr}Js zlCi4gCRH$o-fG&K78r{Fqn#4X$&Zvtjik~U(l&N9%~rL<>{+8QF5PL`j!}4Cuu=OT zdJ#Vdl_RH5o5&c`&Tgl$uQR&y^hO5V-Ly3v6MYcYy@`w2D=)L2aXdenF?VF8f>OuY z)#)d_*_C92h}qF}(l@7Z#*9AYm`tm7EEDc6_o;NSe?k0DrVStOlFCMrgrC8l_5{LL z7KXNK<9xB;a!LA6$Ji3yB%^RS>0|;4NKRanGbuWRZ$JN9{kW4&O6Z@FKc%YiWK^f!L4$2O;XW}Q;`NnW^Ki|0jj>6|d$vdH8b@!m7yT@@YF0wPVqk^KtCP+hDvJBS(S9q7r*#&07`6|=A`7?>B@z_U`H zpdlqW%@j;kns+(sgVTA&z}r|tl2c6(cV(_vQ1+isdK=9X^?nf;#1_h;`k--??r@;Y zr9Rn|(KJho13byy?TQ_#E8Ss36wZxziZ+AnX19(?@;%hExNSg7f0-O_+S50gWS%bJ z(I5T~bp~PCQBJ<;FS0-^AjouHXsTDp$&2MNrgCB&7{{6;bFxs~!=fMA9($SqzTl!U zuMf(kV~%O7Dv}Y_ea2CXdd!9yCR-6*<2B$>Q!+@0rd>fW^#9^mF+d{`YBCNf9K<8uM&glI6~v9Nhx2v`VTGwm1{kUmjg}{t ziDA-43zL+u^PHT3sJSeO7EH4gj}|2i8mCgJh~~u&hjWcm5|r6X>|ho`AxUv>vH=9M zfP3L7WS#P|+HjkOwMOW-rW@<@Rl{W&!$b#&6_U`}cgpf*IqmT$3R*lJL6;E5nvv^3 zqLgZR9h{X1roWYuhs2t-&@d*~d&0Qqk&*Ff=5{&{C7ZHFpRl~^?9yz_X5R%Ndsr3P^3oyKo%IJ?am9@-l=DII>3HMOZm#lnPz-j-jy z$zvYLVIC>$a*bbtLqwsQL=)|;i&@O<=hNgA+HcD+yUmpwsK2UkbeJl) zA0Iz(Z$6b{c`F<|U&De?@-GvAQ|Wk7$~yxCyO$PiK&6kDid=nwN;q`J?H8$q&9@Cr zph}BD15AfS^*iWvcb>sZyqiaTgjqvrh9iqiGk0Cyq>t}T9f=lrORsle(}vO!YMZ*@;(L|L_vACpV&MW&dKl|~y&J5h2O*H_gTjtP~d9 zwHwb^p9Z@W&Q|V{cD&0POvSR`e90-BKi$8*q6?gRbBJmC*y!Z+kX*FgwAu6$9fJ3@ zUa;q3*|}cmc}+Iqkd<|;@etfu#CoRM552=|;tBEwC>d6Zr9|5Q!{-a6FzG63pnI~m z3cX03v9F4EO=zQsZ7NOk30I3G~iG-!>WEGC0EN92`Cy0Xha z-kVRkHF6?{*T5vbq;(Uw<8uG-{QCQsH+-E#6BN)*ssE_XS68aZ)(u4zha+#&N@Y-1 zqrW^peSZDzBmE7!OcPHwz!D*&5d1lJKSAAln?ARe5SpJnltPYJpDqeZkRV%8o=0ww zk;idXU!pgW#P_4t8$&7XdmVgQ(!Z3#INN5Sytotli??u=$(ltU<%1k`kg5B$OHxZQ zZ6!^1g7#qwtMKW2qMSS+2*zE%E;Bpa;&V4ip zQ^;HoTgauHT(%UZZAu(jDAqBqil&$it06>DkTwQQcKIl&OBK%4X&UopAo(@>Cg_F4Fgx2~(EDe4ncfWIkdar7QXP|ZsWh$Yoc*Aw z$ci;4m=hCeP79lV@^S!pA3}~+8KInC^N7-m$WW&2cJlTg? z6~z|LlTS94?vy!=?1guPxG(B?=Qq7mGI_@#cKI~aw!E4IE9N()<1>jzf4si_>-Fhp zax$Xr<5E?-A(IFTy^6^+YXd#^c_5xiDm)c?Qjb3E%-X$V*p8O+rhi;Dv zw?G*>D1OU&t+CL!+zSFmcl%Pr+I~|dekN{RLk4mflS;#iGzOzLTFoX-_bw-sV^{TZ z@Uf|C+->IqMRE*Bi;NvH;w9HmMR&?T?IdkC@*Hz)g^u6q2&nz)s(~JAqMJH_EGv&f zVD?~Sk=I}r}WG)9LkZvZ&i zm(m^0@fc1kTNMbatx?Um)C)8q6WtwzT2H9qb~8b(3K=lT+|> zqCRMuP?8!mwG#?y`2{c|rw#Il&-l1c!*6wh*? zm*WU|(X5+>WPAyVRX(PFAZmu>#3CfB=R|3N2X=K+mzy^*K$CvtAdrH@sD-C_NMF3(@i zw;&f7S>EHGn1i7=fu@bBjSrH`EI7-H9kIUpq^mzK8yg;`*u$=w=oYNrQ=E8m69`*G z6=aaFSvn}2(MyO~0QAq7_t#I)_mnR^y#5P4&;9e`6Xjs#@TeYzwy*ln>iG4f#W!fn zG!P}#2gzS22n}eqKf#JO(dY4vp~Slg~ev(ZR!W@ z1y&jQK_CXAh05I7Q+YL>+^!W|ww8SI-jfYz=3@HnsIboQph#6Yj~)aDDp#BARl0dO zb{)x)kW(V8?8htHQ6GU^$C?Hyo*DP@OE!Bl+tI$@IaYPCNLIN3#Ykg>D@GPfu6VYE zN#SBZEQ-{R8}K9bOawi@D5llxEd7j-hJ|!`e^R7<`|8bN8ezEP+QKG(r3Us%!LhxH z7S}T{3v6$Xc~MbKZPvQ?vh=B-z*6!{sg0$mO)+Um`aE)raxO9Fdaj)&Bg*y6-U+?Q zqHBmvyCiy4Ekg^a4t#++XVk->iZ~Q|tj6B;aIDog8YP)Nx8DzZ;ap|30R^6k>vJRR z@ybGg=@z z-Ft*9wu9@S?(um%sca4shCuw0z6Aj5B8_w?!y$V7*X@C>XY#9iB&bH zW~-%*1Jzk1PW{i^@x?l+Vh3A{l~HOpvk--p^u6!zpWnUk-q7Nc4(2;Zdt1$LV^j#8KiX8W0 z;V(?uxT15n)X(8tUGCAtL;T-B3e*@=XSAgB&X474kWPj>M;xQUZ>3N7@R%cb0j>j4 z{D|(nk9Quq?haDQz_IEwFnZR<`s((T?p?ebm&EaoJJnmsBS%|k&}^A{Kc$M)pfjEp zTlh3OU>Q0;;At7&P@uLOlKhIjV^MtXM`rj+Y(X#(koz_=JlL*L-zy z>${hKUhb(j2;E&q7W0KnS5V`>pVZ$EQM?jH5(g3)xZRlTISA?rb>CX#d0(?-F}elM zbWv}X&{tmbjyE(vjK2PFk_h>?=XdvyANdj;EQ%nS(x=7SRFNyMQs7Gn1FMHam$6Ef z6Qy;wq%I77a+WnOix%~Xfhb|X5)P3`bXdM%s%BF-X*K1(>5 z!#BKQ$>_p4r*Ynyuqjd*o;f&_`2IaR9YXgU}WVK7`@01Co5{2(%RF^f< zbOXziw&{^x#YP=Yb*W?wi`9Aa&YNCxOtNp_5k|w1MpV@0&j54jouIrmE_PN^k~FOX zA5*Kodx%F&=tYLI>3RxTWLJCRq71u-Qq`5m4n=kO=@(>S%Fzwt%!VSPPgV_zlZ0mb zuE*$4+VSPIAh=FEhAB-#&`C7tOl*6cA4&Z;lsb1eNYP1NZe^ zWKo+(x7+AeKy5swY)S*A?vXh$O`a_{>ldZXe+r^N@|arIsh z3$&v7KndHXlT6K*W;jX~t6jcAPCSz8dTv|kFJ<9a2^bY~%6fli*!L)`H6Q&;7DmA0 zf)w?ph?}smmi7$#Z~=Mfz7j&M+>6Y8#78P!U500^5aR;6AZoauq(oYzl-r2hzVrjq zE>?pVr$nxmtz|6WcUH2|Q2^Q1{r>)^&-neZa+twA`Y=vkM|uc>%o%MNEB6eWL*Zl= z5T94h|$4R8);oU$!<2-h;c6v=k4D|^#ozD9D@$4BW2sk2qL)@_P6%hTFTUZhmY zQ7M5e=^DUQBC2TG~Hk+E)SoT+&UAv zyx}|im|sElKE=<}-ru`+SCxoc?K+^57G3#5Lk7_n2!~@^dSa{2PCPa8-iD*N@+^q@ z{PE{-(z>;-*dHevnYF8%xFs^N^#A&NVDiTQbgh1U9DoV4kzfkZ7Os7z(Xo0V$v3Q~{!WHM8m5|EK*AEJ+N+s*TN6 z=>jYe-nM1jY`0XT-7KOpuP4R#dAee+q&v8Dk|gy0Blqc8|6D^-BoE@K-AP29(;~~= zF&@{u^2{882fF${-F-w*zDX7&W1B>=f3a%p2s;p{ z4fZ6(q$Q{uhs;^?Wr9sk24n}8abg*W4j93>PX{wIM&V3`>muIdll6W-F-!NBc^z~F z2hULj`-gK565e)S-H;eMQY<0&1QOQgb1RRA>rMK%TI%FyE|M(096Zx61|hDp3+GYRn7jsLXW#{S&Y)vNC2vMKEEVV4~2qd_u4(F~_>>lw- z)kJWLJP-n^?#X2qxz2c-ThB_ofT=Bk3~TvE-iAqH)z>w<{XK?H4r_I8+5=6s(I`}j z-5Vi`q8JF$871LD#)+K5rfO6F+Hp$UL3={`jCk2Fq(h+%D+T4P!4z)@;JjT|HWwqO zBzSA0geo%qJIXFix8`3uw6ncUYF8&GcNp1yq>|a!^Ao4$G10x^xPun{XQ4NOVo=<|K%43qkhK}MRIzOm|Vkn6#CZv zq6i;u_@s~nC7VoA+qS3W;@{X0NVy=`-l?c)Uzwq4n!Q+6K<{s6NWAHR(vPrYrNd{@ z>#p^V0yRqIX@rkwR_rL?-=>W{)-_sm`LyLNWP}GwL08QqCZL$LA9#B5mD7dJXbI5NGnx+fi2OG*{} ziK0c#^}r*}EusP)>C}g1{Vsx{JZz+{G;^Xt0GO+rBngoUmz=uc_$e zjLv@Z(!kR8=`;alC;Vb?@((8@j1jhHzwy{VPP=DJ$GumFAHOgP`RWl=wl_%B#Wnx`YY9=q5rVDZc9 zlqP@enmNj%i0r@_A?&Rwj9@}}Gy>rMl5FO;S#Xz1M28m{Z3X7yZOsSCX<8uv3MyZ!HC`dDEUVBcU>e~RL9Hf`)& z$)Xpq%j#NDsro`T+L&1zOSPrFSQNx!CIVQC10H26o0yzmOhcu(w(HI@qm%D!m&CS)pkbuyw_-P? zTL4LISS9u)T8JllD7c{QOpUlxpbnAeV_E=6h zOqCgiaP6JPA{hTFg`vDzwG_@_s$h?t)#e)_3F=~mWZ+^>=>Vc6`}uJeEK@|c=4SK% zaz_XCd|rM=Agnxt1!gUchHY_8y#AYo38U(X4_)H3v)@w%R{>gIzRItH;_PjEZ+#SA z0T2b8IP7_L|MDF@Zk|6Ndjn=ZFq>Ca%5#u?_n>s6Xb${SI#;$a4W}D<)Rp&(in}IG z2F1~J{~E9%%{~aZxglXpiBPAhRgUH$k|2j)Ki`Ih zpvz2XQ^e(&eC6M9C{3)ebmml977W5C&>N+p=6GlOba5|;n65#UUtbc%a0KQMP4-&F z!K08~0SW1!cUH_I5buz^UObrWV7Va znbxoAvpzsM-T|75&bEol;oMq5jio)>QDP%fr^}1LodsGCA7w==#lk*k%+>uXDh^;K zct=jl6bMW8K0rtJ(%B>~lsS-u1y5pBSN1Ky&>c=Ar;&7$aKVxVDO46UhiFWHgj5wM z&iSs+N!B#D582D7D))N%6DIth=?%2cK4GJ`(6$flgEO4dvA`EMO(yBsc zmJ|*M#x~0aP~j5>fiw;YkRA|1zDX?k>u1|Na5eI;p+c;z?dt~arZ**A;|%KaEv+(rzk7cm zmAk+M8#aJ{Bc!VSKk}Lqc=5<%$&F}3#@taIhz-6?t2bnCrejf<1Jr5h%HUf^-$d#z z+6HR7O%m@dMs|f zuNVe&IG8Kw1|2tUG=vRcR-I~gT^SQiSP&pUO2fS@fT6dz%Z7A#4Oz{M5P1^dW7xeP z7tr_hKSuwZuX2i7EVCCx<(!Vgc%%hXFri&ALySjsLoW^QSQNqKRbo==ISHK?iOmHB z)cT8xzlfs?lYvdt2e^H_ylxXj00QQo1O9yXjtOANX_ZtzZyZverS=#@hFYmE_?mv?E^LPvdQ&U7+s`}#&SFI z0Tyv7LP{O607bRwT@dBFTuMzmEp;F_Puq+@}eyj+cY2@hh`xu32oZ7 zSYJhsv-k$K<*z%yFK}IsM1b;5R!+u5Wt0EfK5i^=-Syy6jsf?A!+sI|g$gVhioh_V z=sRB>2f;KFc43Pq`a$bAf{eXAHR3gG2Pcb}dn5h?XYs|j867h_L zn6RCZXHLROC_LP(09}$bvKd?hb^N07<4H9k3?1)EAg8o;StCtt4o8p<5IJ65I<+Hr zq9CIXa5^G^T`jRAR25RXBaf*Z`hAFJM|q%&xT0}Y?X2dBpY=x`&l{QsIKde^rxEM;r1nYl{wl!-17Km!}8oZ zaTe3R-r)~EO01<)QO!dR(!!*2FNF@Yb$H^^2B`LLLGS021TCys{0e(upn5mi$_$%dh(FK%{CLx^6@F|y`FH^>}a28F~ zH)Jft$0V9z{#z|_eN&T1=XMfP`_S-uO8{-hn6rPu88Z56Eh49<3KHIOJsfN$cr5VG zQUL9B+AWvF^q{XDlu7Hvz1apc^Lm65?+uSUHlky2Ilm^#g}&rXMY)|QQFkAHims0P zFz3AaK?ue3A9zow|7%GE9N6j~#`x~@o>y2}Akf0t7OjNyK_%;OX-oLh7M1{mrm*5$ z!;$m2h%?z{+-_=&J(+5zx(p#Tz2=f`dt3aVO=~zwuM$t;nc2BEvGNWCmS+D!JAKqh zQKx(a+kFw7YlY$9+d(*oaL_>S;^{6xsl+;@B;`gfvnXQVQqm3UTg&er{Rn>P1GGwQ zyAW>FB!2ziH9^=oQ-xVlxNS-?OMt$uSXUm>bMtgy(t$K3pEsdRp`FNZ03*OcTq5ye zq=el;Iixc?RL~8?+p%-}DUb_4$*{cr(zU_Mww9p`i1G2IYK5u6KY`i4LNbxcGDZ%S zEgEECHQ>1;%o_#8=MI&J)bB1p0%CVna8F}?`yha>%{p%a!0=ic@Mo3pp&JHDDF!7>M^!&hu)HaJYXc>)|Q$k zohnqh9Yx{$VjKRoSO3MEVQafj+`~lozvTn--Df%}ktDwF@F01eOATiIPR8YgRZz-I zgIEREtBZFjq(5lJd-vn5p3s1Hg+KyZ8YPq#d>|m^Z#JYFOI{Eq@F0wEdW9hQQ3JFt z8-Wup?rG2epdoyq$Eu5B=4`CPu>o(FTGKX$(gJ5^6Qg+zfl_^(d|QbmfkvP;DcdzT zcyzJcfNAM4G8c?&cSOG|v3o{I#w(tG1VA(uRR##L;;`Q=A!x;<|1TAUvtON2%0*{L zP1b@UL0D~5?Xr&!JU%>W^vtQB#;!+tp4D4okx)+O8>WXGs4lFp^h_gWZ;ifsS!dL1 zIj`+y!Avi6zo)30w$fPcETh|aImfIhw56W7LHYijQJmiImOTxK=?wfv=R7(P2l73P zWkVbUAdEu(yG=nrOby13$~?aLQl5@J@sKlf7%M1}CWHqsjM zy;v_tNS~bwE&K&6(4?z%QqH!+R|1f)$go<;E!yr*Edr>3529a;+eq1y+93~~T>Wjb zOGCnNWAD4EI$%f}!J-Fk5JeS0xTUzr3buGp4SkHShg|esg&|om3uEO&iV9Kf=bG1h zSHu`fDpxI5ulZPfz9(gGL=|ItP|^#cceRY%nEsD}jb!)2U#Gcoja;_lzFLxB#``$z zS+xKaQ-?{#GC8&*0svfUWf4+)3Fk?!@R*lvw_T>;SoTn~k#T8C&d!aj-!?A%v|^|C zW=HToimnzdYjvL=NR5En@?RQ`F`^^{0yqEZ0kXInQ{#|N=GHlaeQ@PtwQrxo)}@XA zhoKAM!8ps+yYzzKw2zCgxXqeH`}OAxJv7Y-Ux2mb(=`8B(=z+ng-dk{y2mL@N;}Rx z3R;HzMj_i0>T*=Uk&hm7i3*XITd`BXCoNQ1NU-_0DLQn0aesvK?=Lz_tK|BX6-0gX zoyodYystwwXF&uBG2aa9(0`vZ;W__R4)R9ercj=0Xqaoka%c;#(WPstuUnTQuWFNJ z3KO;HL5^#8!^OK$ZyakBpKC3rpTZ&ub1Z>I75VbRjjSpn$c1kf7+oUjCsXx#sCG!| zXMUZw@REY?d)hi3OVRp=|6J*4B~bqTIy1i)mKgg0|Ahf( zKC)AUDxrGyDJd8A&vr2zghd38>PtQGI*8T6Mfdaesy+w zl`=aL6hXkCuWJC+Ul$@hTBJe`gV^fy19W!#>*4_!-QmTXsrtY}^|&kSK`vyY)TH>x z8#++X+v^zDhv7}9X$zT!B!pk=(vyh~4XK%j79B^NMtcLyQ;%ZO=2C}8L7Kr`F%Q_3 zlQZ1XCUJS@KWI<_+~f`e=3)Ij1p-2;+((t?g5AWwjEb zyyc^6!7)9x#cXal8;9~8+x)^p#nJmjRg=$K=rG#S8^r>q!Q@6Iaml`)Nw>8v(p6xz z*9bfUQm(QvPF@sQBYzeyJ^&15qpL#%Iq>gQ7^ixo25p0Czer)M>2NAeiPO&!R75P-o9&{l^v<$ zlft|~m>zV(V7duu@Z~f0s!9ayrSMdkS>Lo6p4WerV$|J&5J|OTh=aG_5@zyCXr?x< zLGKJ1xEF3C&^wpinD(==wZ*qKmo8D8kC}RR;Q0m~Gd1_-%B;SYztJvU&_7d@n+Ij| z^q@8OjI)QUyIa<)p#DFm!>7#untxWh0aYhdN5$SZ3CWbX!J1sR0%ZOd^y(w&&s}~1 zdRNj1ONAp5^@Q?(KD&|uh0cdvNHX8iBmIX5DD;W)dAi%ax&=)I8XZeZ5Ld*Eh}Nl4 zVH94PIPPU6d2D2eJM~^%KHBd7YS5fBKIg<~hImoRSZ*(2nm!snlEXfDuf1H+d71#2{r$)MRd?DJH9`2vvl8t7o zxZ48Q)ro8;Mt4k*pPkuQjcn4Nc>hh{wI*rZF15LxG1U?w0xS|xiV`;HIJ5u zvwX7qSYG6`rLr*|%MvxIf^iO$fVB6D?k5NUb2haVgRiUxw5hGYjW^`{e)#`J#P6R2 zYJo9?oAF3K`$e9mA}#{3#uwEKv=VNfC3OUGD2x&=@cQj6BU6WwVG1&@(kcm`g?WOP z3m8R;&bls$L+TDcx6p}NNVDRM7|w_M+7m3bb4H%KL>ZHZW3*h<4_yn>4*YaWnFWy_ z3woaSli>>tUT!u}K+zY2B>qY3+e1n}0dLO1Fv1B(VN;3iueR#{Ohv&w>aEhAUoYAi0)zP9yi?1og3o+J-L zZ%=M3!*4`BM$XU2gx`!9Id@^whNIQfhGO7;?PP5{o1T`O@rstK0CU=s9s}*OSdtkS5LN19HP>ODcW*#s zDtCJtqF_-Ls}Y7Rn@wA+nKYbP2KDeviX>$Hk2^+otMvxje%+7uCtZ&ycUiTn3X?YC zjq&j6XaSEWuJ5~INQE_GxkI4`yD7^uNDw^VUpN{z%J;bWOUU3iy2;0K*jg=AM)iqD z$3JJ#&aA|a!_cbKgNb$F!>}ErX`C>SA`DUo`X|~`e}qLU2~?X7+Z-Fv`CD2xl>0_~ zx;=L4B7MMaJ`Blw8lW}p)IU?exO{FVh=%1Zg8_=@$C)MB0Pt$6;j=mM_DJQ}0-I0fqVa-R-#%I9U8$T68tO8T&ebogQkMcBDt%Wuhf;h6`t zBv=MdaqBXyZVC5Lw1tNhbl__al}#{&?oiDV2U7f4VSUBv6UeBc4d5ju0l2RE4$DKQe*W;dU?~?UXl6lkT8F0A^j%M!cJv7Fsx8V0po)LTRdZt~vgSTeTjVKFpN=_B^9%Mv zQsuYv=(U3N6;Qezh8^4nT2ksdK{!hN4)@0-!RcXiQPtsP~M#9rsReH`rXa<+OJ|IZIq-Ys?0v z0gR3@$spoNAeJiE-{j42^<|)}+A59{OnTd(hfO|ZtV}2g8lhS<+E4Qp$a>gX6mO;zxMEZesRirYxK_r(=_1KZkmNJH9Uiy zHx1-7JbLIEynqOp>=@a3$QnH^{!^w)BQb;DvM7aVd~o3`Hd5I30}+!STiEb)2q!%4 zLJe})%ssS@`;dMj7?~bz_C@a_Cnxeeqq8>LK*~SYdqf?NYuPj z$DsW)%9Ht>v>^q&1)jep(Gumb1<*cAiAKA974GT64TyLuA%biHYPbFRxns|{&d<9*iLcpCPp|$6kKI9|>LE0J(SW5)$WI*~rf4gw%3y3i z757liPr;GSD(i}Ke>kRaY*+*S1=Uys`Ieo=sK#eGH9gfFl8M{$IR!8AHGZieXI7`f zI8#fkQrZuS?0x;0)8=U4CB#Lux1V?L6*1FYH*4XygIQ>9&eW{#a_Qs9lXmhcahy=< zEz=|F9RuJJ!e$ud@=n}kk31zX|8#Khxh$IH&q<8~V#4HQfp<0{vNWXnWlKXrwV7o$ zyIIvszup1CU?Z+JOkypE!SK&L==P^3!LgNchaQc70uT9xX=^&m1VOYFM>Z_>;4W3K{?(7SP$j2_)GYw3X*D@l<9>6EcU z>-yW8Q@{bWJ=!rL52>|C>e=5&UbuRm>uY7qfrer~Kaww=GgO&X;`z=&x}K__C)Uao zlA_WeJLl!}NYW`)FSv|=sQ%Q>{1J!IWb&RkALZD85jVE=Rm-B2acDL3y zi3uuYX)TvR#m%w$()w&)s*TznRr-6kJzeDxIu~iSelR?+2gC;^%TD8|Lhj* zj*hds*u%Y2HfkP6c-`#e5^q5Q__%X~b8%a~CxW)0D&vQD*T<9Mn(vfguCZ*ZPj)8E zDKAA;=`72UPA`s{Hw52p1C1>bw8$phGNJ#l#MS+|+}?9?#A$g-r`mxK_pq3ftvWCw zl&{Hh01tbNp8N`Rw9$exP8D8((xSi(_5H3AYiTfB2vHDE?PY0+!F%0?#@bC1D=U+LKjO$4jyT&Pc^b6WdAxBs$@5or;_yX(LJ%X(Jn-e zq(Wf^oP(M_CT(NXvWVp8?-lyHk4d5|LrfkEL#of%N_ZzGaZ+e!evaU2mNDzd;2L6R zX3Rqb3hXmI*$-f#A{v%ogV#?rhP#w4NmV1o=`>7l;2jg61?5i z`FmZ$wxF>y-QjC<_p9{0hv}W2oqC7+Nd_yV`INd6Z0Ru_998iC}2hPIUD7~FrRxq^s&sT2+kZ+#+5 zlFc}F<22yIiX^yOmd}^eP@#mz`R~D67n`>+o9E^9i_PBwac23-hVM8}Lg*h+_R7uK zcP%4<-h^nV#VjgytPqpkA+Hri7$2{ zqWq8kmA){W#Jln#CA=?X$!rb%3VsPxZ8Q5KJ~6%2uYn!*ULoNPv&xzVF+XSn*xGKL z-9@BS{9%asYQr2;3!t(65D3fgg?O8%>R;0Z3QBk+fMwx33CHA?INmDIZ)k&x z`~P#4O#rn%UWD2^L0vIT68%3ghnjt=rk5SS`7t_xm39FDuh0-X@7N&$ob-klxfX?Q zG~!tM3-i`FT|aaOla)B+)M0EPGhV(ApM4Z-L#_5h%@TONfNMBw4^rd=YnHQ>9R zNBEy1S4Kc4F(g3rv0^OYq5W|Qo*uFTN&h)^CS-(`#n;!zPESB{ylTr`j>swa5#;L6 zh8w`A ztlv?Fi=b4z+z(caEx{BY@wGI=YNess*({sUZ5AmJZW&SGFDHATso<#Yu~lTsZyloH zmJEIBboEfCoSsS z(OLl5U;VDz*G%(s8?Fz(&EK-vZ@8DX2yhghV6f(cT|}n)4%S->LGAqhspv>g1&$VBU2g5IoTSLI)*Jq7$hO zWKX?Km*pBF;NgC^*t<_{3K}mqNHXMkv{5}P9K14Q>?re0ry2J7y2i7w2+k17tgNI zi8mis9#7t72}K}R>O|prN+O)tkfwzgZA}mMAZ7(0AX&TBZ#L4``sGP~EOK@RE8FZI z7!?zX;utaI5x?Jmm|LL^c7-!=2VHWa#oc`K(1skySh2`di3kZ8Ct}M0LM|NAl&_K# zfUNF2=98t*PuZO!=%G4LItDAqm7ZBLd6j2RO@oo#2_C~zUJ&+E_hLBsfEzFXitcH! z(;gm|lLN~d^b>=Su$bC6+5*aTdf7m+64tDSywg^iHcp|1?@KYA>^TWzoeq|5(wCwa zdH0Rw_?33=v(RNl7-$VKG=VKm+b@#`zh@-6@Lr4uqlh0VEMhW3Z(ZoI6xVV@(020M z9Yfw>)~EY@M(PaA<#a0so8k!P>!9=3+cN^sWmAa$2~J~FdpXyBRkGr1<`)@1_)D%b zVu^Mhx6Y}Y7jhHFl@32^E$fQy)q3MswRfaT{EFeSY>@+ zs*M?KYkA||Q*5u{`ZL`^p;=t8GolbexZ;^RPf@}Yyj)BcIK?@6KvXQ$Zsv~1ucCyr zi&hFn4AU^~F@2e4Uo+_S_vtD8r2i1_CCa@cp2uN4og9TDN-Mooj7_es^_nB3NPV~b zE<=#-7L9?8p9?IkuP=STAPa9J@rlsfPUlgsf_NrXZ7tN+g^h8=Y+`ie`#IIWP+EL; zFO=P+Heb$5r_2lfdC(^oA(flKJvpB>YT5O=6dstfC@Sn8vdgE$6ZWx}F)A#+$VBt3 zk_MRW!VujRBmz*Aaar#+!T)+)07YXLTAogWf{>Jl0MWZg8+shVrsX#x)lAa20^t?zH?T z6X(rhSMYQM9LAwhEt|$nbZbwWgbKj_;X(@m3sD#}NM$pEGm-NaxFo0rKp%(EQBj5W z6hs0WJw%F_*LPZA+np!?vvkumVr>Te@}%(C{{@m;q8Rx#%+U^iL&B=Wa-d**uozBO zR~ccMga#w?5cozpSKqQ~GrMhvQ~i@q^&54Pnm3I@*_|oh+NjU*eBI;~i+hVZkp{+( zXb#*XtRjO;^<248V0)c;)6v8-(>UWG*+3+}1|i##Y-Yy~m6apu^ttPt2}ryXHPE zF^eC1rf%P;T~_O6G_`ub;WOp7Nu9WfSu)V&79Sj|DOYcSK zz}NqbCM*K!qo#OcHFb`NZXuFTtZ~Yfo17ca7W|#nY`u zl;vf{?ehuc-AgTN?5G$KmF0GHj*d9uz)(Cg>x?Q8vGgZbYwU=%o}Q`KbP&pX6lj6W zR_Gf$sza3g)*&laeA99BMaQ(Uc{-z!uMM@}O520uPe(<`lhj6ns)=QI;Q+xROjI>-ff)_=?GxB(?S*boNRIlnQu# zWfqla%M9k(YUe#e(K}tnc|DBTyyeZzU`O~}ekLAHpIxZZMG&8Hi3=K{0s|e~6I&(# zr+jE`Wtq?5N|1X(gKLxi!bg><9EOOJF`6s;&NySU^+z9FI{mUrr1f!3((+Jn|AG=!p_@jH?lmX zW9Ss@jtHaL(CF73m8fg;MB!~&jXBZ4Ntm@$mlL#s$6-)-xKv(oa5V{lwW}52hMn$% z85wZsrF)6eeb@eEg1)$Cpl&=CSN88vN%h$O(x!TbS;hiWTqIV$aBO;L1E&me3n&ww8DzUfrmi5dCSWRz_Cj$9HOH| zDOmRjc2K*{WlH2@PbXyKy1F8Mn7XQW8!p z!jB>34?g{Dt+I7aFqgrMxGKC9Qj&SC?)~YgXLQIiT}z#?7c9NU3BP`# zL*;Q%r|w`bo0Pi5arFCT{U`P-Zez|ye=Y~^-j}eNqa@C_K90zz3o2-OCHkKGP8#C8 zfvz_8{r-TJiRm%di8Y-K?=uVU-$rTbmtd(NP6@@O58CuzEUnrfPe?1F%LWDJ41&wC z^+ix>=t)fdvaGf?-RQ8!@Vm(oIdx_?IR3C7e70JWElJmFGhb7nxQMsj%eD4utofu> z&M<@9#e*!0&33ew{<)_z;wRYJEhtI)Z_yw5dM4zHP>*xhy89%>xll+nm=Un6)q0jQyUApVA0Z9(y&3lWcdjVQR*qQ8Xk!?0=seGI%T#T zF#AzOeE1iQVzyYGG8(J0+JXip4T|(RaF5NDYFOL1%J^?j4W;Cn3jMT9C}B@qQm|5@ zF+rp{w28lOs=5}hM{L)hzXk8@))O3PJs*{#n%O8)Ubxgc8-dc;1OR%~v#w(J<>Lr0 z;R_K>KxL^ENmBy7Xg&Oxs4h7H(fv@)SuiY7ad#u=&I$2 zCL%ty}9CM#e%;plmG-21$_`lP@xTmW>lx z{hwqrv3L2TI@U;;cr1%vWpj49b)|txdlxhUBXdKkh53{C^qW4~yc*@n->2u2H~L=m zx)HXfNWPvuMR~Ky#=@?+fULOp9z;JfI}H+jg5tS^BGuJPN~#I}ykv{ zAU411pt6p*iAnz-XI2l}wK?o-8-p@Ml`_fFt>bd}m})eA%_n_bufLLpqJ$Zry4|79 z&fBl4@!R!oF2P1WZT71!kTSFsSO++#aNv{>J$8x%0NJQ;TB9*8n+abKS?`I6T{|s} zJ5Nv}^^hFMaOW|sJk2MOC)3>f8zg@NY=W*0<8=GP60f0?|1Xq=2Vh7}eGah#&&nO# zSc|nh%#QSj`$#zy47o!vkIu{4TXH_59;^3Xnjf8*=wCyXT53@&D*>)J^10?X`W%m- z14SA283V;#NqikH*sX1c^B>465M;egh%-U9M6&SbQcyh$5AFD^?`ZW;Ah7l4*_4tGlu@c^wy z_S4>HFb1;_Q_Z1Cq$=Vb7u{SS;TZ*TIXN|5R8u?{|Sd+VpHXpWr-& zg4t-rW3mSIH$H@}exmV%Vw;mqRvLt{J299P{PU$aPMc(DAIe(t2K-Ux?ikU5SiI4= zQ28+grqIP)<&)pAA*vK3&U6gvjyWBgk_JZYyb92XZ63H6knP`utIW!qS|H;2K9^9A z`hRba&D)c<&1Qv|3}%*s;vcK3;S}X_R#?4_g1enlZLH69&!eq*5gK;&wq>}Yu35jv z4ueYwa{ggoNroW&_b%9vqJhfWJVn6i+n~OS?%ypA^4xcp@&X=JXeo5^+y*pCERhM9 zeiT+}5hgpcKeu3(&hBJMokeThJwCe7;I@h$z7+il>0)9yNVeEM)9_tMUeXLX0)UsN zcE{btOG%L6`}i~C?)$Jduy`UoFAs-{EmJ36D@>m8?GMFy{_=z~f&7;AtW@@+H2*Xq z@5P8```m?#4T^=u%E(OwPhwJ5aSsRC&}2=EqgE8uYqalF|;3zWJi&Y oA|vfm<%^(KR-QQ;9q_xz(a@xbWMu?Y&@-OlTcpJ6|Gsc8@)qR6n*aa+ literal 0 HcmV?d00001 diff --git a/tests/data/fruit_records.csv.xz b/tests/data/fruit_records.csv.xz new file mode 100644 index 0000000000000000000000000000000000000000..bedc4ebb720cc9206dcae380045cf20627766540 GIT binary patch literal 14792 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

Vg6e!a(z>iN9T4DQ~S{HdP@Lp$C$H!!5K38 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

>3+H=oo2OmuF4_&OzPGf;c?{(m$2Lcb0 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 str: + """Gets the directory in tests/data where data is stored. + + Returns: + A str representing a filepath to tests/data. + """ + return str(Path(__file__).parent / Path("./data")) + + +base_file_config = { + "protocol": "file", + "filepath": data_dir(), } -# Run standard built-in tap tests from the SDK: + +def execute_tap(config: dict = {}): + """Executes a TapFile tap. + + Args: + config: Configuration for the tap. Defaults to {}. + + Returns: + A dictionary containing messages about the tap's invocation, including, schema, + records (both messages about them and the records themselves), and state. + """ + schema_messages: list[dict] = [] + record_messages: list[dict] = [] + state_messages: list[dict] = [] + records: defaultdict = defaultdict(list) + + stdout_buf = io.StringIO() + with redirect_stdout(stdout_buf): + TapFile(config=config).run_sync_dry_run(dry_run_record_limit=None) + stdout_buf.seek(0) + + for message in [ + json.loads(line) for line in stdout_buf.read().strip().split("\n") if line + ]: + if message: + if message["type"] == "STATE": + state_messages.append(message) + continue + if message["type"] == "SCHEMA": + schema_messages.append(message) + continue + if message["type"] == "RECORD": + stream_name = message["stream"] + record_messages.append(message) + records[stream_name].append(message["record"]) + continue + return { + "schema_messages": schema_messages, + "record_messages": record_messages, + "state_messages": state_messages, + "records": records, + } + + +# Run standard built-in tap tests from the SDK on a simple csv. + +sample_config = base_file_config.copy() +sample_config.update({"file_regex": "fruit_records\\.csv"}) + TestTapFile = get_tap_test_class( tap_class=TapFile, - config=SAMPLE_CONFIG, + config=sample_config, +) + +# Run custom tests + +sdc_config = base_file_config.copy() +sdc_config.update({"file_regex": "^fruit_records\\.csv$", "additional_info": True}) +delimited_config = base_file_config.copy() +delimited_config.update( + {"file_type": "delimited", "file_regex": "^fruit_records\\.csv$"}, +) +jsonl_config = base_file_config.copy() +jsonl_config.update( + { + "file_type": "jsonl", + "file_regex": "^employees\\.jsonl$", + "jsonl_sampling_strategy": "first", + "jsonl_type_coercion_strategy": "string", + }, ) +avro_config = base_file_config.copy() +avro_config.update( + { + "file_type": "avro", + "file_regex": "^athletes\\.avro$", + "avro_type_coercion_strategy": "convert", + }, +) +s3_config = {"protocol": "s3", "filepath": "tap-file-taptesting/grocery"} +compression_config = base_file_config.copy() +compression_config.update( + { + "file_regex": "fruit_records", + "compression": "detect", + "delimited_delimiter": ",", + }, +) +header_footer_config = base_file_config.copy() +header_footer_config.update( + { + "file_regex": "^cats\\.csv$", + "delimited_header_skip": 3, + "delimited_footer_skip": 3, + }, +) + + +def test_sdc_fields_present(): + messages = execute_tap(sdc_config) + properties = messages["schema_messages"][0]["schema"]["properties"] + assert properties["_sdc_line_number"], "_sdc_line_number is not present in schema" + assert properties["_sdc_file_name"], "_sdc_file_name is not present in schema" + + +def test_delimited_execution(): + execute_tap(delimited_config) + + +def test_jsonl_execution(): + execute_tap(jsonl_config) + + +def test_avro_execution(): + execute_tap(avro_config) + + +def test_s3_execution(): + execute_tap(s3_config) + + +def test_compression_execution(): + execute_tap(compression_config) + + +def test_header_footer_execution(): + execute_tap(header_footer_config)