dataiter 0.999__tar.gz → 1.0__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.
- {dataiter-0.999 → dataiter-1.0}/COPYING +1 -1
- {dataiter-0.999 → dataiter-1.0}/PKG-INFO +2 -2
- {dataiter-0.999 → dataiter-1.0}/dataiter/__init__.py +2 -1
- {dataiter-0.999 → dataiter-1.0}/dataiter/data_frame.py +58 -28
- {dataiter-0.999 → dataiter-1.0}/dataiter/dt.py +0 -5
- {dataiter-0.999 → dataiter-1.0}/dataiter/dtypes.py +6 -0
- dataiter-1.0/dataiter/regex.py +148 -0
- {dataiter-0.999 → dataiter-1.0}/dataiter/test/test_data_frame.py +66 -3
- dataiter-1.0/dataiter/test/test_regex.py +92 -0
- {dataiter-0.999 → dataiter-1.0}/dataiter/test/test_vector.py +16 -0
- {dataiter-0.999 → dataiter-1.0}/dataiter/vector.py +144 -3
- {dataiter-0.999 → dataiter-1.0}/pyproject.toml +2 -2
- {dataiter-0.999 → dataiter-1.0}/.gitignore +0 -0
- {dataiter-0.999 → dataiter-1.0}/AUTHORS.md +0 -0
- {dataiter-0.999 → dataiter-1.0}/README.md +0 -0
- {dataiter-0.999 → dataiter-1.0}/dataiter/aggregate.py +0 -0
- {dataiter-0.999 → dataiter-1.0}/dataiter/deco.py +0 -0
- {dataiter-0.999 → dataiter-1.0}/dataiter/geojson.py +0 -0
- {dataiter-0.999 → dataiter-1.0}/dataiter/io.py +0 -0
- {dataiter-0.999 → dataiter-1.0}/dataiter/list_of_dicts.py +0 -0
- {dataiter-0.999 → dataiter-1.0}/dataiter/test/__init__.py +0 -0
- {dataiter-0.999 → dataiter-1.0}/dataiter/test/test_aggregate.py +0 -0
- {dataiter-0.999 → dataiter-1.0}/dataiter/test/test_dt.py +0 -0
- {dataiter-0.999 → dataiter-1.0}/dataiter/test/test_geojson.py +0 -0
- {dataiter-0.999 → dataiter-1.0}/dataiter/test/test_io.py +0 -0
- {dataiter-0.999 → dataiter-1.0}/dataiter/test/test_list_of_dicts.py +0 -0
- {dataiter-0.999 → dataiter-1.0}/dataiter/test/test_util.py +0 -0
- {dataiter-0.999 → dataiter-1.0}/dataiter/util.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: dataiter
|
|
3
|
-
Version: 0
|
|
3
|
+
Version: 1.0
|
|
4
4
|
Summary: Simple, light-weight data frames for Python
|
|
5
5
|
Project-URL: Homepage, https://github.com/otsaloma/dataiter
|
|
6
6
|
Author-email: Osmo Salomaa <otsaloma@iki.fi>
|
|
@@ -10,7 +10,7 @@ License-File: COPYING
|
|
|
10
10
|
Requires-Python: >=3.9.0
|
|
11
11
|
Requires-Dist: attd>=0.3
|
|
12
12
|
Requires-Dist: numpy<3.0,>=2.0
|
|
13
|
-
Requires-Dist:
|
|
13
|
+
Requires-Dist: pyarrow>=2.0
|
|
14
14
|
Requires-Dist: wcwidth>=0.1
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
16
|
|
|
@@ -25,7 +25,7 @@ import numpy as np
|
|
|
25
25
|
|
|
26
26
|
from dataiter import util
|
|
27
27
|
|
|
28
|
-
__version__ = "0
|
|
28
|
+
__version__ = "1.0"
|
|
29
29
|
|
|
30
30
|
DEFAULT_PEEK_ELEMENTS = 10
|
|
31
31
|
DEFAULT_PEEK_ITEMS = 3
|
|
@@ -94,6 +94,7 @@ from dataiter.data_frame import DataFrameColumn # noqa
|
|
|
94
94
|
from dataiter.geojson import GeoJSON # noqa
|
|
95
95
|
from dataiter.list_of_dicts import ListOfDicts # noqa
|
|
96
96
|
from dataiter import dt # noqa
|
|
97
|
+
from dataiter import regex # noqa
|
|
97
98
|
|
|
98
99
|
from dataiter.aggregate import all # noqa
|
|
99
100
|
from dataiter.aggregate import any # noqa
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
21
|
# THE SOFTWARE.
|
|
22
22
|
|
|
23
|
+
import codecs
|
|
23
24
|
import dataiter
|
|
24
25
|
import functools
|
|
25
26
|
import itertools
|
|
@@ -474,16 +475,29 @@ class DataFrame(dict):
|
|
|
474
475
|
yield colname, np.delete(column, rows)
|
|
475
476
|
|
|
476
477
|
@classmethod
|
|
478
|
+
@deco.new_from_generator
|
|
477
479
|
def from_arrow(cls, data, *, dtypes={}):
|
|
478
480
|
"""
|
|
479
481
|
Return a new data frame from ``pyarrow.Table`` `data`.
|
|
480
482
|
|
|
481
483
|
`dtypes` is an optional dict mapping column names to NumPy datatypes.
|
|
482
484
|
"""
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
485
|
+
for name, column in zip(data.column_names, data.columns):
|
|
486
|
+
req_dtype = dtypes.get(name, None)
|
|
487
|
+
na = column.is_null(nan_is_null=True).to_numpy()
|
|
488
|
+
column = column.to_numpy()
|
|
489
|
+
if np.issubdtype(column.dtype, np.object_):
|
|
490
|
+
if req_dtype is None or np.dtype(req_dtype) != np.dtype(object):
|
|
491
|
+
# Likely to be strings, but not necessarily.
|
|
492
|
+
# Force type-guessing in Vector.fast.
|
|
493
|
+
column = column.tolist()
|
|
494
|
+
column = DataFrameColumn.fast(column, req_dtype)
|
|
495
|
+
if na.any():
|
|
496
|
+
if column.dtype != column.na_dtype:
|
|
497
|
+
# Upcast integer to float if needed.
|
|
498
|
+
column = column.astype(column.na_dtype)
|
|
499
|
+
column[na] = column.na_value
|
|
500
|
+
yield name, column
|
|
487
501
|
|
|
488
502
|
@classmethod
|
|
489
503
|
def from_json(cls, string, *, columns=[], dtypes={}, **kwargs):
|
|
@@ -508,21 +522,29 @@ class DataFrame(dict):
|
|
|
508
522
|
return cls(**data)
|
|
509
523
|
|
|
510
524
|
@classmethod
|
|
525
|
+
@deco.new_from_generator
|
|
511
526
|
def from_pandas(cls, data, *, dtypes={}):
|
|
512
527
|
"""
|
|
513
528
|
Return a new data frame from ``pandas.DataFrame`` `data`.
|
|
514
529
|
|
|
515
530
|
`dtypes` is an optional dict mapping column names to NumPy datatypes.
|
|
516
531
|
"""
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
if np.issubdtype(
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
532
|
+
for name in data.columns:
|
|
533
|
+
req_dtype = dtypes.get(name, None)
|
|
534
|
+
na = data[name].isna().to_numpy(copy=True)
|
|
535
|
+
column = data[name].to_numpy(copy=True)
|
|
536
|
+
if np.issubdtype(column.dtype, np.object_):
|
|
537
|
+
if req_dtype is None or np.dtype(req_dtype) != np.dtype(object):
|
|
538
|
+
# Likely to be strings, but not necessarily.
|
|
539
|
+
# Force type-guessing in Vector.fast.
|
|
540
|
+
column = column.tolist()
|
|
541
|
+
column = DataFrameColumn.fast(column, req_dtype)
|
|
542
|
+
if na.any():
|
|
543
|
+
if column.dtype != column.na_dtype:
|
|
544
|
+
# Upcast integer to float if needed.
|
|
545
|
+
column = column.astype(column.na_dtype)
|
|
546
|
+
column[na] = column.na_value
|
|
547
|
+
yield name, column
|
|
526
548
|
|
|
527
549
|
def full_join(self, other, *by):
|
|
528
550
|
"""
|
|
@@ -720,8 +742,9 @@ class DataFrame(dict):
|
|
|
720
742
|
self._check_dimensions()
|
|
721
743
|
return len(self)
|
|
722
744
|
|
|
723
|
-
|
|
724
|
-
|
|
745
|
+
@classmethod
|
|
746
|
+
def _new(cls, *args, **kwargs):
|
|
747
|
+
return cls(*args, **kwargs)
|
|
725
748
|
|
|
726
749
|
@property
|
|
727
750
|
def nrow(self):
|
|
@@ -857,19 +880,17 @@ class DataFrame(dict):
|
|
|
857
880
|
`columns` is an optional list of columns to limit to. `dtypes` is an
|
|
858
881
|
optional dict mapping column names to NumPy datatypes.
|
|
859
882
|
"""
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
parse_dates=False,
|
|
867
|
-
encoding=encoding,
|
|
868
|
-
low_memory=False)
|
|
883
|
+
from pyarrow import csv
|
|
884
|
+
table = csv.read_csv(
|
|
885
|
+
path,
|
|
886
|
+
read_options=csv.ReadOptions(encoding=encoding, autogenerate_column_names=not header),
|
|
887
|
+
parse_options=csv.ParseOptions(delimiter=sep, newlines_in_values=True),
|
|
888
|
+
convert_options=csv.ConvertOptions(include_columns=columns))
|
|
869
889
|
|
|
870
890
|
if not header:
|
|
871
|
-
|
|
872
|
-
|
|
891
|
+
names = util.generate_colnames(table.shape[1])
|
|
892
|
+
table = table.rename_columns(names)
|
|
893
|
+
return cls.from_arrow(table, dtypes=dtypes)
|
|
873
894
|
|
|
874
895
|
@classmethod
|
|
875
896
|
def read_json(cls, path, *, encoding="utf-8", columns=[], dtypes={}, **kwargs):
|
|
@@ -1255,9 +1276,18 @@ class DataFrame(dict):
|
|
|
1255
1276
|
|
|
1256
1277
|
Will automatically compress if `path` ends in ``.bz2|.gz|.xz``.
|
|
1257
1278
|
"""
|
|
1258
|
-
|
|
1279
|
+
from pyarrow import csv
|
|
1280
|
+
table = self.to_arrow()
|
|
1259
1281
|
util.makedirs_for_file(path)
|
|
1260
|
-
|
|
1282
|
+
csv.write_csv(table, path, write_options=csv.WriteOptions(include_header=header,
|
|
1283
|
+
delimiter=sep,
|
|
1284
|
+
quoting_style="needed"))
|
|
1285
|
+
|
|
1286
|
+
# XXX: Arrow's WriteOptions doesn't include encoding.
|
|
1287
|
+
# If user requested something else than UTF-8, we need to rewrite!
|
|
1288
|
+
if codecs.lookup(encoding) != codecs.lookup("utf-8"):
|
|
1289
|
+
with util.xopen(path, "rt", encoding="utf-8") as f: text = f.read()
|
|
1290
|
+
with util.xopen(path, "wt", encoding=encoding) as f: f.write(text)
|
|
1261
1291
|
|
|
1262
1292
|
def write_json(self, path, *, encoding="utf-8", **kwargs):
|
|
1263
1293
|
"""
|
|
@@ -50,7 +50,6 @@ def from_string(x, format):
|
|
|
50
50
|
if util.is_scalar(x):
|
|
51
51
|
x = Vector([x], str)
|
|
52
52
|
return from_string(x, format)[0]
|
|
53
|
-
x = util.sequencify(x)
|
|
54
53
|
assert isinstance(x, np.ndarray)
|
|
55
54
|
assert isinstance(x.dtype, StringDType)
|
|
56
55
|
out = np.full_like(x, None, object)
|
|
@@ -150,7 +149,6 @@ def _pull_datetime(x, function):
|
|
|
150
149
|
if util.is_scalar(x):
|
|
151
150
|
x = Vector([x], np.datetime64)
|
|
152
151
|
return _pull_datetime(x, function)[0]
|
|
153
|
-
x = util.sequencify(x)
|
|
154
152
|
assert isinstance(x, np.ndarray)
|
|
155
153
|
assert np.issubdtype(x.dtype, np.datetime64)
|
|
156
154
|
out = np.full_like(x, np.nan)
|
|
@@ -165,7 +163,6 @@ def _pull_int(x, function):
|
|
|
165
163
|
if util.is_scalar(x):
|
|
166
164
|
x = Vector([x], np.datetime64)
|
|
167
165
|
return _pull_int(x, function)[0]
|
|
168
|
-
x = util.sequencify(x)
|
|
169
166
|
assert isinstance(x, np.ndarray)
|
|
170
167
|
assert np.issubdtype(x.dtype, np.datetime64)
|
|
171
168
|
out = np.full_like(x, np.nan, float)
|
|
@@ -180,7 +177,6 @@ def _pull_str(x, function):
|
|
|
180
177
|
if util.is_scalar(x):
|
|
181
178
|
x = Vector([x], np.datetime64)
|
|
182
179
|
return _pull_str(x, function)[0]
|
|
183
|
-
x = util.sequencify(x)
|
|
184
180
|
assert isinstance(x, np.ndarray)
|
|
185
181
|
assert np.issubdtype(x.dtype, np.datetime64)
|
|
186
182
|
out = np.full_like(x, dtypes.string.na_object, object)
|
|
@@ -216,7 +212,6 @@ def replace(x, year=None, month=None, day=None, hour=None, minute=None, second=N
|
|
|
216
212
|
scalar_keys = [x for x in kwargs if util.is_scalar(kwargs[x])]
|
|
217
213
|
vector_keys = [x for x in kwargs if x not in scalar_keys]
|
|
218
214
|
# Like _pull_datetime, but no vectorized function.
|
|
219
|
-
x = util.sequencify(x)
|
|
220
215
|
assert isinstance(x, np.ndarray)
|
|
221
216
|
assert np.issubdtype(x.dtype, np.datetime64)
|
|
222
217
|
out = np.full_like(x, np.nan)
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
21
|
# THE SOFTWARE.
|
|
22
22
|
|
|
23
|
+
import warnings
|
|
24
|
+
|
|
23
25
|
from numpy.dtypes import StringDType
|
|
24
26
|
|
|
25
27
|
#: Instance of NumPy variable-width StringDType used
|
|
@@ -30,3 +32,7 @@ string = StringDType(na_object="")
|
|
|
30
32
|
# input, a distinction between NA and blank cannot usually be made.
|
|
31
33
|
# TODO: Consider changing this to something like ':NA:'.
|
|
32
34
|
# https://numpy.org/doc/stable/user/basics.strings.html#missing-data-support
|
|
35
|
+
|
|
36
|
+
# Ignore pointless warnings about using StringDType in numpy.savez.
|
|
37
|
+
_pattern = "Custom dtypes are saved as python objects using the pickle protocol"
|
|
38
|
+
warnings.filterwarnings("ignore", message=_pattern, category=UserWarning)
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
# Copyright (c) 2025 Osmo Salomaa
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in
|
|
13
|
+
# all copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
# THE SOFTWARE.
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
import re
|
|
25
|
+
|
|
26
|
+
from dataiter import dtypes
|
|
27
|
+
from dataiter import util
|
|
28
|
+
from dataiter import Vector
|
|
29
|
+
from numpy.dtypes import StringDType
|
|
30
|
+
|
|
31
|
+
def _prep(string, dtype, default):
|
|
32
|
+
assert isinstance(string, np.ndarray)
|
|
33
|
+
assert isinstance(string.dtype, StringDType)
|
|
34
|
+
out = np.full_like(string, default, dtype)
|
|
35
|
+
na = string == dtypes.string.na_object
|
|
36
|
+
return out, na
|
|
37
|
+
|
|
38
|
+
def findall(pattern, string, flags=0):
|
|
39
|
+
"""
|
|
40
|
+
Return a list of matches of `pattern` in `string`.
|
|
41
|
+
|
|
42
|
+
https://docs.python.org/3/library/re.html#re.findall
|
|
43
|
+
|
|
44
|
+
>>> x = di.Vector(["asdf", "1234"])
|
|
45
|
+
>>> regex.findall(r"[a-z]", x)
|
|
46
|
+
"""
|
|
47
|
+
if util.is_scalar(string):
|
|
48
|
+
return re.findall(pattern, string, flags=flags)
|
|
49
|
+
out, na = _prep(string, object, None)
|
|
50
|
+
for i in np.flatnonzero(~na):
|
|
51
|
+
out[i] = re.findall(pattern, string[i], flags=flags)
|
|
52
|
+
return Vector.fast(out, object)
|
|
53
|
+
|
|
54
|
+
def fullmatch(pattern, string, flags=0):
|
|
55
|
+
"""
|
|
56
|
+
Return a ``re.Match`` object or ``None``.
|
|
57
|
+
|
|
58
|
+
https://docs.python.org/3/library/re.html#re.fullmatch
|
|
59
|
+
|
|
60
|
+
>>> x = di.Vector(["asdf", "1234"])
|
|
61
|
+
>>> regex.fullmatch(r"[a-z]+", x)
|
|
62
|
+
"""
|
|
63
|
+
if util.is_scalar(string):
|
|
64
|
+
return re.fullmatch(pattern, string, flags=flags)
|
|
65
|
+
out, na = _prep(string, object, None)
|
|
66
|
+
for i in np.flatnonzero(~na):
|
|
67
|
+
out[i] = re.fullmatch(pattern, string[i], flags=flags)
|
|
68
|
+
return Vector.fast(out, object)
|
|
69
|
+
|
|
70
|
+
def match(pattern, string, flags=0):
|
|
71
|
+
"""
|
|
72
|
+
Return a ``re.Match`` object or ``None``.
|
|
73
|
+
|
|
74
|
+
https://docs.python.org/3/library/re.html#re.match
|
|
75
|
+
|
|
76
|
+
>>> x = di.Vector(["asdf", "1234"])
|
|
77
|
+
>>> regex.match(r"[a-z]", x)
|
|
78
|
+
"""
|
|
79
|
+
if util.is_scalar(string):
|
|
80
|
+
return re.match(pattern, string, flags=flags)
|
|
81
|
+
out, na = _prep(string, object, None)
|
|
82
|
+
for i in np.flatnonzero(~na):
|
|
83
|
+
out[i] = re.match(pattern, string[i], flags=flags)
|
|
84
|
+
return Vector.fast(out, object)
|
|
85
|
+
|
|
86
|
+
def search(pattern, string, flags=0):
|
|
87
|
+
"""
|
|
88
|
+
Return a ``re.Match`` object or ``None``.
|
|
89
|
+
|
|
90
|
+
https://docs.python.org/3/library/re.html#re.search
|
|
91
|
+
|
|
92
|
+
>>> x = di.Vector(["asdf", "1234"])
|
|
93
|
+
>>> regex.search(r"[a-z]", x)
|
|
94
|
+
"""
|
|
95
|
+
if util.is_scalar(string):
|
|
96
|
+
return re.search(pattern, string, flags=flags)
|
|
97
|
+
out, na = _prep(string, object, None)
|
|
98
|
+
for i in np.flatnonzero(~na):
|
|
99
|
+
out[i] = re.search(pattern, string[i], flags=flags)
|
|
100
|
+
return Vector.fast(out, object)
|
|
101
|
+
|
|
102
|
+
def split(pattern, string, maxsplit=0, flags=0):
|
|
103
|
+
"""
|
|
104
|
+
Return a list of `string` split by `pattern`.
|
|
105
|
+
|
|
106
|
+
https://docs.python.org/3/library/re.html#re.split
|
|
107
|
+
|
|
108
|
+
>>> x = di.Vector(["one two three", "four"])
|
|
109
|
+
>>> regex.split(r" +", x)
|
|
110
|
+
"""
|
|
111
|
+
if util.is_scalar(string):
|
|
112
|
+
return re.split(pattern, string, maxsplit=maxsplit, flags=flags)
|
|
113
|
+
out, na = _prep(string, object, None)
|
|
114
|
+
for i in np.flatnonzero(~na):
|
|
115
|
+
out[i] = re.split(pattern, string[i], maxsplit=maxsplit, flags=flags)
|
|
116
|
+
return Vector.fast(out, object)
|
|
117
|
+
|
|
118
|
+
def sub(pattern, repl, string, count=0, flags=0):
|
|
119
|
+
"""
|
|
120
|
+
Return `string` with instances of `pattern` replaced with `repl`.
|
|
121
|
+
|
|
122
|
+
https://docs.python.org/3/library/re.html#re.sub
|
|
123
|
+
|
|
124
|
+
>>> x = di.Vector(["great", "fantastic"])
|
|
125
|
+
>>> regex.sub(r"$", r"!", x)
|
|
126
|
+
"""
|
|
127
|
+
if util.is_scalar(string):
|
|
128
|
+
return re.sub(pattern, repl, string, count=count, flags=flags)
|
|
129
|
+
out, na = _prep(string, dtypes.string, dtypes.string.na_object)
|
|
130
|
+
for i in np.flatnonzero(~na):
|
|
131
|
+
out[i] = re.sub(pattern, repl, string[i], count=count, flags=flags)
|
|
132
|
+
return Vector.fast(out, str)
|
|
133
|
+
|
|
134
|
+
def subn(pattern, repl, string, count=0, flags=0):
|
|
135
|
+
"""
|
|
136
|
+
Return `string`, count of instances of `pattern` replaced with `repl`.
|
|
137
|
+
|
|
138
|
+
https://docs.python.org/3/library/re.html#re.subn
|
|
139
|
+
|
|
140
|
+
>>> x = di.Vector(["great", "fantastic"])
|
|
141
|
+
>>> regex.subn(r"$", r"!", x)
|
|
142
|
+
"""
|
|
143
|
+
if util.is_scalar(string):
|
|
144
|
+
return re.subn(pattern, repl, string, count=count, flags=flags)
|
|
145
|
+
out, na = _prep(string, object, None)
|
|
146
|
+
for i in np.flatnonzero(~na):
|
|
147
|
+
out[i] = re.subn(pattern, repl, string[i], count=count, flags=flags)
|
|
148
|
+
return Vector.fast(out, object)
|
|
@@ -31,9 +31,8 @@ from dataiter import test
|
|
|
31
31
|
from pathlib import Path
|
|
32
32
|
from unittest.mock import patch
|
|
33
33
|
|
|
34
|
-
#
|
|
35
|
-
|
|
36
|
-
IGNORE_NPZ_PICKLE_WARNING = "ignore:Custom dtypes are saved as python objects using the pickle protocol."
|
|
34
|
+
# See dataiter/dtypes.py for the equivalent non-test ignore.
|
|
35
|
+
IGNORE_NPZ_PICKLE_WARNING = "ignore:Custom dtypes are saved as python objects using the pickle protocol"
|
|
37
36
|
|
|
38
37
|
class TestDataFrameColumn:
|
|
39
38
|
|
|
@@ -448,6 +447,70 @@ class TestDataFrame:
|
|
|
448
447
|
assert data.make.is_object()
|
|
449
448
|
assert data.model.is_object()
|
|
450
449
|
|
|
450
|
+
def test_read_csv_missing_blank(self):
|
|
451
|
+
text = """
|
|
452
|
+
int,float,string,bool,date
|
|
453
|
+
,2.5,"test",TRUE,2025-01-01
|
|
454
|
+
1,,"test",TRUE,2025-01-01
|
|
455
|
+
1,2.5,"",TRUE,2025-01-01
|
|
456
|
+
1,2.5,"test",,2025-01-01
|
|
457
|
+
1,2.5,"test",TRUE,
|
|
458
|
+
""".strip()
|
|
459
|
+
handle, path = tempfile.mkstemp(".csv")
|
|
460
|
+
Path(path).write_text(text, "utf-8")
|
|
461
|
+
data = DataFrame.read_csv(path)
|
|
462
|
+
for i, column in enumerate(data.columns):
|
|
463
|
+
assert column.is_na().sum() == 1
|
|
464
|
+
assert column.is_na()[i]
|
|
465
|
+
|
|
466
|
+
def test_read_csv_missing_excel(self):
|
|
467
|
+
text = """
|
|
468
|
+
int,float,string,bool,date
|
|
469
|
+
#N/A,2.5,"test",TRUE,2025-01-01
|
|
470
|
+
1,#N/A,"test",TRUE,2025-01-01
|
|
471
|
+
1,2.5,"",TRUE,2025-01-01
|
|
472
|
+
1,2.5,"test",#N/A,2025-01-01
|
|
473
|
+
1,2.5,"test",TRUE,#N/A
|
|
474
|
+
""".strip()
|
|
475
|
+
handle, path = tempfile.mkstemp(".csv")
|
|
476
|
+
Path(path).write_text(text, "utf-8")
|
|
477
|
+
data = DataFrame.read_csv(path)
|
|
478
|
+
for i, column in enumerate(data.columns):
|
|
479
|
+
assert column.is_na().sum() == 1
|
|
480
|
+
assert column.is_na()[i]
|
|
481
|
+
|
|
482
|
+
def test_read_csv_missing_na(self):
|
|
483
|
+
text = """
|
|
484
|
+
int,float,string,bool,date
|
|
485
|
+
NA,2.5,"test",TRUE,2025-01-01
|
|
486
|
+
1,NA,"test",TRUE,2025-01-01
|
|
487
|
+
1,2.5,"",TRUE,2025-01-01
|
|
488
|
+
1,2.5,"test",NA,2025-01-01
|
|
489
|
+
1,2.5,"test",TRUE,NA
|
|
490
|
+
""".strip()
|
|
491
|
+
handle, path = tempfile.mkstemp(".csv")
|
|
492
|
+
Path(path).write_text(text, "utf-8")
|
|
493
|
+
data = DataFrame.read_csv(path)
|
|
494
|
+
for i, column in enumerate(data.columns):
|
|
495
|
+
assert column.is_na().sum() == 1
|
|
496
|
+
assert column.is_na()[i]
|
|
497
|
+
|
|
498
|
+
def test_read_csv_missing_null(self):
|
|
499
|
+
text = """
|
|
500
|
+
int,float,string,bool,date
|
|
501
|
+
NULL,2.5,"test",TRUE,2025-01-01
|
|
502
|
+
1,NULL,"test",TRUE,2025-01-01
|
|
503
|
+
1,2.5,"",TRUE,2025-01-01
|
|
504
|
+
1,2.5,"test",NULL,2025-01-01
|
|
505
|
+
1,2.5,"test",TRUE,NULL
|
|
506
|
+
""".strip()
|
|
507
|
+
handle, path = tempfile.mkstemp(".csv")
|
|
508
|
+
Path(path).write_text(text, "utf-8")
|
|
509
|
+
data = DataFrame.read_csv(path)
|
|
510
|
+
for i, column in enumerate(data.columns):
|
|
511
|
+
assert column.is_na().sum() == 1
|
|
512
|
+
assert column.is_na()[i]
|
|
513
|
+
|
|
451
514
|
def test_read_json(self):
|
|
452
515
|
path = str(test.get_data_path("downloads.json"))
|
|
453
516
|
data = DataFrame.read_json(path)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
# Copyright (c) 2025 Osmo Salomaa
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in
|
|
13
|
+
# all copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
# THE SOFTWARE.
|
|
22
|
+
|
|
23
|
+
import re
|
|
24
|
+
|
|
25
|
+
from dataiter import regex
|
|
26
|
+
from dataiter import Vector
|
|
27
|
+
|
|
28
|
+
class TestRegex:
|
|
29
|
+
|
|
30
|
+
def test_findall(self):
|
|
31
|
+
pattern = r"[a-z]"
|
|
32
|
+
string = Vector(["asdf", "1234", ""])
|
|
33
|
+
result = regex.findall(pattern, string)
|
|
34
|
+
expected = [["a", "s", "d", "f"], [], None]
|
|
35
|
+
assert result.tolist() == expected
|
|
36
|
+
assert regex.findall(pattern, string[0]) == result[0]
|
|
37
|
+
|
|
38
|
+
def test_fullmatch(self):
|
|
39
|
+
pattern = r"[a-z]+"
|
|
40
|
+
string = Vector(["asdf", "1234", ""])
|
|
41
|
+
result = regex.fullmatch(pattern, string)
|
|
42
|
+
assert isinstance(result[0], re.Match)
|
|
43
|
+
assert result[1] is None
|
|
44
|
+
assert result[2] is None
|
|
45
|
+
match = regex.fullmatch(pattern, string[0])
|
|
46
|
+
assert isinstance(match, re.Match)
|
|
47
|
+
|
|
48
|
+
def test_match(self):
|
|
49
|
+
pattern = r"[a-z]"
|
|
50
|
+
string = Vector(["asdf", "1234", ""])
|
|
51
|
+
result = regex.match(pattern, string)
|
|
52
|
+
assert isinstance(result[0], re.Match)
|
|
53
|
+
assert result[1] is None
|
|
54
|
+
assert result[2] is None
|
|
55
|
+
match = regex.match(pattern, string[0])
|
|
56
|
+
assert isinstance(match, re.Match)
|
|
57
|
+
|
|
58
|
+
def test_search(self):
|
|
59
|
+
pattern = r"[a-z]"
|
|
60
|
+
string = Vector(["asdf", "1234", ""])
|
|
61
|
+
result = regex.search(pattern, string)
|
|
62
|
+
assert isinstance(result[0], re.Match)
|
|
63
|
+
assert result[1] is None
|
|
64
|
+
assert result[2] is None
|
|
65
|
+
match = regex.search(pattern, string[0])
|
|
66
|
+
assert isinstance(match, re.Match)
|
|
67
|
+
|
|
68
|
+
def test_split(self):
|
|
69
|
+
pattern = r" +"
|
|
70
|
+
string = Vector(["one two three", "four", ""])
|
|
71
|
+
result = regex.split(pattern, string)
|
|
72
|
+
expected = [["one", "two", "three"], ["four"], None]
|
|
73
|
+
assert result.tolist() == expected
|
|
74
|
+
assert regex.split(pattern, string[0]) == result[0]
|
|
75
|
+
|
|
76
|
+
def test_sub(self):
|
|
77
|
+
pattern = r"$"
|
|
78
|
+
repl = "!"
|
|
79
|
+
string = Vector(["great", "fantastic", ""])
|
|
80
|
+
result = regex.sub(pattern, repl, string)
|
|
81
|
+
expected = ["great!", "fantastic!", None]
|
|
82
|
+
assert result.tolist() == expected
|
|
83
|
+
assert regex.sub(pattern, repl, string[0]) == result[0]
|
|
84
|
+
|
|
85
|
+
def test_subn(self):
|
|
86
|
+
pattern = r"$"
|
|
87
|
+
repl = "!"
|
|
88
|
+
string = Vector(["great", "fantastic", ""])
|
|
89
|
+
result = regex.subn(pattern, repl, string)
|
|
90
|
+
expected = [("great!", 1), ("fantastic!", 1), None]
|
|
91
|
+
assert result.tolist() == expected
|
|
92
|
+
assert regex.subn(pattern, repl, string[0]) == result[0]
|
|
@@ -234,6 +234,12 @@ class TestVector:
|
|
|
234
234
|
b = a.drop_na()
|
|
235
235
|
assert b.tolist() == [1, 2, 3]
|
|
236
236
|
|
|
237
|
+
def test_dt(self):
|
|
238
|
+
a = Vector(["2025-01-11"], np.datetime64)
|
|
239
|
+
assert a.dt.year().tolist() == [2025]
|
|
240
|
+
assert a.dt.month().tolist() == [1]
|
|
241
|
+
assert a.dt.day().tolist() == [11]
|
|
242
|
+
|
|
237
243
|
def test_equal_boolean(self):
|
|
238
244
|
a = Vector([True, False])
|
|
239
245
|
b = Vector([True, False])
|
|
@@ -463,6 +469,10 @@ class TestVector:
|
|
|
463
469
|
assert a.rank(method="min").equal(a.rank(method="max"))
|
|
464
470
|
assert a.rank(method="min").equal(a.rank(method="ordinal"))
|
|
465
471
|
|
|
472
|
+
def test_re(self):
|
|
473
|
+
a = Vector(["great", "fantastic"])
|
|
474
|
+
assert a.re.sub(r"$", r"!").tolist() == ["great!", "fantastic!"]
|
|
475
|
+
|
|
466
476
|
def test_replace_na(self):
|
|
467
477
|
a = Vector([1, 2, 3, None])
|
|
468
478
|
assert a.replace_na(0).tolist() == [1, 2, 3, 0]
|
|
@@ -492,6 +502,12 @@ class TestVector:
|
|
|
492
502
|
assert a.sort(dir=1).tolist() == [1, 2, 3, 4, 5, None, None]
|
|
493
503
|
assert a.sort(dir=-1).tolist() == [5, 4, 3, 2, 1, None, None]
|
|
494
504
|
|
|
505
|
+
def test_str(self):
|
|
506
|
+
a = Vector(["asdf", "1234"])
|
|
507
|
+
assert a.str.isalpha().tolist() == [True, False]
|
|
508
|
+
assert a.str.str_len().tolist() == [4, 4]
|
|
509
|
+
assert a.str.upper().tolist() == ["ASDF", "1234"]
|
|
510
|
+
|
|
495
511
|
def test_tail(self):
|
|
496
512
|
a = Vector([1, 2, 3, 4, 5])
|
|
497
513
|
assert a.tail(3).tolist() == [3, 4, 5]
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
import dataiter
|
|
24
24
|
import datetime
|
|
25
|
+
import functools
|
|
25
26
|
import numpy as np
|
|
26
27
|
import sys
|
|
27
28
|
|
|
@@ -35,6 +36,103 @@ TYPE_CONVERSIONS = {
|
|
|
35
36
|
datetime.datetime: "datetime64[us]",
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
def as_vector(function):
|
|
40
|
+
@functools.wraps(function)
|
|
41
|
+
def wrapper(*args, **kwargs):
|
|
42
|
+
array = function(*args, **kwargs)
|
|
43
|
+
return array.view(Vector)
|
|
44
|
+
return wrapper
|
|
45
|
+
|
|
46
|
+
def not_implemented(*args, **kwargs):
|
|
47
|
+
raise NotImplementedError
|
|
48
|
+
|
|
49
|
+
class DtProxy:
|
|
50
|
+
def __init__(self, vector):
|
|
51
|
+
from dataiter import dt
|
|
52
|
+
wrap = lambda f: functools.partial(f, vector)
|
|
53
|
+
self.day = wrap(dt.day)
|
|
54
|
+
self.from_string = wrap(dt.from_string)
|
|
55
|
+
self.hour = wrap(dt.hour)
|
|
56
|
+
self.isoweek = wrap(dt.isoweek)
|
|
57
|
+
self.isoweekday = wrap(dt.isoweekday)
|
|
58
|
+
self.microsecond = wrap(dt.microsecond)
|
|
59
|
+
self.minute = wrap(dt.minute)
|
|
60
|
+
self.month = wrap(dt.month)
|
|
61
|
+
self.new = wrap(dt.new)
|
|
62
|
+
self.quarter = wrap(dt.quarter)
|
|
63
|
+
self.replace = wrap(dt.replace)
|
|
64
|
+
self.second = wrap(dt.second)
|
|
65
|
+
self.to_string = wrap(dt.to_string)
|
|
66
|
+
self.weekday = wrap(dt.weekday)
|
|
67
|
+
self.year = wrap(dt.year)
|
|
68
|
+
|
|
69
|
+
class ReProxy:
|
|
70
|
+
def __init__(self, vector):
|
|
71
|
+
from dataiter import regex
|
|
72
|
+
wrap = lambda f: functools.partial(f, string=vector)
|
|
73
|
+
self.findall = wrap(regex.findall)
|
|
74
|
+
self.fullmatch = wrap(regex.fullmatch)
|
|
75
|
+
self.match = wrap(regex.match)
|
|
76
|
+
self.search = wrap(regex.search)
|
|
77
|
+
self.split = wrap(regex.split)
|
|
78
|
+
self.sub = wrap(regex.sub)
|
|
79
|
+
self.subn = wrap(regex.subn)
|
|
80
|
+
|
|
81
|
+
class StrProxy:
|
|
82
|
+
def __init__(self, vector):
|
|
83
|
+
wrap = lambda name: as_vector(functools.partial(
|
|
84
|
+
getattr(np.strings, name, not_implemented),
|
|
85
|
+
vector))
|
|
86
|
+
|
|
87
|
+
# https://numpy.org/doc/stable/reference/routines.strings.html
|
|
88
|
+
self.add = wrap("add")
|
|
89
|
+
self.capitalize = wrap("capitalize")
|
|
90
|
+
self.center = wrap("center")
|
|
91
|
+
self.count = wrap("count")
|
|
92
|
+
self.decode = wrap("decode")
|
|
93
|
+
self.encode = wrap("encode")
|
|
94
|
+
self.endswith = wrap("endswith")
|
|
95
|
+
self.equal = wrap("equal")
|
|
96
|
+
self.expandtabs = wrap("expandtabs")
|
|
97
|
+
self.find = wrap("find")
|
|
98
|
+
self.greater = wrap("greater")
|
|
99
|
+
self.greater_equal = wrap("greater_equal")
|
|
100
|
+
self.index = wrap("index")
|
|
101
|
+
self.isalnum = wrap("isalnum")
|
|
102
|
+
self.isalpha = wrap("isalpha")
|
|
103
|
+
self.isdecimal = wrap("isdecimal")
|
|
104
|
+
self.isdigit = wrap("isdigit")
|
|
105
|
+
self.islower = wrap("islower")
|
|
106
|
+
self.isnumeric = wrap("isnumeric")
|
|
107
|
+
self.isspace = wrap("isspace")
|
|
108
|
+
self.istitle = wrap("istitle")
|
|
109
|
+
self.isupper = wrap("isupper")
|
|
110
|
+
self.less = wrap("less")
|
|
111
|
+
self.less_equal = wrap("less_equal")
|
|
112
|
+
self.ljust = wrap("ljust")
|
|
113
|
+
self.lower = wrap("lower")
|
|
114
|
+
self.lstrip = wrap("lstrip")
|
|
115
|
+
self.mod = wrap("mod")
|
|
116
|
+
self.multiply = wrap("multiply")
|
|
117
|
+
self.not_equal = wrap("not_equal")
|
|
118
|
+
self.replace = wrap("replace")
|
|
119
|
+
self.rfind = wrap("rfind")
|
|
120
|
+
self.rindex = wrap("rindex")
|
|
121
|
+
self.rjust = wrap("rjust")
|
|
122
|
+
self.rstrip = wrap("rstrip")
|
|
123
|
+
self.startswith = wrap("startswith")
|
|
124
|
+
self.str_len = wrap("str_len")
|
|
125
|
+
self.strip = wrap("strip")
|
|
126
|
+
self.swapcase = wrap("swapcase")
|
|
127
|
+
self.title = wrap("title")
|
|
128
|
+
self.translate = wrap("translate")
|
|
129
|
+
self.upper = wrap("upper")
|
|
130
|
+
self.zfill = wrap("zfill")
|
|
131
|
+
|
|
132
|
+
# Skip functions that have a difficult return value.
|
|
133
|
+
# self.partition = wrap("partition")
|
|
134
|
+
# self.rpartition = wrap("rpartition")
|
|
135
|
+
|
|
38
136
|
class Vector(np.ndarray):
|
|
39
137
|
|
|
40
138
|
"""
|
|
@@ -76,6 +174,8 @@ class Vector(np.ndarray):
|
|
|
76
174
|
|
|
77
175
|
>>> di.Vector([1, 2, 3], int)
|
|
78
176
|
"""
|
|
177
|
+
# Note that when we call array.view(Vector) for a numpy.ndarray,
|
|
178
|
+
# this will not be called and no attributes added here will exist!
|
|
79
179
|
self._check_dimensions()
|
|
80
180
|
|
|
81
181
|
def __array_wrap__(self, array, context=None, return_scalar=False):
|
|
@@ -108,8 +208,7 @@ class Vector(np.ndarray):
|
|
|
108
208
|
>>> vector.as_bytes()
|
|
109
209
|
"""
|
|
110
210
|
if self.is_string():
|
|
111
|
-
|
|
112
|
-
return array.view(self.__class__)
|
|
211
|
+
return self.str.encode("utf-8")
|
|
113
212
|
return self.astype(bytes)
|
|
114
213
|
|
|
115
214
|
def as_date(self):
|
|
@@ -192,6 +291,20 @@ class Vector(np.ndarray):
|
|
|
192
291
|
"""
|
|
193
292
|
return self[~self.is_na()].copy()
|
|
194
293
|
|
|
294
|
+
@property
|
|
295
|
+
def dt(self) -> DtProxy:
|
|
296
|
+
"""
|
|
297
|
+
Proxy object for calling :mod:`dataiter.dt` functions.
|
|
298
|
+
|
|
299
|
+
>>> x = di.Vector(["2025-01-11"], np.datetime64)
|
|
300
|
+
>>> x.dt.year()
|
|
301
|
+
>>> x.dt.month()
|
|
302
|
+
>>> x.dt.day()
|
|
303
|
+
"""
|
|
304
|
+
if not hasattr(self, "_dt"):
|
|
305
|
+
self._dt = DtProxy(self)
|
|
306
|
+
return self._dt
|
|
307
|
+
|
|
195
308
|
@property
|
|
196
309
|
def dtype_label(self):
|
|
197
310
|
"""
|
|
@@ -457,7 +570,7 @@ class Vector(np.ndarray):
|
|
|
457
570
|
return array
|
|
458
571
|
|
|
459
572
|
def _optimize_for_argsort(self):
|
|
460
|
-
if self.is_string() and (n :=
|
|
573
|
+
if self.is_string() and (n := self.str.str_len().max()) < 50:
|
|
461
574
|
# XXX: We get a huge speed boost often by converting
|
|
462
575
|
# to the old-style fixed-width strings! This is probably
|
|
463
576
|
# temporary and can be removed once StringDType has matured.
|
|
@@ -525,6 +638,18 @@ class Vector(np.ndarray):
|
|
|
525
638
|
return out.view(self.__class__)
|
|
526
639
|
raise ValueError(f"Unexpected method: {method!r}")
|
|
527
640
|
|
|
641
|
+
@property
|
|
642
|
+
def re(self) -> ReProxy:
|
|
643
|
+
"""
|
|
644
|
+
Proxy object for calling :mod:`dataiter.regex` functions.
|
|
645
|
+
|
|
646
|
+
>>> x = di.Vector(["great", "fantastic"])
|
|
647
|
+
>>> x.re.sub(r"$", r"!")
|
|
648
|
+
"""
|
|
649
|
+
if not hasattr(self, "_re"):
|
|
650
|
+
self._re = ReProxy(self)
|
|
651
|
+
return self._re
|
|
652
|
+
|
|
528
653
|
def replace_na(self, value):
|
|
529
654
|
"""
|
|
530
655
|
Return vector with missing values replaced with `value`.
|
|
@@ -623,6 +748,22 @@ class Vector(np.ndarray):
|
|
|
623
748
|
# Usually causes dtype to be object!
|
|
624
749
|
return None
|
|
625
750
|
|
|
751
|
+
@property
|
|
752
|
+
def str(self) -> StrProxy:
|
|
753
|
+
"""
|
|
754
|
+
Proxy object for calling ``numpy.strings`` functions.
|
|
755
|
+
|
|
756
|
+
https://numpy.org/doc/stable/reference/routines.strings.html
|
|
757
|
+
|
|
758
|
+
>>> x = di.Vector(["asdf", "1234"])
|
|
759
|
+
>>> x.str.isalpha()
|
|
760
|
+
>>> x.str.str_len()
|
|
761
|
+
>>> x.str.upper()
|
|
762
|
+
"""
|
|
763
|
+
if not hasattr(self, "_str"):
|
|
764
|
+
self._str = StrProxy(self)
|
|
765
|
+
return self._str
|
|
766
|
+
|
|
626
767
|
def tail(self, n=None):
|
|
627
768
|
"""
|
|
628
769
|
Return the last `n` elements.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["hatchling==1.21.1"]
|
|
2
|
+
requires = ["build==1.2.2.post1", "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.9.0"
|
|
12
12
|
authors = [{ name = "Osmo Salomaa", email = "otsaloma@iki.fi" }]
|
|
13
|
-
dependencies = ["attd>=0.3", "numpy>=2.0,<3.0", "
|
|
13
|
+
dependencies = ["attd>=0.3", "numpy>=2.0,<3.0", "pyarrow>=2.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|