python-dwca-reader 0.16.4__py3-none-any.whl → 0.17.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dwca/descriptors.py +239 -3
- dwca/files.py +381 -55
- dwca/read.py +65 -17
- dwca/rows.py +105 -78
- dwca/star_record.py +2 -3
- dwca/test/archive_builder.py +188 -0
- dwca/test/test_characterization.py +617 -0
- dwca/test/test_datafile.py +286 -3
- dwca/test/test_descriptors.py +331 -0
- dwca/test/test_dwcareader.py +74 -12
- dwca/test/test_rows.py +173 -0
- dwca/version.py +1 -1
- {python_dwca_reader-0.16.4.dist-info → python_dwca_reader-0.17.1.dist-info}/METADATA +16 -3
- python_dwca_reader-0.17.1.dist-info/RECORD +29 -0
- {python_dwca_reader-0.16.4.dist-info → python_dwca_reader-0.17.1.dist-info}/WHEEL +1 -1
- dwca/minibench.py +0 -61
- python_dwca_reader-0.16.4.dist-info/RECORD +0 -28
- {python_dwca_reader-0.16.4.dist-info → python_dwca_reader-0.17.1.dist-info/licenses}/AUTHORS +0 -0
- {python_dwca_reader-0.16.4.dist-info → python_dwca_reader-0.17.1.dist-info/licenses}/LICENSE.txt +0 -0
- {python_dwca_reader-0.16.4.dist-info → python_dwca_reader-0.17.1.dist-info}/top_level.txt +0 -0
dwca/rows.py
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
"""Objects that represents data rows coming from DarwinCore Archives."""
|
|
2
2
|
|
|
3
3
|
import csv
|
|
4
|
-
import
|
|
5
|
-
from typing import Dict, Optional
|
|
4
|
+
from typing import Dict, List, Optional
|
|
6
5
|
|
|
7
6
|
from dwca.descriptors import DataFileDescriptor
|
|
8
|
-
from dwca.exceptions import InvalidArchive
|
|
9
7
|
|
|
10
8
|
|
|
11
9
|
class Row(object):
|
|
@@ -14,6 +12,8 @@ class Row(object):
|
|
|
14
12
|
This class is intended to be subclassed rather than used directly.
|
|
15
13
|
"""
|
|
16
14
|
|
|
15
|
+
__slots__ = ("descriptor", "position", "rowtype", "raw_fields", "data")
|
|
16
|
+
|
|
17
17
|
# Common ground for __str__ between subclasses
|
|
18
18
|
def _build_str(self, source_str, id_str):
|
|
19
19
|
txt = (
|
|
@@ -52,33 +52,50 @@ class Row(object):
|
|
|
52
52
|
def __init__(
|
|
53
53
|
self, csv_line: str, position: int, datafile_descriptor: DataFileDescriptor
|
|
54
54
|
) -> None:
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
self._populate(
|
|
56
|
+
csv_line_to_fields(
|
|
57
|
+
csv_line,
|
|
58
|
+
line_ending=datafile_descriptor.lines_terminated_by,
|
|
59
|
+
field_ending=datafile_descriptor.fields_terminated_by,
|
|
60
|
+
fields_enclosed_by=datafile_descriptor.fields_enclosed_by,
|
|
61
|
+
),
|
|
62
|
+
position,
|
|
63
|
+
datafile_descriptor,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def from_fields(
|
|
68
|
+
cls,
|
|
69
|
+
raw_fields: List[str],
|
|
70
|
+
position: int,
|
|
71
|
+
datafile_descriptor: DataFileDescriptor,
|
|
72
|
+
) -> "Row":
|
|
73
|
+
"""Build a Row from an already-split data row.
|
|
74
|
+
|
|
75
|
+
This is the constructor used by the streaming engine. :meth:`__init__`, which takes a
|
|
76
|
+
raw CSV line, is kept for backwards compatibility.
|
|
77
|
+
"""
|
|
78
|
+
row = cls.__new__(cls)
|
|
79
|
+
row._populate(raw_fields, position, datafile_descriptor)
|
|
80
|
+
return row
|
|
81
|
+
|
|
82
|
+
def _populate(self, raw_fields, position, datafile_descriptor) -> None:
|
|
83
|
+
#: An instance of :class:`dwca.descriptors.DataFileDescriptor` describing the
|
|
84
|
+
#: originating data file.
|
|
57
85
|
self.descriptor = datafile_descriptor # type: DataFileDescriptor
|
|
58
86
|
|
|
59
|
-
#: The row position/index (starting at 0) in the source data file. This can be used,
|
|
60
|
-
#: :meth:`dwca.read.DwCAReader.get_corerow_by_position` or
|
|
87
|
+
#: The row position/index (starting at 0) in the source data file. This can be used,
|
|
88
|
+
#: for example with :meth:`dwca.read.DwCAReader.get_corerow_by_position` or
|
|
89
|
+
#: :meth:`dwca.files.CSVDataFile.get_row_by_position`.
|
|
61
90
|
self.position = position # type: int
|
|
62
91
|
|
|
63
|
-
#: The csv line type as stated in the archive descriptor
|
|
64
|
-
#:
|
|
65
|
-
#: http://rs.tdwg.org/dwc/terms/Occurrence,
|
|
92
|
+
#: The csv line type as stated in the archive descriptor (or None if the archive has
|
|
93
|
+
#: no descriptor). Examples: http://rs.tdwg.org/dwc/terms/Occurrence,
|
|
66
94
|
#: http://rs.gbif.org/terms/1.0/VernacularName, ...
|
|
67
95
|
self.rowtype = self.descriptor.type # type: Optional[str]
|
|
68
96
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
self.raw_fields = csv_line_to_fields(
|
|
72
|
-
csv_line,
|
|
73
|
-
line_ending=self.descriptor.lines_terminated_by,
|
|
74
|
-
field_ending=self.descriptor.fields_terminated_by,
|
|
75
|
-
fields_enclosed_by=self.descriptor.fields_enclosed_by,
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
# TODO: raw_fields is a new property: to test
|
|
79
|
-
|
|
80
|
-
# TODO: Consistency check ?? self.raw_fields length should be :
|
|
81
|
-
# num of self.raw_fields described in core_meta + 2 (id and \n)
|
|
97
|
+
#: A list of the row's raw (unmapped) field values.
|
|
98
|
+
self.raw_fields = raw_fields
|
|
82
99
|
|
|
83
100
|
#: A dict containing the Row data, such as::
|
|
84
101
|
#:
|
|
@@ -90,29 +107,11 @@ class Row(object):
|
|
|
90
107
|
#:
|
|
91
108
|
#: myrow.data['http://rs.tdwg.org/dwc/terms/locality'] # => "Brussels"
|
|
92
109
|
#:
|
|
93
|
-
#: .. note:: The :func:`dwca.darwincore.utils.qualname` helper is available to make
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
column_index = int(field_descriptor["index"])
|
|
99
|
-
field_row_value = self.raw_fields[column_index]
|
|
100
|
-
except TypeError:
|
|
101
|
-
# int() argument must be a string... We don't have an index for this field
|
|
102
|
-
field_row_value = None
|
|
103
|
-
except IndexError:
|
|
104
|
-
msg = (
|
|
105
|
-
"The descriptor references a non-existent field (index={i})".format(
|
|
106
|
-
i=column_index
|
|
107
|
-
)
|
|
108
|
-
)
|
|
109
|
-
raise InvalidArchive(msg)
|
|
110
|
-
|
|
111
|
-
field_default_value = field_descriptor["default"]
|
|
112
|
-
|
|
113
|
-
self.data[field_descriptor["term"]] = (
|
|
114
|
-
field_row_value or field_default_value or ""
|
|
115
|
-
)
|
|
110
|
+
#: .. note:: The :func:`dwca.darwincore.utils.qualname` helper is available to make
|
|
111
|
+
#: such calls less verbose.
|
|
112
|
+
self.data = datafile_descriptor.field_plan.build_data(
|
|
113
|
+
raw_fields
|
|
114
|
+
) # type: Dict[str, str]
|
|
116
115
|
|
|
117
116
|
|
|
118
117
|
class CoreRow(Row):
|
|
@@ -127,14 +126,14 @@ class CoreRow(Row):
|
|
|
127
126
|
id_str = "Row id: " + str(self.id)
|
|
128
127
|
return super(CoreRow, self)._build_str("Core file", id_str)
|
|
129
128
|
|
|
130
|
-
|
|
131
|
-
self, csv_line: str, position: int, datafile_descriptor: DataFileDescriptor
|
|
132
|
-
) -> None:
|
|
133
|
-
super(CoreRow, self).__init__(csv_line, position, datafile_descriptor)
|
|
129
|
+
__slots__ = ("id", "source_metadata", "extension_data_files", "_extensions")
|
|
134
130
|
|
|
135
|
-
|
|
131
|
+
def _populate(self, raw_fields, position, datafile_descriptor) -> None:
|
|
132
|
+
super(CoreRow, self)._populate(raw_fields, position, datafile_descriptor)
|
|
133
|
+
|
|
134
|
+
if datafile_descriptor.id_index is not None:
|
|
136
135
|
#: The row id
|
|
137
|
-
self.id =
|
|
136
|
+
self.id = raw_fields[datafile_descriptor.id_index]
|
|
138
137
|
else:
|
|
139
138
|
self.id = None
|
|
140
139
|
|
|
@@ -183,25 +182,42 @@ class CoreRow(Row):
|
|
|
183
182
|
# Should these 3 be factorized ? How ? Mixin ? Parent class ?
|
|
184
183
|
def __key(self):
|
|
185
184
|
"""Return a tuple representing the row. Common ground between equality and hash."""
|
|
185
|
+
# A CoreRow obtained without going through DwCAReader iteration (e.g.
|
|
186
|
+
# CSVDataFile.get_row_by_position() called directly) never had link_extension_files()
|
|
187
|
+
# / link_source_metadata() called on it, so self.extensions and self.source_metadata
|
|
188
|
+
# don't exist. Fall back to None for those rows instead of letting the attribute
|
|
189
|
+
# access raise AttributeError. This doesn't change equality for linked rows (both
|
|
190
|
+
# attributes are always present after DwCAReader.next() links them).
|
|
191
|
+
extensions = self.extensions if hasattr(self, "extension_data_files") else None
|
|
192
|
+
source_metadata = (
|
|
193
|
+
self.source_metadata if hasattr(self, "source_metadata") else None
|
|
194
|
+
)
|
|
195
|
+
|
|
186
196
|
return (
|
|
187
197
|
self.descriptor,
|
|
188
198
|
self.id,
|
|
189
199
|
self.data,
|
|
190
|
-
|
|
191
|
-
|
|
200
|
+
extensions,
|
|
201
|
+
source_metadata,
|
|
192
202
|
self.rowtype,
|
|
193
203
|
self.raw_fields,
|
|
194
204
|
self.position,
|
|
195
205
|
)
|
|
196
206
|
|
|
197
207
|
def __eq__(self, other):
|
|
198
|
-
|
|
208
|
+
# The isinstance test is required, not just defensive: __key is name-mangled, so
|
|
209
|
+
# other.__key() means other._CoreRow__key(), which an ExtensionRow (or any non-row)
|
|
210
|
+
# doesn't have. Without it, comparing to anything else raises AttributeError.
|
|
211
|
+
if not isinstance(other, CoreRow):
|
|
212
|
+
return NotImplemented
|
|
199
213
|
|
|
200
|
-
|
|
201
|
-
return not self.__eq__(other)
|
|
214
|
+
return self.__key() == other.__key()
|
|
202
215
|
|
|
203
216
|
def __hash__(self):
|
|
204
|
-
|
|
217
|
+
# __key() embeds the data dict, the raw field list and the lazily-loaded extensions,
|
|
218
|
+
# none of which are hashable. Equal rows still hash equally because this is a subset
|
|
219
|
+
# of the equality key.
|
|
220
|
+
return hash((self.descriptor, self.id, self.rowtype, self.position))
|
|
205
221
|
|
|
206
222
|
|
|
207
223
|
class ExtensionRow(Row):
|
|
@@ -215,13 +231,13 @@ class ExtensionRow(Row):
|
|
|
215
231
|
id_str = "Core row id: " + str(self.core_id)
|
|
216
232
|
return super(ExtensionRow, self)._build_str("Extension file", id_str)
|
|
217
233
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
) -> None:
|
|
221
|
-
super(ExtensionRow, self).
|
|
234
|
+
__slots__ = ("core_id",)
|
|
235
|
+
|
|
236
|
+
def _populate(self, raw_fields, position, datafile_descriptor) -> None:
|
|
237
|
+
super(ExtensionRow, self)._populate(raw_fields, position, datafile_descriptor)
|
|
222
238
|
|
|
223
239
|
#: The id of the core row this extension row is referring to.
|
|
224
|
-
self.core_id =
|
|
240
|
+
self.core_id = raw_fields[datafile_descriptor.coreid_index]
|
|
225
241
|
|
|
226
242
|
def __key(self):
|
|
227
243
|
"""Return a tuple representing the row. Common ground between equality and hash."""
|
|
@@ -235,13 +251,15 @@ class ExtensionRow(Row):
|
|
|
235
251
|
)
|
|
236
252
|
|
|
237
253
|
def __eq__(self, other):
|
|
238
|
-
|
|
254
|
+
# See the note on CoreRow.__eq__: __key is name-mangled, so this test is what keeps
|
|
255
|
+
# comparisons against a CoreRow (or anything else) from raising AttributeError.
|
|
256
|
+
if not isinstance(other, ExtensionRow):
|
|
257
|
+
return NotImplemented
|
|
239
258
|
|
|
240
|
-
|
|
241
|
-
return not self.__eq__(other)
|
|
259
|
+
return self.__key() == other.__key()
|
|
242
260
|
|
|
243
261
|
def __hash__(self):
|
|
244
|
-
return hash(self.
|
|
262
|
+
return hash((self.descriptor, self.core_id, self.rowtype, self.position))
|
|
245
263
|
|
|
246
264
|
|
|
247
265
|
def csv_line_to_fields(csv_line, line_ending, field_ending, fields_enclosed_by):
|
|
@@ -249,16 +267,25 @@ def csv_line_to_fields(csv_line, line_ending, field_ending, fields_enclosed_by):
|
|
|
249
267
|
|
|
250
268
|
Return a list of fields. Content is not trimmed.
|
|
251
269
|
"""
|
|
252
|
-
|
|
253
|
-
|
|
270
|
+
# A carriage return is stripped alongside the declared terminator: archives routinely
|
|
271
|
+
# declare "\n" while the file itself has CRLF line endings. This matches both the csv
|
|
272
|
+
# module's own behavior and CSVDataFile._iter_field_lists, so the two access paths agree.
|
|
273
|
+
csv_line = csv_line.rstrip(line_ending + "\r")
|
|
254
274
|
|
|
255
275
|
if fields_enclosed_by == "":
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
276
|
+
# No enclosure: the line is simply split on the separator. This also keeps any
|
|
277
|
+
# quote character that happens to appear in the content.
|
|
278
|
+
return csv_line.split(field_ending)
|
|
279
|
+
|
|
280
|
+
# The csv module unwraps the enclosure and un-doubles escaped quote characters itself.
|
|
281
|
+
# Stripping the quote character afterwards would eat a legitimate leading or trailing
|
|
282
|
+
# one from the field's own content.
|
|
283
|
+
for row in csv.reader(
|
|
284
|
+
[csv_line],
|
|
285
|
+
delimiter=field_ending,
|
|
286
|
+
quotechar=fields_enclosed_by,
|
|
287
|
+
quoting=csv.QUOTE_MINIMAL,
|
|
288
|
+
):
|
|
289
|
+
return row
|
|
290
|
+
|
|
291
|
+
return []
|
dwca/star_record.py
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
from dwca.files import CSVDataFile
|
|
2
|
-
from typing import List
|
|
3
|
-
from typing_extensions import Literal
|
|
2
|
+
from typing import List, Literal
|
|
4
3
|
import itertools
|
|
5
4
|
|
|
6
5
|
|
|
7
6
|
class StarRecordIterator(object):
|
|
8
7
|
"""Object used to iterate over multiple DWCA-files joined on the coreid
|
|
9
8
|
|
|
10
|
-
:param files_to_join: a list of the
|
|
9
|
+
:param files_to_join: a list of the :class:`dwca.files.CSVDataFile` objects to join.
|
|
11
10
|
May or may not include the core file (the core is not treated in a special way)
|
|
12
11
|
:param how: indicates the type of join. "inner" and "outer" correspond vaguely to
|
|
13
12
|
inner and full joins. The outer join includes rows that don't match on all files,
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Build synthetic Darwin Core Archives for tests.
|
|
2
|
+
|
|
3
|
+
Produces a directory-based archive (DwCAReader reads directories directly, so nothing
|
|
4
|
+
needs zipping). Every parameter that the Metafile can express is explicit, which is what
|
|
5
|
+
lets the characterization tests cover encodings, terminators, quoting and header counts
|
|
6
|
+
without committing more binary sample files.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import shutil
|
|
11
|
+
import tempfile
|
|
12
|
+
from xml.sax.saxutils import quoteattr
|
|
13
|
+
|
|
14
|
+
TERM_PREFIX = "http://rs.tdwg.org/dwc/terms/"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def temp_archive_dir(test_case):
|
|
18
|
+
"""Return a temporary directory that is removed when `test_case` finishes.
|
|
19
|
+
|
|
20
|
+
Several tests in the suite count the entries in the system temporary directory, so
|
|
21
|
+
leaking one per test would eventually make them flaky.
|
|
22
|
+
"""
|
|
23
|
+
directory = tempfile.mkdtemp()
|
|
24
|
+
test_case.addCleanup(shutil.rmtree, directory, ignore_errors=True)
|
|
25
|
+
return directory
|
|
26
|
+
|
|
27
|
+
METAFILE_TEMPLATE = """<archive xmlns="http://rs.tdwg.org/dwc/text/">
|
|
28
|
+
<core encoding={encoding} fieldsTerminatedBy={fields_terminated_by} \
|
|
29
|
+
linesTerminatedBy={lines_terminated_by} fieldsEnclosedBy={fields_enclosed_by} \
|
|
30
|
+
ignoreHeaderLines={ignore_header_lines} rowType="http://rs.tdwg.org/dwc/terms/Occurrence">
|
|
31
|
+
<files><location>occurrence.txt</location></files>
|
|
32
|
+
<id index="{id_index}" />
|
|
33
|
+
{fields}
|
|
34
|
+
</core>
|
|
35
|
+
{extension}
|
|
36
|
+
</archive>
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
EXTENSION_TEMPLATE = """ <extension encoding={encoding} fieldsTerminatedBy={fields_terminated_by} \
|
|
40
|
+
linesTerminatedBy={lines_terminated_by} fieldsEnclosedBy={fields_enclosed_by} \
|
|
41
|
+
ignoreHeaderLines={ignore_header_lines} rowType="http://rs.gbif.org/terms/1.0/VernacularName">
|
|
42
|
+
<files><location>extension.txt</location></files>
|
|
43
|
+
<coreid index="0" />
|
|
44
|
+
{fields}
|
|
45
|
+
</extension>"""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _escape(raw):
|
|
49
|
+
# The Metafile stores separators escaped, e.g. the tab character as the two
|
|
50
|
+
# characters backslash-t. Mirror what real archives contain.
|
|
51
|
+
escaped = (
|
|
52
|
+
raw.replace("\\", "\\\\")
|
|
53
|
+
.replace("\t", "\\t")
|
|
54
|
+
.replace("\n", "\\n")
|
|
55
|
+
.replace("\r", "\\r")
|
|
56
|
+
)
|
|
57
|
+
return quoteattr(escaped)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _field_tags(terms, defaults, indent=" "):
|
|
61
|
+
lines = []
|
|
62
|
+
for index, term in enumerate(terms):
|
|
63
|
+
default = (defaults or {}).get(term)
|
|
64
|
+
if default is None:
|
|
65
|
+
lines.append(
|
|
66
|
+
'{i}<field index="{n}" term="{t}"/>'.format(i=indent, n=index, t=term)
|
|
67
|
+
)
|
|
68
|
+
else:
|
|
69
|
+
lines.append(
|
|
70
|
+
'{i}<field index="{n}" term="{t}" default={d}/>'.format(
|
|
71
|
+
i=indent, n=index, t=term, d=quoteattr(default)
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
for term, default in (defaults or {}).items():
|
|
75
|
+
if term not in terms:
|
|
76
|
+
lines.append(
|
|
77
|
+
'{i}<field term="{t}" default={d}/>'.format(
|
|
78
|
+
i=indent, t=term, d=quoteattr(default)
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
return "\n".join(lines)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _render_rows(rows, fields_terminated_by, lines_terminated_by, fields_enclosed_by):
|
|
85
|
+
out = []
|
|
86
|
+
for row in rows:
|
|
87
|
+
if fields_enclosed_by:
|
|
88
|
+
cells = [
|
|
89
|
+
fields_enclosed_by
|
|
90
|
+
+ cell.replace(fields_enclosed_by, fields_enclosed_by * 2)
|
|
91
|
+
+ fields_enclosed_by
|
|
92
|
+
for cell in row
|
|
93
|
+
]
|
|
94
|
+
else:
|
|
95
|
+
cells = list(row)
|
|
96
|
+
out.append(fields_terminated_by.join(cells))
|
|
97
|
+
return lines_terminated_by.join(out) + (lines_terminated_by if out else "")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def build_archive(
|
|
101
|
+
directory,
|
|
102
|
+
rows,
|
|
103
|
+
columns=None,
|
|
104
|
+
encoding="utf-8",
|
|
105
|
+
lines_terminated_by="\n",
|
|
106
|
+
fields_terminated_by="\t",
|
|
107
|
+
fields_enclosed_by="",
|
|
108
|
+
ignore_header_lines=0,
|
|
109
|
+
header_rows=(),
|
|
110
|
+
id_index=0,
|
|
111
|
+
terms=None,
|
|
112
|
+
defaults=None,
|
|
113
|
+
extension=None,
|
|
114
|
+
trailing_newline=True,
|
|
115
|
+
raw_payload=None,
|
|
116
|
+
):
|
|
117
|
+
"""Write a Darwin Core Archive into `directory` and return its path.
|
|
118
|
+
|
|
119
|
+
:param rows: list of lists of str, the data rows.
|
|
120
|
+
:param terms: list of full term URIs, one per column. Defaults to term0, term1, ...
|
|
121
|
+
:param defaults: dict term -> default value. A term not present in `terms` becomes a
|
|
122
|
+
default-only field (no index attribute), as the standard allows.
|
|
123
|
+
:param extension: list of rows for an extension data file, or None. Column 0 is the coreid.
|
|
124
|
+
:param raw_payload: bytes written verbatim as the core data file instead of `rows`.
|
|
125
|
+
Used to build inputs no well-formed writer would produce (undecodable bytes, ragged
|
|
126
|
+
rows, blank lines).
|
|
127
|
+
"""
|
|
128
|
+
if columns is None:
|
|
129
|
+
columns = max([len(r) for r in rows] + [1]) if rows else 1
|
|
130
|
+
if terms is None:
|
|
131
|
+
terms = [TERM_PREFIX + "term" + str(i) for i in range(columns)]
|
|
132
|
+
|
|
133
|
+
metafile = METAFILE_TEMPLATE.format(
|
|
134
|
+
encoding=quoteattr(encoding),
|
|
135
|
+
fields_terminated_by=_escape(fields_terminated_by),
|
|
136
|
+
lines_terminated_by=_escape(lines_terminated_by),
|
|
137
|
+
fields_enclosed_by=_escape(fields_enclosed_by),
|
|
138
|
+
ignore_header_lines=quoteattr(str(ignore_header_lines)),
|
|
139
|
+
id_index=id_index,
|
|
140
|
+
fields=_field_tags(terms, defaults),
|
|
141
|
+
extension=(
|
|
142
|
+
""
|
|
143
|
+
if extension is None
|
|
144
|
+
else EXTENSION_TEMPLATE.format(
|
|
145
|
+
encoding=quoteattr(encoding),
|
|
146
|
+
fields_terminated_by=_escape(fields_terminated_by),
|
|
147
|
+
lines_terminated_by=_escape(lines_terminated_by),
|
|
148
|
+
fields_enclosed_by=_escape(fields_enclosed_by),
|
|
149
|
+
ignore_header_lines=quoteattr(str(ignore_header_lines)),
|
|
150
|
+
fields=_field_tags(
|
|
151
|
+
[TERM_PREFIX + "vernacularName"], None, indent=" "
|
|
152
|
+
).replace('index="0"', 'index="1"'),
|
|
153
|
+
)
|
|
154
|
+
),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
with open(os.path.join(directory, "meta.xml"), "w", encoding="utf-8") as f:
|
|
158
|
+
f.write(metafile)
|
|
159
|
+
|
|
160
|
+
data_path = os.path.join(directory, "occurrence.txt")
|
|
161
|
+
if raw_payload is not None:
|
|
162
|
+
with open(data_path, "wb") as f:
|
|
163
|
+
f.write(raw_payload)
|
|
164
|
+
else:
|
|
165
|
+
payload = _render_rows(
|
|
166
|
+
list(header_rows) + list(rows),
|
|
167
|
+
fields_terminated_by,
|
|
168
|
+
lines_terminated_by,
|
|
169
|
+
fields_enclosed_by,
|
|
170
|
+
)
|
|
171
|
+
if not trailing_newline and payload.endswith(lines_terminated_by):
|
|
172
|
+
payload = payload[: -len(lines_terminated_by)]
|
|
173
|
+
with open(data_path, "w", encoding=encoding, newline="") as f:
|
|
174
|
+
f.write(payload)
|
|
175
|
+
|
|
176
|
+
if extension is not None:
|
|
177
|
+
payload = _render_rows(
|
|
178
|
+
list(header_rows) + list(extension),
|
|
179
|
+
fields_terminated_by,
|
|
180
|
+
lines_terminated_by,
|
|
181
|
+
fields_enclosed_by,
|
|
182
|
+
)
|
|
183
|
+
with open(
|
|
184
|
+
os.path.join(directory, "extension.txt"), "w", encoding=encoding, newline=""
|
|
185
|
+
) as f:
|
|
186
|
+
f.write(payload)
|
|
187
|
+
|
|
188
|
+
return directory
|