python-dwca-reader 0.15.1__tar.gz → 0.16.1__tar.gz

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.
Files changed (38) hide show
  1. {python-dwca-reader-0.15.1 → python-dwca-reader-0.16.1}/AUTHORS +1 -0
  2. {python-dwca-reader-0.15.1 → python-dwca-reader-0.16.1}/CHANGES.txt +14 -1
  3. {python-dwca-reader-0.15.1 → python-dwca-reader-0.16.1}/PKG-INFO +2 -1
  4. {python-dwca-reader-0.15.1 → python-dwca-reader-0.16.1}/dwca/descriptors.py +1 -1
  5. {python-dwca-reader-0.15.1 → python-dwca-reader-0.16.1}/dwca/exceptions.py +1 -1
  6. {python-dwca-reader-0.15.1 → python-dwca-reader-0.16.1}/dwca/files.py +9 -16
  7. {python-dwca-reader-0.15.1 → python-dwca-reader-0.16.1}/dwca/helpers.py +1 -1
  8. {python-dwca-reader-0.15.1 → python-dwca-reader-0.16.1}/dwca/read.py +19 -4
  9. python-dwca-reader-0.16.1/dwca/star_record.py +59 -0
  10. {python-dwca-reader-0.15.1 → python-dwca-reader-0.16.1}/dwca/test/test_datafile.py +4 -0
  11. {python-dwca-reader-0.15.1 → python-dwca-reader-0.16.1}/dwca/test/test_dwcareader.py +20 -0
  12. python-dwca-reader-0.16.1/dwca/test/test_star_record.py +54 -0
  13. python-dwca-reader-0.16.1/dwca/version.py +1 -0
  14. {python-dwca-reader-0.15.1 → python-dwca-reader-0.16.1}/python_dwca_reader.egg-info/PKG-INFO +2 -1
  15. {python-dwca-reader-0.15.1 → python-dwca-reader-0.16.1}/python_dwca_reader.egg-info/SOURCES.txt +2 -0
  16. python-dwca-reader-0.16.1/requirements-dev.txt +4 -0
  17. {python-dwca-reader-0.15.1 → python-dwca-reader-0.16.1}/setup.py +1 -0
  18. python-dwca-reader-0.15.1/dwca/version.py +0 -1
  19. python-dwca-reader-0.15.1/requirements-dev.txt +0 -3
  20. {python-dwca-reader-0.15.1 → python-dwca-reader-0.16.1}/LICENSE.txt +0 -0
  21. {python-dwca-reader-0.15.1 → python-dwca-reader-0.16.1}/MANIFEST.in +0 -0
  22. {python-dwca-reader-0.15.1 → python-dwca-reader-0.16.1}/README.rst +0 -0
  23. {python-dwca-reader-0.15.1 → python-dwca-reader-0.16.1}/code_of_conduct.txt +0 -0
  24. {python-dwca-reader-0.15.1 → python-dwca-reader-0.16.1}/dwca/__init__.py +0 -0
  25. {python-dwca-reader-0.15.1 → python-dwca-reader-0.16.1}/dwca/darwincore/__init__.py +0 -0
  26. {python-dwca-reader-0.15.1 → python-dwca-reader-0.16.1}/dwca/darwincore/build_dc_terms_list.py +0 -0
  27. {python-dwca-reader-0.15.1 → python-dwca-reader-0.16.1}/dwca/darwincore/terms.py +0 -0
  28. {python-dwca-reader-0.15.1 → python-dwca-reader-0.16.1}/dwca/darwincore/utils.py +0 -0
  29. {python-dwca-reader-0.15.1 → python-dwca-reader-0.16.1}/dwca/minibench.py +0 -0
  30. {python-dwca-reader-0.15.1 → python-dwca-reader-0.16.1}/dwca/rows.py +0 -0
  31. {python-dwca-reader-0.15.1 → python-dwca-reader-0.16.1}/dwca/test/__init__.py +0 -0
  32. {python-dwca-reader-0.15.1 → python-dwca-reader-0.16.1}/dwca/test/helpers.py +0 -0
  33. {python-dwca-reader-0.15.1 → python-dwca-reader-0.16.1}/dwca/test/test_descriptors.py +0 -0
  34. {python-dwca-reader-0.15.1 → python-dwca-reader-0.16.1}/dwca/test/test_rows.py +0 -0
  35. {python-dwca-reader-0.15.1 → python-dwca-reader-0.16.1}/dwca/vendor.py +0 -0
  36. {python-dwca-reader-0.15.1 → python-dwca-reader-0.16.1}/python_dwca_reader.egg-info/dependency_links.txt +0 -0
  37. {python-dwca-reader-0.15.1 → python-dwca-reader-0.16.1}/python_dwca_reader.egg-info/top_level.txt +0 -0
  38. {python-dwca-reader-0.15.1 → python-dwca-reader-0.16.1}/setup.cfg +0 -0
@@ -5,6 +5,7 @@ Here is an inevitably incomplete list of MUCH-APPRECIATED CONTRIBUTORS -- people
5
5
  bugs, improved documentation, ... and generally made this library much better:
6
6
 
7
7
  Dimitri Brosens
8
+ Scott Brown
8
9
  Ben Cail
9
10
  Julien Cigar
10
11
  Pedro Correia de Siracusa
@@ -1,8 +1,21 @@
1
+ v0.16.1 (2024-07-08)
2
+ --------------------
3
+
4
+ - Added official support for Python 3.12
5
+ - Proper error message when trying to use an unsupported combination of Pandas option
6
+ and archives with default values (issue #106).
7
+
8
+ v0.16.0 (2023-11-13)
9
+ --------------------
10
+
11
+ - Dropped Python 3.5 and 3.6 support. Use Python 3.7 to 3.11 instead.
12
+ - Added a star record iterator to DwCAReader (thanks to @csbrown)
13
+
1
14
  v0.15.1 (2023-01-17)
2
15
  --------------------
3
16
 
4
17
  - Workaround for errors with buggy GBIF downloads, see https://github.com/gbif/portal-feedback/issues/4533
5
- - Dropped Python 3.5
18
+
6
19
 
7
20
  v0.15.0 (2020-09-09)
8
21
  --------------------
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-dwca-reader
3
- Version: 0.15.1
3
+ Version: 0.16.1
4
4
  Summary: A simple Python package to read Darwin Core Archive (DwC-A) files.
5
5
  Home-page: https://github.com/BelgianBiodiversityPlatform/python-dwca-reader
6
6
  Author: Nicolas Noé - Belgian Biodiversity Platform
@@ -11,6 +11,7 @@ Classifier: Programming Language :: Python :: 3.7
11
11
  Classifier: Programming Language :: Python :: 3.8
12
12
  Classifier: Programming Language :: Python :: 3.9
13
13
  Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
14
15
  Classifier: Programming Language :: Python :: Implementation :: PyPy
15
16
  License-File: LICENSE.txt
16
17
  License-File: AUTHORS
@@ -108,7 +108,7 @@ class DataFileDescriptor(object):
108
108
  """
109
109
  file_encoding = "utf-8"
110
110
 
111
- with io.open(datafile_path, 'rU', encoding=file_encoding) as datafile:
111
+ with io.open(datafile_path, 'r', encoding=file_encoding) as datafile:
112
112
  # Autodetect fields/lines termination
113
113
  dialect = csv.Sniffer().sniff(datafile.readline())
114
114
 
@@ -14,4 +14,4 @@ class InvalidSimpleArchive(InvalidArchive):
14
14
 
15
15
 
16
16
  class NotADataFile(Exception):
17
- """The file doesn't exists or is not a data file."""
17
+ """The file doesn't exist or is not a data file."""
@@ -6,7 +6,7 @@ from array import array
6
6
  from typing import List, Union, IO, Dict, Optional
7
7
 
8
8
  from dwca.descriptors import DataFileDescriptor
9
- from dwca.rows import CoreRow, ExtensionRow
9
+ from dwca.rows import CoreRow, ExtensionRow, Row
10
10
 
11
11
 
12
12
  class CSVDataFile(object):
@@ -90,26 +90,15 @@ class CSVDataFile(object):
90
90
  core_id2: [8, 10] # Rows at position 8 and 10 references a Core Row whose ID is core_id2
91
91
  }
92
92
 
93
- :raises: AttributeError if accessed on a core data file.
94
-
95
- .. warning::
96
-
97
- for permformance reasons, dictionary values are arrays('L') instead of regular python lists
98
-
99
93
  .. warning::
100
94
 
101
- coreid_index is only available for extension data files.
95
+ for performance reasons, dictionary values are arrays('L') instead of regular python lists
102
96
 
103
97
  .. warning::
104
98
 
105
99
  Creating this index can be time and memory consuming for large archives, so it's created on the fly
106
100
  at first access.
107
101
  """
108
- if self.file_descriptor.represents_corefile:
109
- raise AttributeError(
110
- "coreid_index is only available for extension data files"
111
- )
112
-
113
102
  if self._coreid_index is None:
114
103
  self._coreid_index = self._build_coreid_index()
115
104
 
@@ -120,14 +109,18 @@ class CSVDataFile(object):
120
109
  index = {} # type: Dict[str, array[int]]
121
110
 
122
111
  for position, row in enumerate(self):
123
- tmp = ExtensionRow(row, position, self.file_descriptor)
124
- index.setdefault(tmp.core_id, array('L')).append(position)
112
+ if self.file_descriptor.represents_corefile:
113
+ tmp = CoreRow(row, position, self.file_descriptor)
114
+ index.setdefault(tmp.id, array('L')).append(position)
115
+ else:
116
+ tmp = ExtensionRow(row, position, self.file_descriptor)
117
+ index.setdefault(tmp.core_id, array('L')).append(position)
125
118
 
126
119
  return index
127
120
 
128
121
  # TODO: For ExtensionRow and a specific field only, generalize ?
129
122
  # TODO: What happens if called on a Core Row?
130
- def get_all_rows_by_coreid(self, core_id: int) -> List[ExtensionRow]:
123
+ def get_all_rows_by_coreid(self, core_id: int) -> List[Row]:
131
124
  """Return a list of :class:`dwca.rows.ExtensionRow` whose Core Id field match `core_id`."""
132
125
  if core_id not in self.coreid_index:
133
126
  return []
@@ -6,7 +6,7 @@ def remove_tree(path, retries=3, sleep=0.1):
6
6
  for i in range(retries):
7
7
  try:
8
8
  rmtree(path, False)
9
- except WindowsError:
9
+ except OSError:
10
10
  time.sleep(sleep)
11
11
  else:
12
12
  break
@@ -10,6 +10,8 @@ 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
+
13
15
  import dwca.vendor
14
16
  from dwca.descriptors import ArchiveDescriptor, DataFileDescriptor, shorten_term
15
17
  from dwca.exceptions import RowNotFound, InvalidArchive, InvalidSimpleArchive, NotADataFile
@@ -186,7 +188,9 @@ class DwCAReader(object):
186
188
  .. note::
187
189
 
188
190
  Default values of Darwin Core Archive are supported: A column will be added to the DataFrame if a term has
189
- a default value in the Metafile (but no corresponding column in the CSV Data File).
191
+ a default value in the Metafile (but no corresponding column in the CSV Data File). This is unfortunately
192
+ not supported in case the value returned by `pandas.read_csv()` is a `TextFileReader` (e.g. when using
193
+ `chunksize` or `iterator=True`).
190
194
  """
191
195
  datafile_descriptor = self.get_descriptor_for(relative_path) # type: DataFileDescriptor
192
196
 
@@ -200,15 +204,25 @@ class DwCAReader(object):
200
204
  kwargs['header'] = None
201
205
  kwargs['names'] = datafile_descriptor.short_headers
202
206
 
