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.
Files changed (70) hide show
  1. iker_python_common-1.0.2/PKG-INFO +22 -0
  2. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/pyproject.toml +7 -24
  3. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/src/iker/common/utils/dbutils.py +4 -46
  4. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/src/iker/common/utils/logger.py +0 -3
  5. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/src/iker/common/utils/s3utils.py +5 -4
  6. iker_python_common-1.0.2/src/iker/common/utils/sequtils.py +355 -0
  7. iker_python_common-1.0.2/src/iker/common/utils/span.py +221 -0
  8. iker_python_common-1.0.2/src/iker_python_common.egg-info/PKG-INFO +22 -0
  9. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/src/iker_python_common.egg-info/SOURCES.txt +2 -4
  10. iker_python_common-1.0.2/src/iker_python_common.egg-info/requires.txt +16 -0
  11. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/test/iker_tests/common/utils/dbutils_test.py +0 -3
  12. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/test/iker_tests/common/utils/s3utils_test.py +7 -7
  13. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/test/iker_tests/common/utils/sequtils_test.py +148 -270
  14. iker_python_common-1.0.2/test/iker_tests/common/utils/span_test.py +298 -0
  15. iker_python_common-1.0.1/PKG-INFO +0 -39
  16. iker_python_common-1.0.1/src/iker/common/core/exceptions.py +0 -64
  17. iker_python_common-1.0.1/src/iker/common/utils/__init__.py +0 -0
  18. iker_python_common-1.0.1/src/iker/common/utils/sequtils.py +0 -394
  19. iker_python_common-1.0.1/src/iker/common/utils/stream.py +0 -188
  20. iker_python_common-1.0.1/src/iker_python_common.egg-info/PKG-INFO +0 -39
  21. iker_python_common-1.0.1/src/iker_python_common.egg-info/requires.txt +0 -33
  22. iker_python_common-1.0.1/test/iker_tests/common/core/exceptions_test.py +0 -20
  23. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/.editorconfig +0 -0
  24. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/.github/workflows/pr.yml +0 -0
  25. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/.github/workflows/push.yml +0 -0
  26. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/.gitignore +0 -0
  27. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/MANIFEST.in +0 -0
  28. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/README.md +0 -0
  29. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/VERSION +0 -0
  30. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/resources/unittest/shutils/dir.baz/file.bar.baz +0 -0
  31. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/resources/unittest/shutils/dir.baz/file.foo.bar +0 -0
  32. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/resources/unittest/shutils/dir.baz/file.foo.baz +0 -0
  33. {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
  34. {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
  35. {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
  36. {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
  37. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/resources/unittest/shutils/dir.foo/file.bar +0 -0
  38. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/resources/unittest/shutils/dir.foo/file.baz +0 -0
  39. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/resources/unittest/shutils/dir.foo/file.foo +0 -0
  40. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/setup.cfg +0 -0
  41. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/setup.py +0 -0
  42. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/src/iker/common/__init__.py +0 -0
  43. {iker_python_common-1.0.1/src/iker/common/core → iker_python_common-1.0.2/src/iker/common/utils}/__init__.py +0 -0
  44. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/src/iker/common/utils/config.py +0 -0
  45. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/src/iker/common/utils/dockerutils.py +0 -0
  46. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/src/iker/common/utils/dtutils.py +0 -0
  47. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/src/iker/common/utils/funcutils.py +0 -0
  48. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/src/iker/common/utils/numutils.py +0 -0
  49. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/src/iker/common/utils/randutils.py +0 -0
  50. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/src/iker/common/utils/retry.py +0 -0
  51. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/src/iker/common/utils/shutils.py +0 -0
  52. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/src/iker/common/utils/strutils.py +0 -0
  53. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/src/iker/common/utils/testutils.py +0 -0
  54. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/src/iker_python_common.egg-info/dependency_links.txt +0 -0
  55. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/src/iker_python_common.egg-info/not-zip-safe +0 -0
  56. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/src/iker_python_common.egg-info/top_level.txt +0 -0
  57. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/test/iker_tests/__init__.py +0 -0
  58. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/test/iker_tests/common/utils/config_test.py +0 -0
  59. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/test/iker_tests/common/utils/dockerutils_test.py +0 -0
  60. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/test/iker_tests/common/utils/dtutils_test.py +0 -0
  61. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/test/iker_tests/common/utils/funcutils_test.py +0 -0
  62. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/test/iker_tests/common/utils/logger_test.py +0 -0
  63. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/test/iker_tests/common/utils/numutils_test.py +0 -0
  64. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/test/iker_tests/common/utils/randutils_test.py +0 -0
  65. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/test/iker_tests/common/utils/retry_test.py +0 -0
  66. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/test/iker_tests/common/utils/shutils_test.py +0 -0
  67. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/test/iker_tests/common/utils/strutils_test.py +0 -0
  68. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/test/iker_tests/common/utils/testutils_test.py +0 -0
  69. {iker_python_common-1.0.1 → iker_python_common-1.0.2}/test/iker_tests/docker_fixtures.py +0 -0
  70. {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~=72.0",
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.34",
20
- "brotli~=1.0",
19
+ "boto3~=1.35",
21
20
  "docker~=7.1",
22
- "jsonpath-ng~=1.6",
23
- "jsonschema~=4.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]~=4.2",
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.2",
52
- "pytest-postgresql~=6.0",
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 psycopg2
8
- import psycopg2.extensions
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+mysqldb"
39
- Postgresql = "postgresql+psycopg2"
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:
@@ -1,6 +1,3 @@
1
- """
2
- Module-aware logging
3
- """
4
1
  import inspect
5
2
  import logging
6
3
 
@@ -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
- session = boto3.Session(aws_access_key_id=trim_to_none(access_key_id),
53
- aws_secret_access_key=trim_to_none(secret_access_key))
54
- client = session.client("s3", region_name=trim_to_none(region_name), endpoint_url=trim_to_none(endpoint_url))
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