dataiter 0.49__tar.gz → 0.50__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 (25) hide show
  1. {dataiter-0.49 → dataiter-0.50}/COPYING +1 -1
  2. {dataiter-0.49 → dataiter-0.50}/PKG-INFO +2 -1
  3. {dataiter-0.49 → dataiter-0.50}/dataiter/__init__.py +1 -1
  4. {dataiter-0.49 → dataiter-0.50}/dataiter/data_frame.py +2 -2
  5. {dataiter-0.49 → dataiter-0.50}/dataiter/list_of_dicts.py +55 -4
  6. {dataiter-0.49 → dataiter-0.50}/dataiter/test/test_list_of_dicts.py +16 -0
  7. {dataiter-0.49 → dataiter-0.50}/dataiter/test/test_util.py +9 -4
  8. {dataiter-0.49 → dataiter-0.50}/dataiter/util.py +23 -16
  9. {dataiter-0.49 → dataiter-0.50}/dataiter/vector.py +3 -3
  10. {dataiter-0.49 → dataiter-0.50}/pyproject.toml +2 -2
  11. {dataiter-0.49 → dataiter-0.50}/.gitignore +0 -0
  12. {dataiter-0.49 → dataiter-0.50}/AUTHORS.md +0 -0
  13. {dataiter-0.49 → dataiter-0.50}/README.md +0 -0
  14. {dataiter-0.49 → dataiter-0.50}/dataiter/aggregate.py +0 -0
  15. {dataiter-0.49 → dataiter-0.50}/dataiter/deco.py +0 -0
  16. {dataiter-0.49 → dataiter-0.50}/dataiter/dt.py +0 -0
  17. {dataiter-0.49 → dataiter-0.50}/dataiter/geojson.py +0 -0
  18. {dataiter-0.49 → dataiter-0.50}/dataiter/io.py +0 -0
  19. {dataiter-0.49 → dataiter-0.50}/dataiter/test/__init__.py +0 -0
  20. {dataiter-0.49 → dataiter-0.50}/dataiter/test/test_aggregate.py +0 -0
  21. {dataiter-0.49 → dataiter-0.50}/dataiter/test/test_data_frame.py +0 -0
  22. {dataiter-0.49 → dataiter-0.50}/dataiter/test/test_dt.py +0 -0
  23. {dataiter-0.49 → dataiter-0.50}/dataiter/test/test_geojson.py +0 -0
  24. {dataiter-0.49 → dataiter-0.50}/dataiter/test/test_io.py +0 -0
  25. {dataiter-0.49 → dataiter-0.50}/dataiter/test/test_vector.py +0 -0
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2019-2023 Osmo Salomaa
3
+ Copyright (c) 2019-2024 Osmo Salomaa
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dataiter
3
- Version: 0.49
3
+ Version: 0.50
4
4
  Summary: Classes for data manipulation
5
5
  Project-URL: Homepage, https://github.com/otsaloma/dataiter
6
6
  Author-email: Osmo Salomaa <otsaloma@iki.fi>
@@ -11,6 +11,7 @@ Requires-Python: >=3.6.0
11
11
  Requires-Dist: attd>=0.3
12
12
  Requires-Dist: numpy>=1.7
13
13
  Requires-Dist: pandas>=1.0
14
+ Requires-Dist: wcwidth>=0.1
14
15
  Description-Content-Type: text/markdown
15
16
 
16
17
  Python Classes for Data Manipulation
@@ -24,7 +24,7 @@ import contextlib
24
24
 
25
25
  from dataiter import util
26
26
 
27
- __version__ = "0.49"
27
+ __version__ = "0.50"
28
28
 
29
29
  DEFAULT_PEEK_ELEMENTS = 10
30
30
  DEFAULT_PEEK_ITEMS = 3
@@ -1185,7 +1185,7 @@ class DataFrame(dict):
1185
1185
  max_width = max_width or util.get_print_width()
1186
1186
  truncate_width = truncate_width or dataiter.PRINT_TRUNCATE_WIDTH
1187
1187
  n = min(self.nrow, max_rows)