203
- df = read_csv(self.absolute_temporary_path(relative_path), **kwargs)
207
+ df_or_textreader = read_csv(self.absolute_temporary_path(relative_path), **kwargs)
204
208
 
205
209
  # Add a column for default values, if present in the file descriptor
206
210
  for field in datafile_descriptor.fields:
207
211
  field_default_value = field['default']
208
212
  if field_default_value is not None:
209
- df[shorten_term(field['term'])] = field_default_value
213
+ if isinstance(df_or_textreader, TextFileReader):
214
+ # I don't see how to assign default values to a TextFileReader, so
215
+ # this is currently unsupported
216
+ raise ValueError(
217
+ "Pandas read_csv() was called with a chunksize or iterator=True, "
218
+ "and therefore returns a TextFileReader instead of a DataFrame "
219
+ "which is not supported in combination with default values of "
220
+ "the archive."
221
+ )
222
+
223
+ df_or_textreader[shorten_term(field['term'])] = field_default_value
210
224
 
211
- return df
225
+ return df_or_textreader
212
226
 
213
227
  def orphaned_extension_rows(self) -> Dict[str, Dict[str, List[int]]]:
214
228
  """Return a dict of the orphaned extension rows.
@@ -434,6 +448,7 @@ class DwCAReader(object):
434
448
  except zipfile.BadZipfile:
435
449
  # Doesn't look like a valid zip, let's see if it's a tar archive (possibly compressed)
436
450
  try:
451
+ # TODO: Once we only support Python 3.12+, we should pass the filter="data" argument to extractall()
437
452
  tarfile.open(self.archive_path, 'r:*').extractall(tmp_dir)
438
453
  except tarfile.ReadError:
439
454
  raise InvalidArchive("The archive cannot be read. Is it a .zip or .tgz file?")
@@ -0,0 +1,59 @@
1
+ from dwca.files import CSVDataFile
2
+ from typing import List
3
+ from typing_extensions import Literal
4
+ import itertools
5
+
6
+
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.
16
+ """
17
+ def __init__(self, files_to_join: List[CSVDataFile], how: Literal["inner", "outer"] = "inner"):
18
+ self.files_to_join = files_to_join
19
+
20
+ # gather the coreids we want to join over.
21
+ self.common_core = set(self.files_to_join[0].coreid_index)
22
+ for data_file in self.files_to_join[1:]:
23
+ # inner join: coreid must be in all files
24
+ if how == "inner":
25
+ self.common_core &= set(data_file.coreid_index)
26
+ # outer join: coreid may be in any files
27
+ elif how == "outer":
28
+ self.common_core |= set(data_file.coreid_index)
29
+
30
+ # initialize iterator variables
31
+ self._common_core_iterator = iter(self.common_core)
32
+ self._cross_product_iterator = iter([])
33
+
34
+
35
+ def __next__(self):
36
+ # the next combination of rows matching this coreid
37
+ next_positions = next(self._cross_product_iterator, None)
38
+ # we finished all the combinations for this coreid
39
+ if not next_positions:
40
+ # get the next coreid
41
+ self._current_coreid = next(self._common_core_iterator)
42
+ 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
+ # this iterates over all combinations of rows matching a coreid from all files
46
+ self._cross_product_iterator = itertools.product(
47
+ *(
48
+ csv_file.coreid_index[self._current_coreid]
49
+ for csv_file in self._files_with_current_coreid
50
+ ))
51
+ # go back and try to iterate over the rows for the new coreid
52
+ return next(self)
53
+ # zip up this combination of rows from all of the files.
54
+ return (
55
+ csv_file.get_row_by_position(position) for position, csv_file in zip(next_positions, self._files_with_current_coreid)
56
+ )
57
+
58
+
59
+ def __iter__(self): return self
@@ -31,9 +31,13 @@ class TestCSVDataFile(unittest.TestCase):
31
31
  with DwCAReader(sample_data_path("dwca-2extensions.zip")) as dwca:
32
32
  extension_files = dwca.extension_files
33
33
 
34
+ core_txt = dwca.core_file
34
35
  description_txt = extension_files[0]
35
36
  vernacular_txt = extension_files[1]
36
37
 
38
+ expected_core = {'1': array('L', [0]), '2': array('L', [1]), '3': array('L', [2]), '4': array('L', [3])}
39
+ assert core_txt.coreid_index == expected_core
40
+
37
41
  expected_vernacular = {"1": array('L', [0, 1, 2]), "2": array('L', [3])}
38
42
  assert vernacular_txt.coreid_index == expected_vernacular
39
43
 
@@ -48,6 +48,26 @@ class TestPandasIntegration(unittest.TestCase):
48
48
  assert df["scientificName"].values.tolist() == \
49
49
  ["tetraodon fluviatilis", "betta splendens"]
50
50
 
51
+ def test_pd_read_chunked_default_value(self):
52
+ """Pandas chuncksize should not be used with default values.
53
+
54
+ See: https://github.com/BelgianBiodiversityPlatform/python-dwca-reader/issues/106
55
+ """
56
+ with DwCAReader(sample_data_path("dwca-test-default.zip")) as dwca:
57
+ with pytest.raises(ValueError):
58
+ for chunk in dwca.pd_read("occurrence.txt", chunksize=1):
59
+ pass
60
+
61
+ def test_pd_read_chunked(self):
62
+ """If no default values are available in the archive, chunksize should work.
63
+
64
+ See: https://github.com/BelgianBiodiversityPlatform/python-dwca-reader/issues/106
65
+ """
66
+ with DwCAReader(sample_data_path("dwca-simple-test-archive.zip")) as dwca:
67
+ for chunk in dwca.pd_read("occurrence.txt", chunksize=2):
68
+ assert isinstance(chunk, pd.DataFrame)
69
+
70
+
51
71
  def test_pd_read_no_data_files(self):
52
72
  with DwCAReader(sample_data_path("dwca-simple-test-archive.zip")) as dwca:
53
73
  with pytest.raises(NotADataFile):
@@ -0,0 +1,54 @@
1
+ from dwca.read import DwCAReader
2
+ from dwca.rows import CoreRow
3
+ from dwca.star_record import StarRecordIterator
4
+ from .helpers import sample_data_path
5
+ import unittest
6
+
7
+ class TestStarRecordIterator(unittest.TestCase):
8
+
9
+ def test_inner_join(self):
10
+
11
+ expected_inner_join = frozenset({
12
+ frozenset({('1', 0, 'Description'), ('1', 0, 'Taxon'), ('1', 0, 'VernacularName')}),
13
+ frozenset({('1', 0, 'Description'), ('1', 0, 'Taxon'), ('1', 1, 'VernacularName')}),
14
+ frozenset({('1', 0, 'Description'), ('1', 0, 'Taxon'), ('1', 2, 'VernacularName')}),
15
+ frozenset({('1', 1, 'Description'), ('1', 0, 'Taxon'), ('1', 0, 'VernacularName')}),
16
+ frozenset({('1', 1, 'Description'), ('1', 0, 'Taxon'), ('1', 1, 'VernacularName')}),
17
+ frozenset({('1', 1, 'Description'), ('1', 0, 'Taxon'), ('1', 2, 'VernacularName')})
18
+ })
19
+
20
+ with DwCAReader(sample_data_path("dwca-2extensions.zip")) as dwca:
21
+ star_records = StarRecordIterator(dwca.extension_files + [dwca.core_file], how="inner")
22
+ stars = []
23
+ for star_record in star_records:
24
+ rows = []
25
+ for row in star_record:
26
+ rows.append((row.id if isinstance(row, CoreRow) else row.core_id, row.position, row.rowtype.split('/')[-1]))
27
+ stars.append(frozenset(rows))
28
+
29
+ assert frozenset(stars) == expected_inner_join
30
+
31
+ def test_outer_join(self):
32
+
33
+ expected_outer_join = frozenset({
34
+ frozenset({('4', 2, 'Description'), ('4', 3, 'Taxon')}),
35
+ frozenset({('1', 0, 'Description'), ('1', 0, 'Taxon'), ('1', 0, 'VernacularName')}),
36
+ frozenset({('1', 0, 'Description'), ('1', 0, 'Taxon'), ('1', 1, 'VernacularName')}),
37
+ frozenset({('1', 0, 'Description'), ('1', 0, 'Taxon'), ('1', 2, 'VernacularName')}),
38
+ frozenset({('1', 1, 'Description'), ('1', 0, 'Taxon'), ('1', 0, 'VernacularName')}),
39
+ frozenset({('1', 1, 'Description'), ('1', 0, 'Taxon'), ('1', 1, 'VernacularName')}),
40
+ frozenset({('1', 1, 'Description'), ('1', 0, 'Taxon'), ('1', 2, 'VernacularName')}),
41
+ frozenset({('3', 2, 'Taxon')}),
42
+ frozenset({('2', 1, 'Taxon'), ('2', 3, 'VernacularName')})
43
+ })
44
+
45
+ with DwCAReader(sample_data_path("dwca-2extensions.zip")) as dwca:
46
+ star_records = StarRecordIterator(dwca.extension_files + [dwca.core_file], how="outer")
47
+ stars = []
48
+ for star_record in star_records:
49
+ rows = []
50
+ for row in star_record:
51
+ rows.append((row.id if isinstance(row, CoreRow) else row.core_id, row.position, row.rowtype.split('/')[-1]))
52
+ stars.append(frozenset(rows))
53
+
54
+ assert frozenset(stars) == expected_outer_join
@@ -0,0 +1 @@
1
+ __version__ = '0.16.1'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-dwca-reader
3
- Version: 0.15.1
3
+ Version: 0.16.1
4
4
  Summary: A simple Python package to read Darwin Core Archive (DwC-A) files.
5
5
  Home-page: https://github.com/BelgianBiodiversityPlatform/python-dwca-reader
6
6
  Author: Nicolas Noé - Belgian Biodiversity Platform
@@ -11,6 +11,7 @@ Classifier: Programming Language :: Python :: 3.7
11
11
  Classifier: Programming Language :: Python :: 3.8
12
12
  Classifier: Programming Language :: Python :: 3.9
13
13
  Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
14
15
  Classifier: Programming Language :: Python :: Implementation :: PyPy
15
16
  License-File: LICENSE.txt
16
17
  License-File: AUTHORS
@@ -15,6 +15,7 @@ dwca/helpers.py
15
15
  dwca/minibench.py
16
16
  dwca/read.py
17
17
  dwca/rows.py
18
+ dwca/star_record.py
18
19
  dwca/vendor.py
19
20
  dwca/version.py
20
21
  dwca/darwincore/__init__.py
@@ -27,6 +28,7 @@ dwca/test/test_datafile.py
27
28
  dwca/test/test_descriptors.py
28
29
  dwca/test/test_dwcareader.py
29
30
  dwca/test/test_rows.py
31
+ dwca/test/test_star_record.py
30
32
  python_dwca_reader.egg-info/PKG-INFO
31
33
  python_dwca_reader.egg-info/SOURCES.txt
32
34
  python_dwca_reader.egg-info/dependency_links.txt
@@ -0,0 +1,4 @@
1
+ pandas
2
+ mock==2.0.0
3
+ pytest
4
+ typing-extensions
@@ -18,6 +18,7 @@ setup(
18
18
  'Programming Language :: Python :: 3.8',
19
19
  'Programming Language :: Python :: 3.9',
20
20
  'Programming Language :: Python :: 3.10',
21
+ 'Programming Language :: Python :: 3.11',
21
22
  'Programming Language :: Python :: Implementation :: PyPy',
22
23
  ]
23
24
  )
@@ -1 +0,0 @@
1
- __version__ = '0.15.1'
@@ -1,3 +0,0 @@
1
- pandas
2
- mock==2.0.0
3
- pytest