python-dwca-reader 0.16.2__py3-none-any.whl → 0.16.4__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/read.py CHANGED
@@ -10,11 +10,14 @@ from tempfile import mkdtemp
10
10
  from typing import List, Optional, Dict, Any, IO, Tuple
11
11
  from xml.etree.ElementTree import Element
12
12
 
13
- from pandas.io.parsers import TextFileReader
14
-
15
13
  import dwca.vendor
16
14
  from dwca.descriptors import ArchiveDescriptor, DataFileDescriptor, shorten_term
17
- from dwca.exceptions import RowNotFound, InvalidArchive, InvalidSimpleArchive, NotADataFile
15
+ from dwca.exceptions import (
16
+ RowNotFound,
17
+ InvalidArchive,
18
+ InvalidSimpleArchive,
19
+ NotADataFile,
20
+ )
18
21
  from dwca.files import CSVDataFile
19
22
  from dwca.helpers import remove_tree
20
23
  from dwca.rows import CoreRow
@@ -72,7 +75,7 @@ class DwCAReader(object):
72
75
 
73
76
  default_metadata_filename = "EML.xml"
74
77
  default_metafile_name = "meta.xml"
75
- source_metadata_directory = 'dataset'
78
+ source_metadata_directory = "dataset"
76
79
 
77
80
  def __enter__(self):
78
81
  return self
@@ -80,7 +83,13 @@ class DwCAReader(object):
80
83
  def __exit__(self, exc_type, exc_value, exc_traceback):
81
84
  self.close()
82
85
 
83
- def __init__(self, path: str, extensions_to_ignore: List[str] = None, tmp_dir: str = None) -> None:
86
+ def __init__(
87
+ self,
88
+ path: str,
89
+ extensions_to_ignore: List[str] = None,
90
+ tmp_dir: str = None,
91
+ skip_metadata: bool = False,
92
+ ) -> None:
84
93
  """Open the Darwin Core Archive."""
85
94
  if extensions_to_ignore is None:
86
95
  extensions_to_ignore = []
@@ -92,7 +101,9 @@ class DwCAReader(object):
92
101
  #: The path to the Darwin Core Archive file, as passed to the constructor.
93
102
  self.archive_path = path # type: str
94
103
 
95
- if os.path.isdir(self.archive_path): # Archive is a (directly readable) directory
104
+ if os.path.isdir(
105
+ self.archive_path
106
+ ): # Archive is a (directly readable) directory
96
107
  self._working_directory_path = self.archive_path
97
108
  self._directory_to_clean = None # type: Optional[str]
98
109
  else: # Archive is zipped/tgzipped, we have to extract it first.
@@ -104,15 +115,18 @@ class DwCAReader(object):
104
115
  self._metafile_handle = None
105
116
  try:
106
117
  self._metafile_handle = self.open_included_file(self.default_metafile_name)
107
- self.descriptor = ArchiveDescriptor(self._metafile_handle.read(),
108
- files_to_ignore=extensions_to_ignore)
118
+ self.descriptor = ArchiveDescriptor(
119
+ self._metafile_handle.read(), files_to_ignore=extensions_to_ignore
120
+ )
109
121
  except IOError as exc:
110
122
  if exc.errno == ENOENT:
111
123
  pass
112
124
 
113
125
  #: A :class:`xml.etree.ElementTree.Element` instance containing the (scientific) metadata
114
- #: of the archive, or `None` if the archive has no metadata.
115
- self.metadata = self._parse_metadata_file() # type: Optional[Element]
126
+ #: of the archive, or `None` if the archive has no metadata or if the `skip_metadata` parameter is True.
127
+ self.metadata = None # type: Optional[Element]
128
+ if not skip_metadata:
129
+ self.metadata = self._parse_metadata_file() # type: Optional[Element]
116
130
 
117
131
  #: If the archive contains source-level metadata (typically, GBIF downloads), this is a dict such as::
118
132
  #:
@@ -122,23 +136,33 @@ class DwCAReader(object):
122
136
  #: See :doc:`gbif_results` for more details.
123
137
  self.source_metadata = self._get_source_metadata() # type: Dict[str, Element]
124
138
 
125
- if self.descriptor: # We have an Archive descriptor that we can use to access data files.
139
+ if (
140
+ self.descriptor
141
+ ): # We have an Archive descriptor that we can use to access data files.
126
142
  #: An instance of :class:`dwca.files.CSVDataFile` for the core data file.