1188
- columns = {colname: util.pad(
1188
+ columns = {colname: util.upad(
1189
1189
  [colname] +
1190
1190
  [str(column.dtype)] +
1191
1191
  [str(x) for x in column[:n].to_strings(
@@ -1194,7 +1194,7 @@ class DataFrame(dict):
1194
1194
  for column in columns.values():
1195
1195
  column.insert(2, "─" * util.ulen(column[0]))
1196
1196
  row_numbers = [str(i) for i in range(n)]
1197
- row_numbers = util.pad(["", "", ""] + row_numbers)
1197
+ row_numbers = util.upad(["", "", ""] + row_numbers)
1198
1198
  # If the length of rows exceeds max_width, split to
1199
1199
  # batches of columns (like R's print.data.frame).
1200
1200
  rows_to_print = []
@@ -28,6 +28,7 @@ import json
28
28
  import operator
29
29
  import pickle
30
30
  import random
31
+ import sys
31
32
 
32
33
  from attd import AttributeDict
33
34
  from dataiter import deco
@@ -219,6 +220,18 @@ class ListOfDicts(list):
219
220
  """
220
221
  return self.__deepcopy__()
221
222
 
223
+ @deco.new_from_generator
224
+ def drop_na(self, *keys):
225
+ """
226
+ Return list without items that have missing values in `keys`.
227
+
228
+ >>> data = di.read_json("data/listings.json")
229
+ >>> data.drop_na("sqft")
230
+ """
231
+ for item in self:
232
+ if not any(item.get(x, None) is None for x in keys):
233
+ yield item
234
+
222
235
  @deco.new_from_generator
223
236
  def extend(self, other):
224
237
  """
@@ -245,8 +258,7 @@ class ListOfDicts(list):
245
258
  >>> data = data.fill_missing_keys()
246
259
  """
247
260
  if not key_value_pairs:
248
- keys = util.unique_keys(itertools.chain(*self))
249
- key_value_pairs = dict.fromkeys(keys, None)
261
+ key_value_pairs = dict.fromkeys(self.keys(), None)
250
262
  key_value_pairs = key_value_pairs.items()
251
263
  for item in self:
252
264
  for key, value in key_value_pairs:
@@ -423,6 +435,15 @@ class ListOfDicts(list):
423
435
  yield item
424
436
  yield self[i]
425
437
 
438
+ def keys(self):
439
+ """
440
+ Return an iterator over unique keys in all items.
441
+
442
+ >>> data = di.read_json("data/listings.json")
443
+ >>> list(data.keys())
444
+ """
445
+ yield from dict.fromkeys(itertools.chain(*self))
446
+
426
447
  @deco.obsoletes
427
448
  @deco.new_from_generator
428
449
  def left_join(self, other, *by):
@@ -539,6 +560,36 @@ class ListOfDicts(list):
539
560
  """
540
561
  print(self.to_string(max_items=max_items))
541
562
 
563
+ def print_memory_use(self):
564
+ """
565
+ Print memory use by key and total.
566
+
567
+ >>> data = di.read_json("data/listings.json")
568
+ >>> data.print_memory_use()
569
+ """
570
+ from dataiter import DataFrame
571
+ mem = DataFrame()
572
+ for key in self.keys():
573
+ new = DataFrame(key=key)
574
+ values = self.pluck(key)
575
+ values_real = list(filter(None, values))
576
+ first = values_real[0] if values_real else None
577
+ total = sum(sys.getsizeof(x) for x in values)
578
+ new.type = first.__class__.__name__
579
+ new.item_size = int(round(total / len(values)))
580
+ new.total_size = total
581
+ mem = mem.rbind(new)
582
+ new = DataFrame(key="TOTAL")
583
+ new.type = "--"
584
+ new.item_size = mem.item_size.sum()
585
+ new.total_size = mem.total_size.sum()
586
+ mem = mem.rbind(new)
587
+ # Format sizes into sensible values for display.
588
+ mem.item_size = [f"{x:.0f} B" for x in mem.item_size]
589
+ mem.total_size = [f"{x/1024**2:,.0f} MB" for x in mem.total_size]
590
+ mem.colnames = [x.upper() for x in mem.colnames]
591
+ print(mem)
592
+
542
593
  def print_na_counts(self):
543
594
  """
544
595
  Print counts of missing values by key.
@@ -550,7 +601,7 @@ class ListOfDicts(list):
550
601
  >>> data.print_na_counts()
551
602
  """
552
603
  print("Missing counts:")
553
- for key in util.unique_keys(itertools.chain(*self)):
604
+ for key in self.keys():
554
605
  n = sum(x.get(key, None) is None for x in self)
555
606
  if n == 0: continue
556
607
  pc = 100 * n / len(self)
@@ -838,7 +889,7 @@ class ListOfDicts(list):
838
889
  if not self:
839
890
  raise ValueError("Cannot write empty CSV file")
840
891
  # Take a superset of all keys.
841
- keys = util.unique_keys(itertools.chain(*self))
892
+ keys = list(self.keys())
842
893
  util.makedirs_for_file(path)
843
894
  with util.xopen(path, "wt", encoding=encoding) as f:
844
895
  writer = csv.DictWriter(f,
@@ -171,6 +171,12 @@ class TestListOfDicts:
171
171
  assert b._predecessor is a
172
172
  assert c._predecessor is None
173
173
 
174
+ def test_drop_na(self):
175
+ data = test.list_of_dicts("listings.json")
176
+ data = data.drop_na("sqft")
177
+ assert len(data) == 396
178
+ assert None not in data.pluck("sqft")
179
+
174
180
  def test_extend(self):
175
181
  orig = test.list_of_dicts("downloads.json")
176
182
  data = orig.extend(orig)
@@ -260,6 +266,10 @@ class TestListOfDicts:
260
266
  assert isinstance(data[100], AttributeDict)
261
267
  assert data[100] == item
262
268
 
269
+ def test_keys(self):
270
+ data = test.list_of_dicts("listings.json")
271
+ assert list(data.keys()) == ["id", "hood", "zipcode", "guests", "sqft", "price"]
272
+
263
273
  def test_left_join(self):
264
274
  orig = test.list_of_dicts("downloads.json")
265
275
  holidays = test.list_of_dicts("holidays.json")
@@ -321,6 +331,12 @@ class TestListOfDicts:
321
331
  assert len(downloads) == len(data)
322
332
  assert sum(downloads) == 541300639
323
333
 
334
+ @patch("builtins.print")
335
+ def test_print_memory_use(self, mock_print):
336
+ data = test.list_of_dicts("vehicles.json")
337
+ data.print_memory_use()
338
+ mock_print.assert_called()
339
+
324
340
  @patch("builtins.print")
325
341
  def test_print_na_counts(self, mock_print):
326
342
  data = test.list_of_dicts("vehicles.json")
@@ -112,10 +112,6 @@ class TestUtil:
112
112
  assert util.length([1]) == 1
113
113
  assert util.length([1, 2]) == 2
114
114
 
115
- def test_pad(self):
116
- assert util.pad(["a", "aa", "aaa"], align="right") == [" a", " aa", "aaa"]
117
- assert util.pad(["a", "aa", "aaa"], align="left") == ["a ", "aa ", "aaa"]
118
-
119
115
  def test_quote(self):
120
116
  assert util.quote("hello") == '"hello"'
121
117
  assert util.quote('"hello"') == '"\\"hello\\""'
@@ -140,6 +136,15 @@ class TestUtil:
140
136
  def test_unique_types(self):
141
137
  assert util.unique_types([1, 2, 3.3, np.nan, None]) == {int, float}
142
138
 
139
+ def test_upad(self):
140
+ assert util.upad(["a", "aa", "aaa"], align="right") == [" a", " aa", "aaa"]
141
+ assert util.upad(["a", "aa", "aaa"], align="left") == ["a ", "aa ", "aaa"]
142
+
143
+ def test_utruncate(self):
144
+ assert util.utruncate("abcdef", 4) == "abcd"
145
+ assert util.utruncate("abc\u200bdef", 4) == "abc\u200bd"
146
+ assert util.utruncate("abc\u200bdef\u200b", 4) == "abc\u200bd"
147
+
143
148
  def test_xopen_bz2(self):
144
149
  text = "test åäö"
145
150
  handle, path = tempfile.mkstemp(".bz2")
@@ -31,6 +31,7 @@ import numpy as np
31
31
  import os
32
32
  import shutil
33
33
  import string
34
+ import wcwidth
34
35
 
35
36
  from dataiter import deco
36
37
  from pathlib import Path
@@ -91,15 +92,6 @@ def length(value):
91
92
  def makedirs_for_file(path):
92
93
  return Path(path).parent.mkdir(parents=True, exist_ok=True)
93
94
 
94
- @deco.listify
95
- def pad(strings, *, align="right"):
96
- width = max(ulen(x) for x in strings)
97
- for value in strings:
98
- padding = " " * (width - ulen(value))
99
- yield (padding + value
100
- if align == "right"
101
- else value + padding)
102
-
103
95
  def parse_env_boolean(name):
104
96
  return {
105
97
  "1": True,
@@ -132,19 +124,34 @@ def unique_keys(keys):
132
124
 
133
125
  def ulen(string):
134
126
  # Return the display length of string accounting for
135
- # Unicode characters that have a display width of zero.
136
- return len(string
137
- .replace("\u200b", "") # zero width space
138
- .replace("\u200c", "") # zero width non-joiner
139
- .replace("\u200d", "") # zero width joiner
140
- .replace("\u2060", "") # word joiner
141
- )
127
+ # Unicode characters that have a display width != 1.
128
+ length = wcwidth.wcswidth(string)
129
+ return length if length >= 0 else 0
142
130
 
143
131
  def unique_types(seq):
144
132
  return set(x.__class__ for x in seq if
145
133
  x is not None and
146
134
  not (isinstance(x, float) and np.isnan(x)))
147
135
 
136
+ @deco.listify
137
+ def upad(strings, *, align="right"):
138
+ # Pad strings for display accounting for
139
+ # Unicode characters that have a display width != 1.
140
+ width = max(ulen(x) for x in strings)
141
+ for value in strings:
142
+ padding = " " * (width - ulen(value))
143
+ yield (padding + value
144
+ if align == "right"
145
+ else value + padding)
146
+
147
+ def utruncate(string, width):
148
+ # Truncate string to display width accounting for
149
+ # Unicode characters that have a display width != 1.
150
+ for i in range(1, len(string)):
151
+ if ulen(string[:i]) > width:
152
+ return string[:(i-1)]
153
+ return string
154
+
148
155
  def xopen(path, mode="r", **kwargs):
149
156
  if "b" not in mode:
150
157
  kwargs.setdefault("encoding", "utf-8")
@@ -647,7 +647,7 @@ class Vector(np.ndarray):
647
647
  if ksep is None:
648
648
  ksep = dataiter.PRINT_THOUSAND_SEPARATOR
649
649
  quote = util.quote if quote else identity
650
- pad = util.pad if pad else identity
650
+ pad = util.upad if pad else identity
651
651
  if self.is_float():
652
652
  strings = util.format_floats(self, ksep=ksep)
653
653
  return self.__class__.fast(pad(strings), str)
@@ -660,7 +660,7 @@ class Vector(np.ndarray):
660
660
  lines = strings[i].splitlines()
661
661
  if (util.ulen(strings[i]) > truncate_width or
662
662
  (len(lines) > 1 and truncate_width < inf)):
663
- strings[i] = lines[0][:(truncate_width-1)] + "…"
663
+ strings[i] = util.utruncate(lines[0], truncate_width-1) + "…"
664
664
  return self.__class__.fast(pad(strings), str)
665
665
  if self.is_string():
666
666
  strings = [quote(x) for x in self]
@@ -668,7 +668,7 @@ class Vector(np.ndarray):
668
668
  lines = strings[i].splitlines()
669
669
  if (util.ulen(strings[i]) > truncate_width or
670
670
  (len(lines) > 1 and truncate_width < inf)):
671
- strings[i] = lines[0][:(truncate_width-1)] + "…"
671
+ strings[i] = util.utruncate(lines[0], truncate_width-1) + "…"
672
672
  return self.__class__.fast(pad(strings), str)
673
673
  strings = [str(x) for x in self]
674
674
  return self.__class__.fast(pad(strings), str)
@@ -1,5 +1,5 @@
1
1
  [build-system]
2
- requires = ["hatchling"]
2
+ requires = ["hatchling==1.21.1"]
3
3
  build-backend = "hatchling.build"
4
4
 
5
5
  [project]
@@ -10,7 +10,7 @@ readme = "README.md"
10
10
  license = "MIT"
11
11
  requires-python = ">=3.6.0"
12
12
  authors = [{ name = "Osmo Salomaa", email = "otsaloma@iki.fi" }]
13
- dependencies = ["attd>=0.3", "numpy>=1.7", "pandas>=1.0"]
13
+ dependencies = ["attd>=0.3", "numpy>=1.7", "pandas>=1.0", "wcwidth>=0.1"]
14
14
 
15
15
  [project.urls]
16
16
  Homepage = "https://github.com/otsaloma/dataiter"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes