cesar_garcia_saez
06/17/2022, 1:56 PMedgar_ramirez_mondragon
06/17/2022, 1:59 PMmeltano.yml with any sensitive info redactedcesar_garcia_saez
06/17/2022, 2:03 PMmeltano.yml file:
version: 1
default_environment: dev
send_anonymous_usage_stats: false
plugins:
extractors:
- name: tap-spreadsheets-anywhere
variant: ets
pip_url: git+<https://github.com/ets/tap-spreadsheets-anywhere.git>
loaders:
- name: target-csv
variant: hotgluexyz
pip_url: git+<https://github.com/hotgluexyz/target-csv.git@0.3.3>
environments:
- name: dev
config:
plugins:
extractors:
- name: tap-spreadsheets-anywhere
config:
tables:
- path: <https://datos.madrid.es/egob/catalogo>
pattern: 300497-11326516-distritos-municipales-madrid.csv
name: distritos_madrid
key_properties:
- COD_DIS
format: csv
delimiter: ;
quotechar: '"'
start_date: '2017-05-01T00:00:00Z'
loaders:
- name: target-csv
config:
delimiter: ;
- name: staging
- name: prodcesar_garcia_saez
06/17/2022, 2:07 PMmeltano invoke tap-spreadsheets-anywhere --discover returns the schema as expected. However, when I try to write the contents to a csv file using `target-csv`I get the following error:
2022-06-17T14:02:28.888121Z [info ] Traceback (most recent call last): cmd_type=elb consumer=True name=target-csv producer=False stdio=stderr string_id=target-csv
2022-06-17T14:02:28.888548Z [info ] File "/Users/cesargarciasaez/dev/meltano-tests/meltano-distritos-local/.meltano/loaders/target-csv/venv/bin/target-csv", line 8, in <module> cmd_type=elb consumer=True name=target-csv producer=False stdio=stderr string_id=target-csv
2022-06-17T14:02:28.889068Z [info ] sys.exit(main()) cmd_type=elb consumer=True name=target-csv producer=False stdio=stderr string_id=target-csv
2022-06-17T14:02:28.889418Z [info ] File "/Users/cesargarciasaez/dev/meltano-tests/meltano-distritos-local/.meltano/loaders/target-csv/venv/lib/python3.9/site-packages/target_csv.py", line 158, in main cmd_type=elb consumer=True name=target-csv producer=False stdio=stderr string_id=target-csv
2022-06-17T14:02:28.889716Z [info ] state = persist_messages( cmd_type=elb consumer=True name=target-csv producer=False stdio=stderr string_id=target-csv
2022-06-17T14:02:28.890183Z [info ] File "/Users/cesargarciasaez/dev/meltano-tests/meltano-distritos-local/.meltano/loaders/target-csv/venv/lib/python3.9/site-packages/target_csv.py", line 102, in persist_messages cmd_type=elb consumer=True name=target-csv producer=False stdio=stderr string_id=target-csv
2022-06-17T14:02:28.890489Z [info ] writer.writerow(flattened_record) cmd_type=elb consumer=True name=target-csv producer=False stdio=stderr string_id=target-csv
2022-06-17T14:02:28.890750Z [info ] File "/Users/cesargarciasaez/opt/anaconda3/lib/python3.9/csv.py", line 154, in writerow cmd_type=elb consumer=True name=target-csv producer=False stdio=stderr string_id=target-csv
2022-06-17T14:02:28.891365Z [info ] return self.writer.writerow(self._dict_to_list(rowdict)) cmd_type=elb consumer=True name=target-csv producer=False stdio=stderr string_id=target-csv
2022-06-17T14:02:28.891757Z [info ] File "/Users/cesargarciasaez/opt/anaconda3/lib/python3.9/encodings/utf_8_sig.py", line 37, in encode cmd_type=elb consumer=True name=target-csv producer=False stdio=stderr string_id=target-csv
2022-06-17T14:02:28.892061Z [info ] return codecs.utf_8_encode(input, self.errors)[0] cmd_type=elb consumer=True name=target-csv producer=False stdio=stderr string_id=target-csv
2022-06-17T14:02:28.892296Z [info ] UnicodeEncodeError: 'utf-8' codec can't encode character '\udced' in position 48: surrogates not allowed cmd_type=elb consumer=True name=target-csv producer=False stdio=stderr string_id=target-csv
2022-06-17T14:02:28.892545Z [info ] INFO Wrote 21 records for stream "distritos_madrid". cmd_type=elb consumer=False name=tap-spreadsheets-anywhere producer=True stdio=stderr string_id=tap-spreadsheets-anywhere
2022-06-17T14:02:28.925860Z [error ] Loader failedcesar_garcia_saez
06/17/2022, 2:10 PMutf-8-sig instead of plain utf-8 encoding, but result is the same.
Modified code:
91 with open(filename, "a", encoding="utf-8-sig") as csvfile:
92 writer = csv.DictWriter(
93 csvfile,
94 headers[o["stream"]],
95 extrasaction="ignore",
96 delimiter=delimiter,
97 quotechar=quotechar,
98 )
99 if file_is_empty:
100 writer.writeheader()
101
102 writer.writerow(flattened_record)cesar_garcia_saez
06/17/2022, 2:12 PMedgar_ramirez_mondragon
06/17/2022, 3:10 PMmeltano invoke tap-spreadsheets-anywhere > singer.jsonl
• cat singer.jsonl | meltano invoke target-csvcesar_garcia_saez
06/17/2022, 3:37 PMcat singer.jsonl | meltano invoke target-csv
2022-06-17T15:36:44.138808Z [info ] Environment 'dev' is active
INFO Sending version information to <http://singer.io|singer.io>. To disable sending anonymous usage data, set the config parameter "disable_collection" to true
Traceback (most recent call last):
File "/Users/cesargarciasaez/dev/meltano-tests/meltano-distritos-local/.meltano/loaders/target-csv/venv/bin/target-csv", line 8, in <module>
sys.exit(main())
File "/Users/cesargarciasaez/dev/meltano-tests/meltano-distritos-local/.meltano/loaders/target-csv/venv/lib/python3.9/site-packages/target_csv.py", line 158, in main
state = persist_messages(
File "/Users/cesargarciasaez/dev/meltano-tests/meltano-distritos-local/.meltano/loaders/target-csv/venv/lib/python3.9/site-packages/target_csv.py", line 102, in persist_messages
writer.writerow(flattened_record)
File "/Users/cesargarciasaez/opt/anaconda3/lib/python3.9/csv.py", line 154, in writerow
return self.writer.writerow(self._dict_to_list(rowdict))
File "/Users/cesargarciasaez/opt/anaconda3/lib/python3.9/encodings/utf_8_sig.py", line 37, in encode
return codecs.utf_8_encode(input, self.errors)[0]
UnicodeEncodeError: 'utf-8' codec can't encode character '\udced' in position 48: surrogates not allowededgar_ramirez_mondragon
06/17/2022, 5:47 PMsinger.jsonl look ok or are there some unrecognized characters in there?cesar_garcia_saez
06/17/2022, 9:05 PM{
"type": "RECORD",
"stream": "distritos_madrid",
"record": {
"objectid": 17,
"shape_leng": "13396,81708",
"shape_area": "9175481,878",
"cod_dis": 5,
"cod_dis_tx": 5,
"distri_may": "CHAMARTIN",
"nombre": "Chamart\udcedn",
"distri_mt": "CHAMART\udccdN",
"_smart_source_bucket": "<https://datos.madrid.es/egob/catalogo>",
"_smart_source_file": "300497-11326516-distritos-municipales-madrid.csv",
"_smart_source_lineno": 6
}
}cesar_garcia_saez
06/17/2022, 9:06 PMcesar_garcia_saez
06/17/2022, 9:11 PMedgar_ramirez_mondragon
06/17/2022, 9:41 PMPYTHONIOENCODING env var? https://docs.python.org/3/using/cmdline.html#envvar-PYTHONIOENCODINGedgar_ramirez_mondragon
06/17/2022, 9:41 PMsys.stdout use the right encodingcesar_garcia_saez
06/17/2022, 9:42 PMcesar_garcia_saez
06/17/2022, 9:45 PMFor me, the key was not in manipulating the csv DictReader args, but the file opener itself. This did the trick:
with open(filepath, mode="r", encoding="utf-8-sig") as csv_file:
csv_reader = csv.DictReader(csv_file)
No special class required. Now I can open files either with or without BOM without crashing.
Tap-spreadsheets-anywhere is not opening the file per se, but calling a reader using get_streamreader function. This function uses smart_open (that also supports encoding parameter as the regular open). So I modified my call like this:
def get_streamreader(uri, universal_newlines=True,newline='',open_mode='r'):
streamreader = smart_open.open(uri, open_mode, newline=newline, errors='surrogateescape', encoding='utf-8-sig')
if not universal_newlines and isinstance(streamreader, StreamReader):
return monkey_patch_streamreader(streamreader)
return streamreader
But file is encoded in the same way as before. I get the same output file. I will try the PYTHONIOENCODING setting instead.cesar_garcia_saez
06/17/2022, 9:58 PMPYTHONIOENCODING to utf-8-sig but I am still getting the same characters in the output file.cesar_garcia_saez
06/17/2022, 10:02 PMURLError: <urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate has expired (_ssl.c:1129)> . It's 12:01 PM here.cesar_garcia_saez
06/17/2022, 10:03 PMcesar_garcia_saez
06/17/2022, 10:06 PMedgar_ramirez_mondragon
06/17/2022, 10:10 PMcesar_garcia_saez
06/17/2022, 10:29 PMPYTHONIOENCODING to utf-8-sig. There is one interesting discovery I made during this last run. I added --log-level=debug and got this in the output:
2022-06-17T22:25:56.696833Z [debug ] Encoding detection: ascii is most likely the one.cesar_garcia_saez
06/17/2022, 10:34 PMcurl -L <https://datos.madrid.es/egob/catalogo/300497-11326516-distritos-municipales-madrid.csv> --output ./input/Distritos_via_curl.csv. Then I tried opening the file using VSCode. I clicked on file encoding and then VSCode suggested that the encoding for the file is Windows 1252.cesar_garcia_saez
06/17/2022, 10:36 PMPYTHONIOENCODING I get random interrogation symbols instead of accented characterscesar_garcia_saez
06/17/2022, 10:38 PMcesar_garcia_saez
06/17/2022, 10:41 PMcesar_garcia_saez
06/17/2022, 10:47 PMcesar_garcia_saez
06/17/2022, 11:07 PMstreamreader = smart_open.open(uri, open_mode, newline=newline, errors='surrogateescape', encoding='utf-8-sig')
There is a clear errors='surrogateescape' argument right there! So whenever tap-spreadsheets-anywhere finds an error reading a character, it replaces it with a surrogate escape. Error manifested in the second part of the elt, when composing the csv file:
UnicodeEncodeError: 'utf-8' codec can't encode character '\udced' in position 48: surrogates not allowed cmd_type=elb consumer=True name=target-csv
So taps-spreadsheets-anywhere is silently replacing the characters it can’t read by their surrogate counterparts. And `target-csv`doesn’t allow surrogate characters, as it doesn’t know how to encode them.
Based on this article (https://vstinner.github.io/pep-383.html ) this is exactly what we are seeing here: “The surrogateescape encoding is based on *Markus Kuhn*’s idea that he called UTF-8b. Undecodable bytes in range 0x80-0xff are mapped as Unicode surrogate characters: range U+DC80 - U+DCFF.”
So, character í (represented in unicode as 0xED is mapped to surrogate U+DCED),Í character 0xCD is mapped to U+DCCDedgar_ramirez_mondragon
06/17/2022, 11:14 PMjson.dumps trying to force ascii output:
Normal execution. Notice how it turns utf-8 María into Mar\u00eda
$ tap-spreadsheets-anywhere -c config.json 2> log.txt
{"type": "SCHEMA", "stream": "data", "schema": {"properties": {"id": {"type": ["null", "integer"]}, "name": {"type": ["null", "string"]}, "_smart_source_bucket": {"type": "string"}, "_smart_source_file": {"type": "string"}, "_smart_source_lineno": {"type": "integer"}}, "selected": true, "type": "object"}, "key_properties": ["id"]}
{"type": "RECORD", "stream": "data", "record": {"id": 1, "name": "Mar\u00eda", "_smart_source_bucket": "<file://data>", "_smart_source_file": "1.csv", "_smart_source_lineno": 2}}
{"type": "STATE", "value": {"data": {"modified_since": "2022-06-17T22:54:34.295314+00:00"}}}
Patching json.dumps(..., _ensure_ascii_=False)
$ tap-spreadsheets-anywhere -c config.json 2> log.txt
{"type": "SCHEMA", "stream": "data", "schema": {"properties": {"id": {"type": ["null", "integer"]}, "name": {"type": ["null", "string"]}, "_smart_source_bucket": {"type": "string"}, "_smart_source_file": {"type": "string"}, "_smart_source_lineno": {"type": "integer"}}, "selected": true, "type": "object"}, "key_properties": ["id"]}
{"type": "RECORD", "stream": "data", "record": {"id": 1, "name": "María", "_smart_source_bucket": "<file://data>", "_smart_source_file": "1.csv", "_smart_source_lineno": 2}}
{"type": "STATE", "value": {"data": {"modified_since": "2022-06-17T22:54:34.295314+00:00"}}}cesar_garcia_saez
06/17/2022, 11:32 PMstreamreader = smart_open.open(
uri, open_mode, newline=newline, errors="surrogateescape", encoding="cp1252")
forces tap-spreadsheets-anywhere to use cp1252. Instead of surrogate characters (\udced) you get \u00ed in the output file:
{
"type": "RECORD",
"stream": "distritos_madrid",
"record": {
"objectid": 17,
"shape_leng": "13396,81708",
"shape_area": "9175481,878",
"cod_dis": 5,
"cod_dis_tx": 5,
"distri_may": "CHAMARTIN",
"nombre": "Chamartín",
"distri_mt": "CHAMARTÍN",
"_smart_source_bucket": "<https://datos.madrid.es/egob/catalogo>",
"_smart_source_file": "300497-11326516-distritos-municipales-madrid.csv",
"_smart_source_lineno": 6
}
}
Then it tried running cat singer.jsonl | meltano invoke target-csv and it worked!
This is the output of the ETL for that particular line:
17;13396,81708;9175481,878;5;5;CHAMARTIN;Chamartín;CHAMARTÍN;<https://datos.madrid.es/egob/catalogo;300497-11326516-distritos-municipales-madrid.csv;6>edgar_ramirez_mondragon
06/17/2022, 11:34 PMtap-spreadsheets-anywhere?cesar_garcia_saez
06/17/2022, 11:43 PMtap-spreadsheets-anywhere but it looks a bit more complex than that to me. I was trying to download a csv file with non-English characters and it failed to read those properly. The default error level seems to be: in case of errors, use surrogates. So, the tap doesn’t fail or display errors, but data can’t be handled by targets down the line as it is using surrogates.
One option would be to expose the encoding as a config parameter. It could be configured in a source by source basis but… you need to detect the errors somehow, or they will end up in your datawarehouse down the line.
Other option could be selecting other error mode, so that the import fails if it can’t read or decode characters, so no data errors are introduced into the pipeline.
I am also wondering why target-csv doesn’t accept surrogates, given they should be trivially converted to non-surrogates, replacing the \udcXX by \u00xx and then to original encoding.cesar_garcia_saez
06/17/2022, 11:53 PMcesar_garcia_saez
06/17/2022, 11:54 PMcesar_garcia_saez
06/18/2022, 11:01 PM'surrogateescape' will represent any incorrect bytes as low surrogate code units ranging from U+DC80 to U+DCFF. These surrogate code units will then be turned back into the same bytes when the surrogateescape error handler is used when writing data. This is useful for processing files in an unknown encoding.cesar_garcia_saez
06/18/2022, 11:05 PMsurrogateescape to target-csv writter shoud revert the encoding in the input, leaving the data in the original format. Do you know what is the best-practice in this regard? Do taps use surrogateescape by default? Should it be suggested for targets to pass it this by default? I assume there is a lot of work/agreement done in this regard in the Singer community.christoph
06/19/2022, 9:32 AMContent-Type: text/plain; charset=UTF-8 and then sends a body that does NOT use UTF-8 encoding.
This is not a behaviour that is possible to cater for in a generic way.
Also, it would seem that tap-spreadsheets-anywhere is probably assuming that all input is utf-8 encoded, which doesn't sound like a good assumption to make, but is probably a trade-off that is worth to make.cesar_garcia_saez
06/19/2022, 1:53 PMchristoph
06/19/2022, 10:41 PM» curl -s -o /dev/stdout --dump-header /dev/stderr <https://geoportal.madrid.es/fsdescargas/IDEAM_WBGEOPORTAL/LIMITES_ADMINISTRATIVOS/Distritos/Distritos.csv> | file -
HTTP/1.1 200 OK
Date: Sun, 19 Jun 2022 22:40:34 GMT
Strict-Transport-Security: max-age=63072000; includeSubdomains;
Last-Modified: Fri, 19 Jun 2020 11:03:37 GMT
Accept-Ranges: bytes
Content-Length: 1507
Content-Type: text/plain; charset=UTF-8
Connection: close
Set-Cookie: TS01d9dd6e=01e90f9b8bf58914fdb827515e1d94aed9f2f598edc3a7b4c01d6bc75acf1c5fdfd2abaa370ad896e07271c299d098d6bd7567eafd; Path=/; Domain=.<http://geoportal.madrid.es|geoportal.madrid.es>
/dev/stdin: ISO-8859 text, with CRLF line terminatorscesar_garcia_saez
06/21/2022, 1:30 PMcesar_garcia_saez
06/22/2022, 2:55 PMcurl -s -o /dev/stdout --dump-header /dev/stderr <https://datos.madrid.es/egob/catalogo/300497-11326519-distritos-municipales-madrid.txt> | file -
HTTP/2 302
x-frame-options: SAMEORIGIN
x-xss-protection: 1; mode=block
x-content-type-options: nosniff
location: <https://geoportal.madrid.es/fsdescargas/IDEAM_WBGEOPORTAL/LIMITES_ADMINISTRATIVOS/Distritos/Distritos.txt>
content-encoding: gzip
x-ua-compatible: IE=8
content-length: 20
content-type: text/plain
date: Wed, 22 Jun 2022 14:50:11 GMT
set-cookie: ROUTEID=.app04; path=/
strict-transport-security: max-age=15768000christoph
06/22/2022, 9:43 PMno encoding information appears@cesar_garcia_saez You will need to either specify
-L (follow redirects) on curl, or use the final URL path to the file directly. You are getting a HTTP 302 response with a Location header, which has no body. Hence file will read an empty document from stdin
curl -s --dump-header /dev/stderr <https://geoportal.madrid.es/fsdescargas/IDEAM_WBGEOPORTAL/LIMITES_ADMINISTRATIVOS/Distritos/Distritos.txt> | file -
HTTP/1.1 200 OK
Date: Wed, 22 Jun 2022 21:42:01 GMT
Strict-Transport-Security: max-age=63072000; includeSubdomains;
Last-Modified: Thu, 11 Jun 2020 11:23:19 GMT
Accept-Ranges: bytes
Content-Length: 2117
Content-Type: text/plain; charset=UTF-8
Connection: close
Set-Cookie: TS01d9dd6e=01e90f9b8b1ab667e3203e7b1cee9ae5109b238a9f4efdee50568e1130396a5d325fa4a06d3d32f8b5769b797742f63984a7deed45; Path=/; Domain=.<http://geoportal.madrid.es|geoportal.madrid.es>
/dev/stdin: Unicode text, UTF-8 text, with CRLF line terminatorschristoph
06/22/2022, 9:45 PM.txt file which has UTF-8 encoding and is semicolon separated.cesar_garcia_saez
06/23/2022, 12:20 PM