127
- self.core_file = CSVDataFile(self._working_directory_path, self.descriptor.core) # type: CSVDataFile
143
+ self.core_file = CSVDataFile(
144
+ self._working_directory_path, self.descriptor.core
145
+ ) # type: CSVDataFile
128
146
 
129
147
  #: A list of :class:`dwca.files.CSVDataFile`, one entry for each extension data file , sorted by order of
130
148
  #: appearance in the Metafile (or an empty list if the archive doesn't use extensions).
131
- self.extension_files = [CSVDataFile(work_directory=self._working_directory_path,
132
- file_descriptor=d)
133
- for d in self.descriptor.extensions] # type: List[CSVDataFile]
149
+ self.extension_files = [
150
+ CSVDataFile(
151
+ work_directory=self._working_directory_path, file_descriptor=d
152
+ )
153
+ for d in self.descriptor.extensions
154
+ ] # type: List[CSVDataFile]
134
155
  else: # Archive without descriptor, we'll have to find and inspect the data file
135
156
  try:
136
157
  datafile_name = self._is_valid_simple_archive()
137
158
  descriptor = DataFileDescriptor.make_from_file(
138
- os.path.join(self._working_directory_path, datafile_name))
159
+ os.path.join(self._working_directory_path, datafile_name)
160
+ )
139
161
 
140
- self.core_file = CSVDataFile(work_directory=self._working_directory_path,
141
- file_descriptor=descriptor)
162
+ self.core_file = CSVDataFile(
163
+ work_directory=self._working_directory_path,
164
+ file_descriptor=descriptor,
165
+ )
142
166
  self.extension_files = []
143
167
  except InvalidSimpleArchive:
144
168
  msg = "No Metafile was found, but the archive contains multiple files/directories."
@@ -146,14 +170,17 @@ class DwCAReader(object):
146
170
 
147
171
  def _get_source_metadata(self) -> Dict[str, Element]:
148
172
  source_metadata = {} # type: Dict[str, Element]
149
- source_metadata_dir = os.path.join(self._working_directory_path, self.source_metadata_directory)
173
+ source_metadata_dir = os.path.join(
174
+ self._working_directory_path, self.source_metadata_directory
175
+ )
150
176
 
151
177
  if os.path.isdir(source_metadata_dir):
152
178
  for f in os.listdir(source_metadata_dir):
153
179
  if os.path.isfile(os.path.join(source_metadata_dir, f)):
154
180
  dataset_key = os.path.splitext(f)[0]
155
181
  source_metadata[dataset_key] = self._parse_xml_included_file(
156
- os.path.join(self.source_metadata_directory, f))
182
+ os.path.join(self.source_metadata_directory, f)
183
+ )
157
184
 
158
185
  return source_metadata
159
186
 
@@ -192,23 +219,28 @@ class DwCAReader(object):
192
219
  not supported in case the value returned by `pandas.read_csv()` is a `TextFileReader` (e.g. when using
193
220
  `chunksize` or `iterator=True`).
194
221
  """
195
- datafile_descriptor = self.get_descriptor_for(relative_path) # type: DataFileDescriptor
222
+ datafile_descriptor = self.get_descriptor_for(
223
+ relative_path
224
+ ) # type: DataFileDescriptor
196
225
 
197
226
  if not dwca.vendor._has_pandas:
198
227
  raise ImportError("Pandas is missing.")
199
228
 
200
229
  from pandas import read_csv
230
+ from pandas.io.parsers import TextFileReader
201
231
 
202
- kwargs['delimiter'] = datafile_descriptor.fields_terminated_by
203
- kwargs['skiprows'] = datafile_descriptor.lines_to_ignore
204
- kwargs['header'] = None
205
- kwargs['names'] = datafile_descriptor.short_headers
232
+ kwargs["delimiter"] = datafile_descriptor.fields_terminated_by
233
+ kwargs["skiprows"] = datafile_descriptor.lines_to_ignore
234
+ kwargs["header"] = None
235
+ kwargs["names"] = datafile_descriptor.short_headers
206
236
 
207
- df_or_textreader = read_csv(self.absolute_temporary_path(relative_path), **kwargs)
237
+ df_or_textreader = read_csv(
238
+ self.absolute_temporary_path(relative_path), **kwargs
239
+ )
208
240
 
209
241
  # Add a column for default values, if present in the file descriptor
210
242
  for field in datafile_descriptor.fields:
211
- field_default_value = field['default']
243
+ field_default_value = field["default"]
212
244
  if field_default_value is not None:
213
245
  if isinstance(df_or_textreader, TextFileReader):
214
246
  # I don't see how to assign default values to a TextFileReader, so
@@ -220,7 +252,7 @@ class DwCAReader(object):
220
252
  "the archive."
221
253
  )
222
254
 
223
- df_or_textreader[shorten_term(field['term'])] = field_default_value
255
+ df_or_textreader[shorten_term(field["term"])] = field_default_value
224
256
 
225
257
  return df_or_textreader
226
258
 
@@ -249,7 +281,9 @@ class DwCAReader(object):
249
281
  ids = temp_ids.keys()
250
282
 
251
283
  for extension in self.extension_files:
252
- coreid_index = dict((k, v.tolist()) for k, v in extension.coreid_index.items())
284
+ coreid_index = dict(
285
+ (k, v.tolist()) for k, v in extension.coreid_index.items()
286
+ )
253
287
  for id in ids:
254
288
  coreid_index.pop(id, None)
255
289
  indexes[extension.file_descriptor.file_location] = coreid_index
@@ -311,7 +345,7 @@ class DwCAReader(object):
311
345
  - The position is often an appropriate way to unambiguously identify a core row in a DwCA.
312
346
 
313
347
  """
314
- for (i, row) in enumerate(self):
348
+ for i, row in enumerate(self):
315
349
  if i == position:
316
350
  return row
317
351
 
@@ -340,7 +374,9 @@ class DwCAReader(object):
340
374
  File existence is not tested.
341
375
 
342
376
  """
343
- return os.path.abspath(os.path.join(self._working_directory_path, relative_path))
377
+ return os.path.abspath(
378
+ os.path.join(self._working_directory_path, relative_path)
379
+ )
344
380
 
345
381
  def get_descriptor_for(self, relative_path: str) -> DataFileDescriptor:
346
382
  """Return a descriptor for the data file located at relative_path.
@@ -365,7 +401,6 @@ class DwCAReader(object):
365
401
  raise NotADataFile("{fn} is not a data file".format(fn=relative_path))
366
402
 
367
403
  def _is_valid_simple_archive(self) -> str:
368
-
369
404
  # If the working dir appear to contains a valid simple darwin core archive
370
405
  # (one single data file + possibly some metadata), returns the name of the data file.
371
406
  #
@@ -396,7 +431,7 @@ class DwCAReader(object):
396
431
  """Load the archive (scientific) Metadata file, parse it with\
397
432
  ElementTree and return its content (or `None` if the archive has no metadata).
398
433
 
399
- :raises: :class:`dwca.exceptions.InvalidArchive` if the archive references an non-existent
434
+ :raises: :class:`dwca.exceptions.InvalidArchive` if the archive references a non-existent
400
435
  metadata file.
401
436
  """
402
437
  # If the archive has descriptor, look for the metadata filename there.
@@ -407,14 +442,18 @@ class DwCAReader(object):
407
442
  return self._parse_xml_included_file(filename)
408
443
  except IOError as exc:
409
444
  if exc.errno == ENOENT: # File not found
410
- msg = "{} is referenced in the archive descriptor but missing.".format(filename)
445
+ msg = "{} is referenced in the archive descriptor but missing.".format(
446
+ filename
447
+ )
411
448
  raise InvalidArchive(msg)
412
449
 
413
450
  else: # Otherwise, the metadata file has to be named 'EML.xml'
414
451
  try:
415
452
  return self._parse_xml_included_file(self.default_metadata_filename)
416
453
  except IOError as exc:
417
- if exc.errno == ENOENT: # File not found, this is an archive without metadata
454
+ if (
455
+ exc.errno == ENOENT
456
+ ): # File not found, this is an archive without metadata
418
457
  return None
419
458
 
420
459
  assert False # For MyPy, see: https://github.com/python/mypy/issues/4223#issuecomment-342865133
@@ -428,7 +467,7 @@ class DwCAReader(object):
428
467
  # In that case, we work with a stripped string instead.
429
468
  # Note that this method cannot be generalized because it won't work well with encoding specified in the xml
430
469
  # tag. This is why we're only choosing it in case of error
431
- data_as_string = self.open_included_file(relative_path, 'r').read()
470
+ data_as_string = self.open_included_file(relative_path, "r").read()
432
471
  return ET.fromstring(data_as_string.strip())
433
472
 
434
473
  def _unzip_or_untar(self) -> str:
@@ -444,14 +483,16 @@ class DwCAReader(object):
444
483
  try:
445
484
  # Security note: with Python < 2.7.4, a zip file may be able to write outside of the
446
485
  # directory using absolute paths, parent (..) path, ... See note in ZipFile.extract doc
447
- zipfile.ZipFile(self.archive_path, 'r').extractall(tmp_dir)
486
+ zipfile.ZipFile(self.archive_path, "r").extractall(tmp_dir)
448
487
  except zipfile.BadZipfile:
449
488
  # Doesn't look like a valid zip, let's see if it's a tar archive (possibly compressed)
450
489
  try:
451
490
  # TODO: Once we only support Python 3.12+, we should pass the filter="data" argument to extractall()
452
- tarfile.open(self.archive_path, 'r:*').extractall(tmp_dir)
491
+ tarfile.open(self.archive_path, "r:*").extractall(tmp_dir)
453
492
  except tarfile.ReadError:
454
- raise InvalidArchive("The archive cannot be read. Is it a .zip or .tgz file?")
493
+ raise InvalidArchive(
494
+ "The archive cannot be read. Is it a .zip or .tgz file?"
495
+ )
455
496
 
456
497
  return tmp_dir
457
498
 
@@ -487,7 +528,7 @@ class DwCAReader(object):
487
528
  extension_file.close()
488
529
  if self._metafile_handle:
489
530
  self._metafile_handle.close()
490
-
531
+
491
532
  if self._directory_to_clean:
492
533
  remove_tree(self._directory_to_clean)
493
534
 
@@ -495,7 +536,7 @@ class DwCAReader(object):
495
536
  """Return `True` if the Core file of the archive contains the `term_url` term."""
496
537
  return term_url in self.core_file.file_descriptor.terms
497
538
 
498
- def __iter__(self) -> 'DwCAReader':
539
+ def __iter__(self) -> "DwCAReader":
499
540
  self._corefile_pointer = 0
500
541
  return self
501
542
 
dwca/rows.py CHANGED
@@ -16,36 +16,45 @@ class Row(object):
16
16
 
17
17
  # Common ground for __str__ between subclasses
18
18
  def _build_str(self, source_str, id_str):
19
- txt = ("--\n"
20
- "Rowtype: {rowtype}\n"
21
- "Position: {position}\n"
22
- "Source: {source_str}\n"
23
- "{id_row}\n"
24
- "Reference extension rows: {extension_flag}\n"
25
- "Reference source metadata: {source_metadata_flag}\n"
26
- "Data: {data}\n"
27
- "--\n")
28
-
29
- extension_flag = "Yes" if (hasattr(self, 'extensions') and (len(self.extensions) > 0)) else "No"
30
-
31
- if hasattr(self, 'source_metadata') and (self.source_metadata is not None):
32
- source_metadata_flag = 'Yes'
19
+ txt = (
20
+ "--\n"
21
+ "Rowtype: {rowtype}\n"
22
+ "Position: {position}\n"
23
+ "Source: {source_str}\n"
24
+ "{id_row}\n"
25
+ "Reference extension rows: {extension_flag}\n"
26
+ "Reference source metadata: {source_metadata_flag}\n"
27
+ "Data: {data}\n"
28
+ "--\n"
29
+ )
30
+
31
+ extension_flag = (
32
+ "Yes"
33
+ if (hasattr(self, "extensions") and (len(self.extensions) > 0))
34
+ else "No"
35
+ )
36
+
37
+ if hasattr(self, "source_metadata") and (self.source_metadata is not None):
38
+ source_metadata_flag = "Yes"
33
39
  else:
34
- source_metadata_flag = 'No'
35
-
36
- return txt.format(rowtype=self.rowtype,
37
- position=self.position,
38
- source_str=source_str,
39
- data=self.data,
40
- id_row=id_str,
41
- extension_flag=extension_flag,
42
- source_metadata_flag=source_metadata_flag)
43
-
44
- def __init__(self, csv_line: str, position: int, datafile_descriptor: DataFileDescriptor) -> None:
45
-
40
+ source_metadata_flag = "No"
41
+
42
+ return txt.format(
43
+ rowtype=self.rowtype,
44
+ position=self.position,
45
+ source_str=source_str,
46
+ data=self.data,
47
+ id_row=id_str,
48
+ extension_flag=extension_flag,
49
+ source_metadata_flag=source_metadata_flag,
50
+ )
51
+
52
+ def __init__(
53
+ self, csv_line: str, position: int, datafile_descriptor: DataFileDescriptor
54
+ ) -> None:
46
55
  #: An instance of :class:`dwca.descriptors.DataFileDescriptor` describing the originating
47
56
  #: data file.
48
- self.descriptor = datafile_descriptor # type: DataFileDescriptor
57
+ self.descriptor = datafile_descriptor # type: DataFileDescriptor
49
58
 
50
59
  #: The row position/index (starting at 0) in the source data file. This can be used, for example with
51
60
  #: :meth:`dwca.read.DwCAReader.get_corerow_by_position` or :meth:`dwca.files.CSVDataFile.get_row_by_position`.
@@ -59,10 +68,12 @@ class Row(object):
59
68
 
60
69
  # self.raw_fields is a list of the csv_line's content
61
70
  #:
62
- self.raw_fields = csv_line_to_fields(csv_line,
63
- line_ending=self.descriptor.lines_terminated_by,
64
- field_ending=self.descriptor.fields_terminated_by,
65
- fields_enclosed_by=self.descriptor.fields_enclosed_by)
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
+ )
66
77
 
67
78
  # TODO: raw_fields is a new property: to test
68
79
 
@@ -84,18 +95,24 @@ class Row(object):
84
95
 
85
96
  for field_descriptor in self.descriptor.fields:
86
97
  try:
87
- column_index = int(field_descriptor['index'])
98
+ column_index = int(field_descriptor["index"])
88
99
  field_row_value = self.raw_fields[column_index]
89
100
  except TypeError:
90
101
  # int() argument must be a string... We don't have an index for this field
91
102
  field_row_value = None
92
103
  except IndexError:
93
- msg = 'The descriptor references a non-existent field (index={i})'.format(i=column_index)
104
+ msg = (
105
+ "The descriptor references a non-existent field (index={i})".format(
106
+ i=column_index
107
+ )
108
+ )
94
109
  raise InvalidArchive(msg)
95
110
 
96
- field_default_value = field_descriptor['default']
111
+ field_default_value = field_descriptor["default"]
97
112
 
98
- self.data[field_descriptor['term']] = field_row_value or field_default_value or ''
113
+ self.data[field_descriptor["term"]] = (
114
+ field_row_value or field_default_value or ""
115
+ )
99
116
 
100
117
 
101
118
  class CoreRow(Row):
@@ -110,7 +127,9 @@ class CoreRow(Row):
110
127
  id_str = "Row id: " + str(self.id)
111
128
  return super(CoreRow, self)._build_str("Core file", id_str)
112
129
 
113
- def __init__(self, csv_line: str, position: int, datafile_descriptor: DataFileDescriptor) -> None:
130
+ def __init__(
131
+ self, csv_line: str, position: int, datafile_descriptor: DataFileDescriptor
132
+ ) -> None:
114
133
  super(CoreRow, self).__init__(csv_line, position, datafile_descriptor)
115
134
 
116
135
  if self.descriptor.id_index is not None:
@@ -129,14 +148,14 @@ class CoreRow(Row):
129
148
  # provide some, but not for this row, it will be set to None).
130
149
  #
131
150
  # If this method is never called, the source_metadata attribute will not exist
132
- field_name = 'http://rs.tdwg.org/dwc/terms/datasetID'
151
+ field_name = "http://rs.tdwg.org/dwc/terms/datasetID"
133
152
 
134
153
  #: Row-level metadata (if provided by the archive).
135
154
  #: This is a non-standard DwCA feature currently that we can sometimes encounter (in downloads from GBIF.org
136
155
  #: for example).
137
156
  self.source_metadata = None
138
157
 
139
- if (archive_source_metadata and (field_name in self.data)):
158
+ if archive_source_metadata and (field_name in self.data):
140
159
  try:
141
160
  self.source_metadata = archive_source_metadata[self.data[field_name]]
142
161
  except KeyError:
@@ -150,10 +169,13 @@ class CoreRow(Row):
150
169
  # type () -> List[ExtensionRow]
151
170
  """A list of :class:`.ExtensionRow` instances that relates to this Core row."""
152
171
  # We use lazy loading
153
- if not hasattr(self, '_extensions'):
172
+ if not hasattr(self, "_extensions"):
154
173
  self._extensions = []
155
174
  for csv_file in self.extension_data_files:
156
- [self._extensions.append(r) for r in csv_file.get_all_rows_by_coreid(self.id)]
175
+ [
176
+ self._extensions.append(r)
177
+ for r in csv_file.get_all_rows_by_coreid(self.id)
178
+ ]
157
179
 
158
180
  return self._extensions
159
181
 
@@ -161,8 +183,16 @@ class CoreRow(Row):
161
183
  # Should these 3 be factorized ? How ? Mixin ? Parent class ?
162
184
  def __key(self):
163
185
  """Return a tuple representing the row. Common ground between equality and hash."""
164
- return (self.descriptor, self.id, self.data, self.extensions, self.source_metadata,
165
- self.rowtype, self.raw_fields, self.position)
186
+ return (
187
+ self.descriptor,
188
+ self.id,
189
+ self.data,
190
+ self.extensions,
191
+ self.source_metadata,
192
+ self.rowtype,
193
+ self.raw_fields,
194
+ self.position,
195
+ )
166
196
 
167
197
  def __eq__(self, other):
168
198
  return self.__key() == other.__key()
@@ -185,7 +215,9 @@ class ExtensionRow(Row):
185
215
  id_str = "Core row id: " + str(self.core_id)
186
216
  return super(ExtensionRow, self)._build_str("Extension file", id_str)
187
217
 
188
- def __init__(self, csv_line: str, position: int, datafile_descriptor: DataFileDescriptor) -> None:
218
+ def __init__(
219
+ self, csv_line: str, position: int, datafile_descriptor: DataFileDescriptor
220
+ ) -> None:
189
221
  super(ExtensionRow, self).__init__(csv_line, position, datafile_descriptor)
190
222
 
191
223
  #: The id of the core row this extension row is referring to.
@@ -193,7 +225,14 @@ class ExtensionRow(Row):
193
225
 
194
226
  def __key(self):
195
227
  """Return a tuple representing the row. Common ground between equality and hash."""
196
- return (self.descriptor, self.core_id, self.data, self.rowtype, self.raw_fields, self.position)
228
+ return (
229
+ self.descriptor,
230
+ self.core_id,
231
+ self.data,
232
+ self.rowtype,
233
+ self.raw_fields,
234
+ self.position,
235
+ )
197
236
 
198
237
  def __eq__(self, other):
199
238
  return self.__key() == other.__key()
@@ -214,10 +253,9 @@ def csv_line_to_fields(csv_line, line_ending, field_ending, fields_enclosed_by):
214
253
  raw_fields = []
215
254
 
216
255
  if fields_enclosed_by == "":
217
- opts = {'quoting': csv.QUOTE_NONE}
256
+ opts = {"quoting": csv.QUOTE_NONE}
218
257
  else:
219
- opts = {'quoting': csv.QUOTE_ALL,
220
- 'quotechar': fields_enclosed_by}
258
+ opts = {"quoting": csv.QUOTE_ALL, "quotechar": fields_enclosed_by}
221
259
 
222
260
  for row in csv.reader([csv_line], delimiter=field_ending, **opts):
223
261
  for f in row:
dwca/star_record.py CHANGED
@@ -5,16 +5,19 @@ import itertools
5
5
 
6
6
 
7
7
  class StarRecordIterator(object):
8
- """ Object used to iterate over multiple DWCA-files joined on the coreid
9
-
10
- :param files_to_join: a list of the `dwca.files.CSVDataFile`s we'd like to join.
11
- May or may not include the core file (the core is not treated in a special way)
12
- :param how: indicates the type of join. "inner" and "outer" correspond vaguely to
13
- inner and full joins. The outer join includes rows that don't match on all files,
14
- however, it doesn't create null fields to fill in when rows are missing in files.
15
- Attempts to conform to pandas.DataFrame.merge API.
8
+ """Object used to iterate over multiple DWCA-files joined on the coreid
9
+
10
+ :param files_to_join: a list of the `dwca.files.CSVDataFile`s we'd like to join.
11
+ May or may not include the core file (the core is not treated in a special way)
12
+ :param how: indicates the type of join. "inner" and "outer" correspond vaguely to
13
+ inner and full joins. The outer join includes rows that don't match on all files,
14
+ however, it doesn't create null fields to fill in when rows are missing in files.
15
+ Attempts to conform to pandas.DataFrame.merge API.
16
16
  """
17
- def __init__(self, files_to_join: List[CSVDataFile], how: Literal["inner", "outer"] = "inner"):
17
+
18
+ def __init__(
19
+ self, files_to_join: List[CSVDataFile], how: Literal["inner", "outer"] = "inner"
20
+ ):
18
21
  self.files_to_join = files_to_join
19
22
 
20
23
  # gather the coreids we want to join over.
@@ -26,12 +29,11 @@ class StarRecordIterator(object):
26
29
  # outer join: coreid may be in any files
27
30
  elif how == "outer":
28
31
  self.common_core |= set(data_file.coreid_index)
29
-
32
+
30
33
  # initialize iterator variables
31
34
  self._common_core_iterator = iter(self.common_core)
32
35
  self._cross_product_iterator = iter([])
33
36
 
34
-
35
37
  def __next__(self):
36
38
  # the next combination of rows matching this coreid
37
39
  next_positions = next(self._cross_product_iterator, None)
@@ -40,20 +42,26 @@ class StarRecordIterator(object):
40
42
  # get the next coreid
41
43
  self._current_coreid = next(self._common_core_iterator)
42
44
  self._files_with_current_coreid = [
43
- csv_file for csv_file in self.files_to_join
44
- if self._current_coreid in csv_file.coreid_index]
45
+ csv_file
46
+ for csv_file in self.files_to_join
47
+ if self._current_coreid in csv_file.coreid_index
48
+ ]
45
49
  # this iterates over all combinations of rows matching a coreid from all files
46
50
  self._cross_product_iterator = itertools.product(
47
51
  *(
48
- csv_file.coreid_index[self._current_coreid]
49
- for csv_file in self._files_with_current_coreid
50
- ))
52
+ csv_file.coreid_index[self._current_coreid]
53
+ for csv_file in self._files_with_current_coreid
54
+ )
55
+ )
51
56
  # go back and try to iterate over the rows for the new coreid
52
57
  return next(self)
53
58
  # zip up this combination of rows from all of the files.
54
59
  return (
55
- csv_file.get_row_by_position(position) for position, csv_file in zip(next_positions, self._files_with_current_coreid)
60
+ csv_file.get_row_by_position(position)
61
+ for position, csv_file in zip(
62
+ next_positions, self._files_with_current_coreid
63
+ )
56
64
  )
57
65
 
58
-
59
- def __iter__(self): return self
66
+ def __iter__(self):
67
+ return self
dwca/test/helpers.py CHANGED
@@ -4,4 +4,4 @@ import os
4
4
 
5
5
 
6
6
  def sample_data_path(filename):
7
- return os.path.join(os.path.dirname(__file__), 'sample_files', filename)
7
+ return os.path.join(os.path.dirname(__file__), "sample_files", filename)
@@ -35,13 +35,18 @@ class TestCSVDataFile(unittest.TestCase):
35
35
  description_txt = extension_files[0]
36
36
  vernacular_txt = extension_files[1]
37
37
 
38
- expected_core = {'1': array('L', [0]), '2': array('L', [1]), '3': array('L', [2]), '4': array('L', [3])}
38
+ expected_core = {
39
+ "1": array("L", [0]),
40
+ "2": array("L", [1]),
41
+ "3": array("L", [2]),
42
+ "4": array("L", [3]),
43
+ }
39
44
  assert core_txt.coreid_index == expected_core
40
45
 
41
- expected_vernacular = {"1": array('L', [0, 1, 2]), "2": array('L', [3])}
46
+ expected_vernacular = {"1": array("L", [0, 1, 2]), "2": array("L", [3])}
42
47
  assert vernacular_txt.coreid_index == expected_vernacular
43
48
 
44
- expected_description = {"1": array('L', [0, 1]), "4": array('L', [2])}
49
+ expected_description = {"1": array("L", [0, 1]), "4": array("L", [2])}
45
50
  assert description_txt.coreid_index == expected_description
46
51
 
47
52
  with pytest.raises(AttributeError):