iker-python-common 1.0.1__tar.gz → 1.0.2__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.
- iker_python_common-1.0.2/PKG-INFO +22 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/pyproject.toml +7 -24
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/src/iker/common/utils/dbutils.py +4 -46
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/src/iker/common/utils/logger.py +0 -3
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/src/iker/common/utils/s3utils.py +5 -4
- iker_python_common-1.0.2/src/iker/common/utils/sequtils.py +355 -0
- iker_python_common-1.0.2/src/iker/common/utils/span.py +221 -0
- iker_python_common-1.0.2/src/iker_python_common.egg-info/PKG-INFO +22 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/src/iker_python_common.egg-info/SOURCES.txt +2 -4
- iker_python_common-1.0.2/src/iker_python_common.egg-info/requires.txt +16 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/test/iker_tests/common/utils/dbutils_test.py +0 -3
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/test/iker_tests/common/utils/s3utils_test.py +7 -7
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/test/iker_tests/common/utils/sequtils_test.py +148 -270
- iker_python_common-1.0.2/test/iker_tests/common/utils/span_test.py +298 -0
- iker_python_common-1.0.1/PKG-INFO +0 -39
- iker_python_common-1.0.1/src/iker/common/core/exceptions.py +0 -64
- iker_python_common-1.0.1/src/iker/common/utils/__init__.py +0 -0
- iker_python_common-1.0.1/src/iker/common/utils/sequtils.py +0 -394
- iker_python_common-1.0.1/src/iker/common/utils/stream.py +0 -188
- iker_python_common-1.0.1/src/iker_python_common.egg-info/PKG-INFO +0 -39
- iker_python_common-1.0.1/src/iker_python_common.egg-info/requires.txt +0 -33
- iker_python_common-1.0.1/test/iker_tests/common/core/exceptions_test.py +0 -20
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/.editorconfig +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/.github/workflows/pr.yml +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/.github/workflows/push.yml +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/.gitignore +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/MANIFEST.in +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/README.md +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/VERSION +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/resources/unittest/shutils/dir.baz/file.bar.baz +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/resources/unittest/shutils/dir.baz/file.foo.bar +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/resources/unittest/shutils/dir.baz/file.foo.baz +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/resources/unittest/shutils/dir.foo/dir.foo.bar/dir.foo.bar.baz/file.foo.bar.baz +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/resources/unittest/shutils/dir.foo/dir.foo.bar/file.bar.baz +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/resources/unittest/shutils/dir.foo/dir.foo.bar/file.foo.bar +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/resources/unittest/shutils/dir.foo/dir.foo.bar/file.foo.baz +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/resources/unittest/shutils/dir.foo/file.bar +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/resources/unittest/shutils/dir.foo/file.baz +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/resources/unittest/shutils/dir.foo/file.foo +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/setup.cfg +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/setup.py +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/src/iker/common/__init__.py +0 -0
- {iker_python_common-1.0.1/src/iker/common/core → iker_python_common-1.0.2/src/iker/common/utils}/__init__.py +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/src/iker/common/utils/config.py +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/src/iker/common/utils/dockerutils.py +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/src/iker/common/utils/dtutils.py +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/src/iker/common/utils/funcutils.py +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/src/iker/common/utils/numutils.py +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/src/iker/common/utils/randutils.py +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/src/iker/common/utils/retry.py +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/src/iker/common/utils/shutils.py +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/src/iker/common/utils/strutils.py +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/src/iker/common/utils/testutils.py +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/src/iker_python_common.egg-info/dependency_links.txt +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/src/iker_python_common.egg-info/not-zip-safe +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/src/iker_python_common.egg-info/top_level.txt +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/test/iker_tests/__init__.py +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/test/iker_tests/common/utils/config_test.py +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/test/iker_tests/common/utils/dockerutils_test.py +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/test/iker_tests/common/utils/dtutils_test.py +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/test/iker_tests/common/utils/funcutils_test.py +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/test/iker_tests/common/utils/logger_test.py +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/test/iker_tests/common/utils/numutils_test.py +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/test/iker_tests/common/utils/randutils_test.py +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/test/iker_tests/common/utils/retry_test.py +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/test/iker_tests/common/utils/shutils_test.py +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/test/iker_tests/common/utils/strutils_test.py +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/test/iker_tests/common/utils/testutils_test.py +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/test/iker_tests/docker_fixtures.py +0 -0
- {iker_python_common-1.0.1 → iker_python_common-1.0.2}/test/iker_tests/iker_test.py +0 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: iker-python-common
|
|
3
|
+
Version: 1.0.2
|
|
4
|
+
Classifier: Programming Language :: Python :: 3
|
|
5
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
6
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
7
|
+
Requires-Python: <3.13,>=3.11
|
|
8
|
+
Requires-Dist: boto3~=1.35
|
|
9
|
+
Requires-Dist: docker~=7.1
|
|
10
|
+
Requires-Dist: numpy~=1.26
|
|
11
|
+
Requires-Dist: psycopg~=3.2
|
|
12
|
+
Requires-Dist: pymysql~=1.1
|
|
13
|
+
Requires-Dist: sqlalchemy~=2.0
|
|
14
|
+
Provides-Extra: test
|
|
15
|
+
Requires-Dist: ddt~=1.7; extra == "test"
|
|
16
|
+
Requires-Dist: moto[all,ec2,s3]~=5.0; extra == "test"
|
|
17
|
+
Requires-Dist: pytest-cov~=5.0; extra == "test"
|
|
18
|
+
Requires-Dist: pytest-mock~=3.14; extra == "test"
|
|
19
|
+
Requires-Dist: pytest-mysql~=3.0; extra == "test"
|
|
20
|
+
Requires-Dist: pytest-order~=1.3; extra == "test"
|
|
21
|
+
Requires-Dist: pytest-postgresql~=6.1; extra == "test"
|
|
22
|
+
Requires-Dist: pytest~=8.3; extra == "test"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[build-system]
|
|
2
2
|
requires = [
|
|
3
|
-
"setuptools~=
|
|
3
|
+
"setuptools~=75.0",
|
|
4
4
|
"setuptools-scm~=8.0",
|
|
5
5
|
"iker-python-setup~=1.0",
|
|
6
6
|
]
|
|
@@ -16,40 +16,23 @@ classifiers = [
|
|
|
16
16
|
"Programming Language :: Python :: 3.12",
|
|
17
17
|
]
|
|
18
18
|
dependencies = [
|
|
19
|
-
"boto3~=1.
|
|
20
|
-
"brotli~=1.0",
|
|
19
|
+
"boto3~=1.35",
|
|
21
20
|
"docker~=7.1",
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"llvmlite~=0.43",
|
|
25
|
-
"lxml~=4.9",
|
|
26
|
-
"munch~=4.0",
|
|
27
|
-
"numpy~=2.1",
|
|
28
|
-
"pandas~=2.2",
|
|
29
|
-
"psycopg2-binary~=2.9",
|
|
30
|
-
"pygeodesy~=23.12",
|
|
21
|
+
"numpy~=1.26",
|
|
22
|
+
"psycopg~=3.2",
|
|
31
23
|
"pymysql~=1.1",
|
|
32
|
-
"pyparsing~=3.1",
|
|
33
|
-
"pyquaternion~=0.9",
|
|
34
|
-
"pysocks~=1.7",
|
|
35
|
-
"python-dateutil~=2.8",
|
|
36
|
-
"pyyaml~=6.0",
|
|
37
|
-
"requests~=2.31",
|
|
38
|
-
"scipy~=1.14",
|
|
39
|
-
"shapely~=2.0",
|
|
40
24
|
"sqlalchemy~=2.0",
|
|
41
|
-
"ujson~=5.10",
|
|
42
25
|
]
|
|
43
26
|
|
|
44
27
|
[project.optional-dependencies]
|
|
45
28
|
test = [
|
|
46
29
|
"ddt~=1.7",
|
|
47
|
-
"moto[ec2,s3,all]~=
|
|
30
|
+
"moto[ec2,s3,all]~=5.0",
|
|
48
31
|
"pytest-cov~=5.0",
|
|
49
32
|
"pytest-mock~=3.14",
|
|
50
33
|
"pytest-mysql~=3.0",
|
|
51
|
-
"pytest-order~=1.
|
|
52
|
-
"pytest-postgresql~=6.
|
|
34
|
+
"pytest-order~=1.3",
|
|
35
|
+
"pytest-postgresql~=6.1",
|
|
53
36
|
"pytest~=8.3",
|
|
54
37
|
]
|
|
55
38
|
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import contextlib
|
|
2
2
|
import dataclasses
|
|
3
|
-
import datetime
|
|
4
3
|
import urllib.parse
|
|
5
4
|
from typing import Any, ContextManager, Type
|
|
6
5
|
|
|
7
|
-
import
|
|
8
|
-
import
|
|
6
|
+
import psycopg
|
|
7
|
+
import pymysql
|
|
9
8
|
import sqlalchemy
|
|
10
9
|
import sqlalchemy.ext.compiler
|
|
11
10
|
import sqlalchemy.orm
|
|
12
11
|
|
|
13
|
-
from iker.common.utils.funcutils import singleton
|
|
14
12
|
from iker.common.utils.sequtils import head_or_none
|
|
15
13
|
from iker.common.utils.strutils import is_blank
|
|
16
14
|
|
|
@@ -18,12 +16,6 @@ __all__ = [
|
|
|
18
16
|
"DBAdapter",
|
|
19
17
|
"orm_to_dict",
|
|
20
18
|
"orm_clone",
|
|
21
|
-
"to_pg_date",
|
|
22
|
-
"to_pg_time",
|
|
23
|
-
"to_pg_ts",
|
|
24
|
-
"to_pg_ts",
|
|
25
|
-
"pg_date_max",
|
|
26
|
-
"pg_ts_max",
|
|
27
19
|
"mysql_insert_ignore",
|
|
28
20
|
"postgresql_insert_on_conflict_do_nothing",
|
|
29
21
|
]
|
|
@@ -35,8 +27,8 @@ class DBAdapter(object):
|
|
|
35
27
|
"""
|
|
36
28
|
|
|
37
29
|
class Drivers:
|
|
38
|
-
Mysql = "mysql+
|
|
39
|
-
Postgresql = "postgresql+
|
|
30
|
+
Mysql = f"mysql+{pymysql.__name__}"
|
|
31
|
+
Postgresql = f"postgresql+{psycopg.__name__}"
|
|
40
32
|
|
|
41
33
|
def __init__(
|
|
42
34
|
self,
|
|
@@ -145,40 +137,6 @@ def orm_clone(orm: sqlalchemy.orm.DeclarativeBase, exclude: set[str] = None, no_
|
|
|
145
137
|
return new_orm
|
|
146
138
|
|
|
147
139
|
|
|
148
|
-
def to_pg_date(dt: datetime.datetime | datetime.date | int | float):
|
|
149
|
-
if isinstance(dt, (datetime.datetime, datetime.date)):
|
|
150
|
-
return psycopg2.extensions.DateFromPy(dt)
|
|
151
|
-
elif isinstance(dt, (int, float)):
|
|
152
|
-
return psycopg2.DateFromTicks(dt)
|
|
153
|
-
raise TypeError("should be one of 'datetime.datetime', 'datetime.date', 'int', 'float'")
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
def to_pg_time(dt: datetime.time | int | float):
|
|
157
|
-
if isinstance(dt, datetime.time):
|
|
158
|
-
return psycopg2.extensions.TimeFromPy(dt)
|
|
159
|
-
elif isinstance(dt, (int, float)):
|
|
160
|
-
return psycopg2.TimeFromTicks(dt)
|
|
161
|
-
raise TypeError("should be one of 'datetime.time', 'int', 'float'")
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
def to_pg_ts(dt: datetime.datetime | datetime.date | int | float):
|
|
165
|
-
if isinstance(dt, (datetime.datetime, datetime.date)):
|
|
166
|
-
return psycopg2.extensions.TimestampFromPy(dt)
|
|
167
|
-
elif isinstance(dt, (int, float)):
|
|
168
|
-
return psycopg2.TimestampFromTicks(dt)
|
|
169
|
-
raise TypeError("should be one of 'datetime.datetime', 'datetime.date', 'int', 'float'")
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
@singleton
|
|
173
|
-
def pg_date_max():
|
|
174
|
-
return psycopg2.Date(9999, 12, 31)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
@singleton
|
|
178
|
-
def pg_ts_max():
|
|
179
|
-
return psycopg2.Timestamp(9999, 12, 31, 23, 59, 59.999, tzinfo=datetime.timezone.utc)
|
|
180
|
-
|
|
181
|
-
|
|
182
140
|
def mysql_insert_ignore(enabled: bool = True):
|
|
183
141
|
@sqlalchemy.ext.compiler.compiles(sqlalchemy.sql.Insert, "mysql")
|
|
184
142
|
def dispatch(insert: sqlalchemy.sql.Insert, compiler: sqlalchemy.sql.compiler.SQLCompiler, **kwargs) -> str:
|
|
@@ -49,10 +49,11 @@ def s3_make_client(
|
|
|
49
49
|
:param endpoint_url: AWS service endpoint url
|
|
50
50
|
:return: context manager which wraps the AWS S3 client instance
|
|
51
51
|
"""
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
client = boto3.client("s3",
|
|
53
|
+
region_name=trim_to_none(region_name),
|
|
54
|
+
endpoint_url=trim_to_none(endpoint_url),
|
|
55
|
+
aws_access_key_id=trim_to_none(access_key_id),
|
|
56
|
+
aws_secret_access_key=trim_to_none(secret_access_key))
|
|
56
57
|
return contextlib.closing(client)
|
|
57
58
|
|
|
58
59
|
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import itertools
|
|
5
|
+
from typing import Callable, Generator, Generic, Iterable, Self, Sequence, TypeVar
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"head",
|
|
9
|
+
"head_or_none",
|
|
10
|
+
"last",
|
|
11
|
+
"last_or_none",
|
|
12
|
+
"tail",
|
|
13
|
+
"init",
|
|
14
|
+
"grouped",
|
|
15
|
+
"deduped",
|
|
16
|
+
"batch_yield",
|
|
17
|
+
"chunk",
|
|
18
|
+
"chunk_between",
|
|
19
|
+
"chunk_with_key",
|
|
20
|
+
"merge_chunks",
|
|
21
|
+
"Seq",
|
|
22
|
+
"seq",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
T = TypeVar("T")
|
|
26
|
+
K = TypeVar("K")
|
|
27
|
+
U = TypeVar("U")
|
|
28
|
+
V = TypeVar("V")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# See Haskell's list operations head, tail, init, and last
|
|
32
|
+
# which is also provided in Scala list operations
|
|
33
|
+
|
|
34
|
+
def head(ms: Sequence[T]) -> T:
|
|
35
|
+
return ms[0]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def head_or_none(ms: Sequence[T]) -> T | None:
|
|
39
|
+
if len(ms) > 0:
|
|
40
|
+
return ms[0]
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def last(ms: Sequence[T]) -> T:
|
|
45
|
+
return ms[-1]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def last_or_none(ms: Sequence[T]) -> T | None:
|
|
49
|
+
if len(ms) > 0:
|
|
50
|
+
return ms[-1]
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def tail(ms: Sequence[T]) -> Sequence[T]:
|
|
55
|
+
return ms[1:]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def init(ms: Sequence[T]) -> Sequence[T]:
|
|
59
|
+
return ms[:-1]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def grouped(
|
|
63
|
+
ms: Sequence[T],
|
|
64
|
+
key_func: Callable[[T], K],
|
|
65
|
+
keys_ordered: bool = False,
|
|
66
|
+
values_only: bool = False,
|
|
67
|
+
) -> list[tuple[K, list[T]]] | list[list[T]]:
|
|
68
|
+
"""
|
|
69
|
+
Groups the given list of elements according to key generator function
|
|
70
|
+
|
|
71
|
+
:param ms: list of elements
|
|
72
|
+
:param key_func: key generator function
|
|
73
|
+
:param keys_ordered: True if the return elements are sorted according to the keys
|
|
74
|
+
:param values_only: True if only return elements groups without corresponding keys
|
|
75
|
+
:return: grouped elements, with corresponding keys if `values_only` is set to False
|
|
76
|
+
"""
|
|
77
|
+
if ms is None or len(ms) == 0:
|
|
78
|
+
return []
|
|
79
|
+
grouped_ms: dict[K, list[T]] = {}
|
|
80
|
+
for m in ms:
|
|
81
|
+
k = key_func(m)
|
|
82
|
+
grouped_ms.setdefault(k, []).append(m)
|
|
83
|
+
groups = sorted(grouped_ms.items()) if keys_ordered else grouped_ms.items()
|
|
84
|
+
return [d for _, d in groups] if values_only else [(k, d) for k, d in groups]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def deduped(ms: Sequence[T], comp_func: Callable[[T, T], bool]) -> list[T]:
|
|
88
|
+
"""
|
|
89
|
+
Dedupes the given list of elements
|
|
90
|
+
|
|
91
|
+
:param ms: list of elements
|
|
92
|
+
:param comp_func: comparator generator function
|
|
93
|
+
:return: deduped elements
|
|
94
|
+
"""
|
|
95
|
+
if ms is None or len(ms) == 0:
|
|
96
|
+
return []
|
|
97
|
+
deduped_ms: list[T] = [head(ms)]
|
|
98
|
+
for m in tail(ms):
|
|
99
|
+
if not comp_func(last(deduped_ms), m):
|
|
100
|
+
deduped_ms.append(m)
|
|
101
|
+
return deduped_ms
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def batch_yield(ms: Iterable[T], batch_size: int) -> Generator[list[T]]:
|
|
105
|
+
"""
|
|
106
|
+
Splits the given input sequence into batches according to the specific batch size
|
|
107
|
+
|
|
108
|
+
:param ms: sequence of elements
|
|
109
|
+
:param batch_size: batch size
|
|
110
|
+
:return: batches of sequences
|
|
111
|
+
"""
|
|
112
|
+
if batch_size < 1:
|
|
113
|
+
raise ValueError("illegal batch size")
|
|
114
|
+
batch: list[T] = []
|
|
115
|
+
for m in ms:
|
|
116
|
+
batch.append(m)
|
|
117
|
+
if len(batch) == batch_size:
|
|
118
|
+
yield batch
|
|
119
|
+
batch = []
|
|
120
|
+
if len(batch) > 0:
|
|
121
|
+
yield batch
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def chunk(ms: Sequence[T], chunk_func: Callable[[Sequence[T], T], bool], exclusive_end: bool = False) -> list[list[T]]:
|
|
125
|
+
"""
|
|
126
|
+
Chops the list of elements into chunks
|
|
127
|
+
|
|
128
|
+
:param ms: list of elements
|
|
129
|
+
:param chunk_func: chunk generator function with compares the current chunk and the next element from the list
|
|
130
|
+
:param exclusive_end: set to true to make each chunk (except the last one) carrying the first element of
|
|
131
|
+
the next chunk as an exclusive end
|
|
132
|
+
:return: list of element chunks
|
|
133
|
+
"""
|
|
134
|
+
if ms is None or len(ms) == 0:
|
|
135
|
+
return []
|
|
136
|
+
chunks: list[list[T]] = [[head(ms)]]
|
|
137
|
+
for m in tail(ms):
|
|
138
|
+
if chunk_func(last(chunks), m):
|
|
139
|
+
if exclusive_end:
|
|
140
|
+
last(chunks).append(m)
|
|
141
|
+
chunks.append([m])
|
|
142
|
+
else:
|
|
143
|
+
last(chunks).append(m)
|
|
144
|
+
return chunks
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def chunk_between(ms: Sequence[T], chunk_func: Callable[[T, T], bool], exclusive_end: bool = False) -> list[list[T]]:
|
|
148
|
+
return chunk(ms, lambda x, y: chunk_func(last(x), y), exclusive_end)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def chunk_with_key(ms: Sequence[T], key_func: Callable[[T], K], exclusive_end: bool = False) -> list[list[T]]:
|
|
152
|
+
return chunk_between(ms, lambda x, y: key_func(x) != key_func(y), exclusive_end)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def merge_chunks(
|
|
156
|
+
chunks: Sequence[Sequence[T]],
|
|
157
|
+
merge_func: Callable[[Sequence[T], Sequence[T]], bool],
|
|
158
|
+
drop_exclusive_end: bool = False,
|
|
159
|
+
) -> list[list[T]]:
|
|
160
|
+
"""
|
|
161
|
+
Merges chunks according to the given merging criteria
|
|
162
|
+
|
|
163
|
+
:param chunks: chunks to be merged into larger ones
|
|
164
|
+
:param merge_func: merged chunk generator function
|
|
165
|
+
:param drop_exclusive_end: set to true if each of the given chunk (except the last one) as an exclusive end element,
|
|
166
|
+
and these exclusive end elements will be dropped while merging their chunks to the corresponding next chunks
|
|
167
|
+
:return: merged chunks
|
|
168
|
+
"""
|
|
169
|
+
if chunks is None or len(chunks) == 0:
|
|
170
|
+
return []
|
|
171
|
+
|
|
172
|
+
merged_chunks: list[list[T]] = []
|
|
173
|
+
|
|
174
|
+
def stateful_reducer(a: Sequence[T], b: Sequence[T]) -> Sequence[T]:
|
|
175
|
+
if merge_func(a, b):
|
|
176
|
+
if drop_exclusive_end:
|
|
177
|
+
return list(itertools.chain(init(a), b))
|
|
178
|
+
return list(itertools.chain(a, b))
|
|
179
|
+
else:
|
|
180
|
+
merged_chunks.append(list(a))
|
|
181
|
+
return b
|
|
182
|
+
|
|
183
|
+
last_chunk = functools.reduce(stateful_reducer, chunks)
|
|
184
|
+
merged_chunks.append(list(last_chunk))
|
|
185
|
+
return merged_chunks
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class Seq(Generic[T]):
|
|
189
|
+
def __init__(self, data: Iterable[T] | Self):
|
|
190
|
+
if isinstance(data, Seq):
|
|
191
|
+
self.data = data.data
|
|
192
|
+
elif isinstance(data, Iterable):
|
|
193
|
+
self.data = list(data)
|
|
194
|
+
else:
|
|
195
|
+
raise ValueError("unsupported data type")
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def count(self) -> int:
|
|
199
|
+
return len(self.data)
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def empty(self) -> bool:
|
|
203
|
+
return self.count == 0
|
|
204
|
+
|
|
205
|
+
def __add__(self, other: Self) -> Self:
|
|
206
|
+
return self.concat(other)
|
|
207
|
+
|
|
208
|
+
def __getitem__(self, item):
|
|
209
|
+
if isinstance(item, slice):
|
|
210
|
+
return Seq(self.data[item])
|
|
211
|
+
elif isinstance(item, int):
|
|
212
|
+
return Seq([self.data[item]])
|
|
213
|
+
raise ValueError("unsupported index type")
|
|
214
|
+
|
|
215
|
+
def concat(self, other: Self) -> Self:
|
|
216
|
+
return Seq(self.data + other.data)
|
|
217
|
+
|
|
218
|
+
def take_left(self, n: int) -> Self:
|
|
219
|
+
return self[:n]
|
|
220
|
+
|
|
221
|
+
def take_right(self, n: int) -> Self:
|
|
222
|
+
return self[-n:]
|
|
223
|
+
|
|
224
|
+
take = take_left
|
|
225
|
+
|
|
226
|
+
def reverse(self) -> Self:
|
|
227
|
+
return Seq(reversed(self.data))
|
|
228
|
+
|
|
229
|
+
def distinct(self) -> Self:
|
|
230
|
+
return Seq(list(set(self.data)))
|
|
231
|
+
|
|
232
|
+
def scan_left(self, zero: U | None, func: Callable[[U, T], U]) -> "Seq[U]":
|
|
233
|
+
def scan():
|
|
234
|
+
accum = zero
|
|
235
|
+
for elem in self.data:
|
|
236
|
+
accum = func(accum, elem)
|
|
237
|
+
yield accum
|
|
238
|
+
|
|
239
|
+
return Seq(scan())
|
|
240
|
+
|
|
241
|
+
def scan_right(self, zero: U | None, func: Callable[[U, T], U]) -> "Seq[U]":
|
|
242
|
+
def scan():
|
|
243
|
+
accum = zero
|
|
244
|
+
for elem in reversed(self.data):
|
|
245
|
+
accum = func(accum, elem)
|
|
246
|
+
yield accum
|
|
247
|
+
|
|
248
|
+
return Seq(reversed(list(scan())))
|
|
249
|
+
|
|
250
|
+
scan = scan_left
|
|
251
|
+
|
|
252
|
+
def map(self, func: Callable[[T], U]) -> "Seq[U]":
|
|
253
|
+
return self.scan(None, lambda x, y: func(y))
|
|
254
|
+
|
|
255
|
+
def fold_left(self, zero: U | None, func: Callable[[U, T], U]) -> "Seq[U]":
|
|
256
|
+
if self.empty:
|
|
257
|
+
return Seq([]) if zero is None else Seq([zero])
|
|
258
|
+
data = self.data if zero is None else [zero] + self.data
|
|
259
|
+
accum = head(data)
|
|
260
|
+
for elem in tail(data):
|
|
261
|
+
accum = func(accum, elem)
|
|
262
|
+
return Seq([accum])
|
|
263
|
+
|
|
264
|
+
def fold_right(self, zero: U | None, func: Callable[[U, T], U]) -> "Seq[U]":
|
|
265
|
+
if self.empty:
|
|
266
|
+
return Seq([]) if zero is None else Seq([zero])
|
|
267
|
+
data = self.data if zero is None else self.data + [zero]
|
|
268
|
+
accum = last(data)
|
|
269
|
+
for elem in reversed(init(data)):
|
|
270
|
+
accum = func(accum, elem)
|
|
271
|
+
return Seq([accum])
|
|
272
|
+
|
|
273
|
+
fold = fold_left
|
|
274
|
+
|
|
275
|
+
def reduce(self, func: Callable[[T, T], T]) -> Self:
|
|
276
|
+
return self.fold(None, lambda x, y: func(x, y))
|
|
277
|
+
|
|
278
|
+
def max(self, func: Callable[[T, T], bool] = None) -> Self:
|
|
279
|
+
func = func or (lambda x, y: x > y)
|
|
280
|
+
return self.reduce(lambda x, y: x if func(x, y) else y)
|
|
281
|
+
|
|
282
|
+
def min(self, func: Callable[[T, T], bool] = None) -> Self:
|
|
283
|
+
func = func or (lambda x, y: x < y)
|
|
284
|
+
return self.reduce(lambda x, y: x if func(x, y) else y)
|
|
285
|
+
|
|
286
|
+
def group(self, func: Callable[[T], K]) -> "Seq[tuple[K, list[T]]]":
|
|
287
|
+
return Seq(grouped(self.data, key_func=func, keys_ordered=True))
|
|
288
|
+
|
|
289
|
+
def keys(self):
|
|
290
|
+
return Seq(key for key, _ in self.data)
|
|
291
|
+
|
|
292
|
+
def values(self):
|
|
293
|
+
return Seq(value for _, value in self.data)
|
|
294
|
+
|
|
295
|
+
def swap(self):
|
|
296
|
+
return Seq((value, key) for key, value in self.data)
|
|
297
|
+
|
|
298
|
+
def map_keys(self, func: Callable[[T], U]) -> Self:
|
|
299
|
+
return Seq((func(key), value) for key, value in self.data)
|
|
300
|
+
|
|
301
|
+
def map_values(self, func: Callable[[T], U]) -> Self:
|
|
302
|
+
return Seq((key, func(value)) for key, value in self.data)
|
|
303
|
+
|
|
304
|
+
def flatten(self):
|
|
305
|
+
data = []
|
|
306
|
+
for d in self.data:
|
|
307
|
+
data.extend(d)
|
|
308
|
+
return Seq(data)
|
|
309
|
+
|
|
310
|
+
def flat_map(self, func: Callable[[T], U]) -> "Seq[U]":
|
|
311
|
+
return self.flatten().map(func)
|
|
312
|
+
|
|
313
|
+
def group_map(self, group_func: Callable[[T], K], map_func: Callable[[T], U]) -> "Seq[tuple[K, list[U]]]":
|
|
314
|
+
return self.group(group_func).map_values(lambda x: list(map(map_func, x)))
|
|
315
|
+
|
|
316
|
+
def filter(self, func: Callable[[T], bool]) -> Self:
|
|
317
|
+
return Seq(filter(func, self.data))
|
|
318
|
+
|
|
319
|
+
def filter_not(self, func: Callable[[T], bool]) -> Self:
|
|
320
|
+
return self.filter(lambda x: not func(x))
|
|
321
|
+
|
|
322
|
+
def sort(self, func: Callable[[T], K]) -> Self:
|
|
323
|
+
return Seq(sorted(self.data, key=func))
|
|
324
|
+
|
|
325
|
+
def head(self) -> Self:
|
|
326
|
+
return Seq([head(self.data)])
|
|
327
|
+
|
|
328
|
+
def last(self) -> Self:
|
|
329
|
+
return Seq([last(self.data)])
|
|
330
|
+
|
|
331
|
+
def init(self) -> Self:
|
|
332
|
+
return Seq(init(self.data))
|
|
333
|
+
|
|
334
|
+
def tail(self) -> Self:
|
|
335
|
+
return Seq(tail(self.data))
|
|
336
|
+
|
|
337
|
+
def foreach(self, func: Callable[[T], None]) -> Self:
|
|
338
|
+
for elem in self.data:
|
|
339
|
+
func(elem)
|
|
340
|
+
return self
|
|
341
|
+
|
|
342
|
+
def exists(self, func: Callable[[T], bool]) -> "Seq[bool]":
|
|
343
|
+
return Seq([any(map(func, self.data))])
|
|
344
|
+
|
|
345
|
+
def forall(self, func: Callable[[T], bool]) -> "Seq[bool]":
|
|
346
|
+
return Seq([all(map(func, self.data))])
|
|
347
|
+
|
|
348
|
+
def union(self, other: Self) -> Self:
|
|
349
|
+
return Seq(list(set(self.data).union(set(other.data))))
|
|
350
|
+
|
|
351
|
+
def intersect(self, other: Self) -> Self:
|
|
352
|
+
return Seq(list(set(self.data).intersection(set(other.data))))
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
seq = Seq
|