compressed-lists 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,92 @@
1
+ from typing import List, Optional, Sequence
2
+
3
+ import numpy as np
4
+
5
+ from .CompressedList import CompressedList
6
+ from .partition import Partitioning
7
+
8
+ __author__ = "Jayaram Kancherla"
9
+ __copyright__ = "Jayaram Kancherla"
10
+ __license__ = "MIT"
11
+
12
+
13
+ class CompressedIntegerList(CompressedList):
14
+ """CompressedList implementation for lists of integers."""
15
+
16
+ def __init__(
17
+ self,
18
+ unlist_data: np.ndarray,
19
+ partitioning: Partitioning,
20
+ element_metadata: dict = None,
21
+ metadata: dict = None,
22
+ **kwargs,
23
+ ):
24
+ """Initialize a CompressedIntegerList.
25
+
26
+ Args:
27
+ unlist_data:
28
+ NumPy array of integers.
29
+
30
+ partitioning:
31
+ Partitioning object defining element boundaries.
32
+
33
+ element_metadata:
34
+ Optional metadata for elements.
35
+
36
+ metadata:
37
+ Optional general metadata.
38
+
39
+ kwargs:
40
+ Additional arguments.
41
+ """
42
+ super().__init__(
43
+ unlist_data, partitioning, element_type="integer", element_metadata=element_metadata, metadata=metadata
44
+ )
45
+
46
+ def _extract_range(self, start: int, end: int) -> np.ndarray:
47
+ """Extract a range from unlist_data.
48
+
49
+ Args:
50
+ start:
51
+ Start index (inclusive).
52
+
53
+ end:
54
+ End index (exclusive).
55
+
56
+ Returns:
57
+ Same type as unlist_data.
58
+ """
59
+ return self._unlist_data[start:end]
60
+
61
+ @classmethod
62
+ def from_list(
63
+ cls, lst: List[List[int]], names: Optional[Sequence[str]] = None, metadata: dict = None
64
+ ) -> "CompressedIntegerList":
65
+ """
66
+ Create a CompressedIntegerList from a list of integer lists.
67
+
68
+ Args:
69
+ lst:
70
+ List of integer lists.
71
+
72
+ names:
73
+ Optional names for list elements.
74
+
75
+ metadata:
76
+ Optional metadata.
77
+
78
+ Returns:
79
+ A new CompressedIntegerList.
80
+ """
81
+ # Flatten the list
82
+ flat_data = []
83
+ for sublist in lst:
84
+ flat_data.extend(sublist)
85
+
86
+ # Create partitioning
87
+ partitioning = Partitioning.from_list(lst, names)
88
+
89
+ # Create unlist_data
90
+ unlist_data = np.array(flat_data, dtype=np.int64)
91
+
92
+ return cls(unlist_data, partitioning, metadata=metadata)
@@ -0,0 +1,583 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any, Callable, Iterator, List, Optional, Sequence, Union
3
+ from warnings import warn
4
+
5
+ import biocutils as ut
6
+ import numpy as np
7
+
8
+ from .partition import Partitioning
9
+
10
+ __author__ = "Jayaram Kancherla"
11
+ __copyright__ = "Jayaram Kancherla"
12
+ __license__ = "MIT"
13
+
14
+
15
+ def _validate_data_and_partitions(unlist_data, partition):
16
+ if len(unlist_data) != partition.nobj():
17
+ raise ValueError(
18
+ f"Length of 'unlist_data' ({len(unlist_data)}) "
19
+ f"doesn't match 'partitioning' total length ({partition.nobj()})."
20
+ )
21
+
22
+
23
+ class CompressedList(ABC):
24
+ """Base class for compressed list objects.
25
+
26
+ `CompressedList` stores list elements concatenated in a single vector-like object
27
+ with partitioning information that defines where each list element starts and ends.
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ unlist_data: Any,
33
+ partitioning: Partitioning,
34
+ element_type: str = None,
35
+ element_metadata: dict = None,
36
+ metadata: Optional[dict] = None,
37
+ validate: bool = True,
38
+ ):
39
+ """Initialize a CompressedList.
40
+
41
+ Args:
42
+ unlist_data:
43
+ Vector-like object containing concatenated elements.
44
+
45
+ partitioning:
46
+ Partitioning object defining element boundaries.
47
+
48
+ element_type:
49
+ String identifier for the type of elements.
50
+
51
+ element_metadata:
52
+ Optional metadata for elements.
53
+
54
+ metadata:
55
+ Optional general metadata.
56
+
57
+ validate:
58
+ Internal use only.
59
+ """
60
+ self._unlist_data = unlist_data
61
+ self._partitioning = partitioning
62
+ self._element_type = element_type
63
+ self._element_metadata = element_metadata or {}
64
+ self._metadata = metadata or {}
65
+
66
+ if validate:
67
+ _validate_data_and_partitions(self._unlist_data, self._partitioning)
68
+
69
+ def _define_output(self, in_place: bool = False) -> "Partitioning":
70
+ if in_place is True:
71
+ return self
72
+ else:
73
+ return self.__copy__()
74
+
75
+ #########################
76
+ ######>> Copying <<######
77
+ #########################
78
+
79
+ def __deepcopy__(self, memo=None, _nil=[]):
80
+ """
81
+ Returns:
82
+ A deep copy of the current ``Partitioning``.
83
+ """
84
+ from copy import deepcopy
85
+
86
+ _unlistdata_copy = deepcopy(self._unlist_data)
87
+ _part_copy = deepcopy(self._partitioning)
88
+ _elem_type_copy = deepcopy(self._element_type)
89
+ _elem_metadata_copy = deepcopy(self._element_metadata)
90
+ _metadata_copy = deepcopy(self._metadata)
91
+
92
+ current_class_const = type(self)
93
+ return current_class_const(
94
+ unlist_data=_unlistdata_copy,
95
+ partitioning=_part_copy,
96
+ element_type=_elem_type_copy,
97
+ element_metadata=_elem_metadata_copy,
98
+ metadata=_metadata_copy,
99
+ )
100
+
101
+ def __copy__(self):
102
+ """
103
+ Returns:
104
+ A shallow copy of the current ``Partitioning``.
105
+ """
106
+ current_class_const = type(self)
107
+ return current_class_const(
108
+ unlist_data=self._unlist_data,
109
+ partitioning=self._partitioning,
110
+ element_type=self._element_type,
111
+ element_metadata=self._element_metadata,
112
+ metadata=self._metadata,
113
+ )
114
+
115
+ def copy(self):
116
+ """Alias for :py:meth:`~__copy__`."""
117
+ return self.__copy__()
118
+
119
+ ######################################
120
+ ######>> length and iterators <<######
121
+ ######################################
122
+
123
+ def __len__(self) -> int:
124
+ """Return the number of list elements."""
125
+ return len(self._partitioning)
126
+
127
+ def get_element_lengths(self) -> np.ndarray:
128
+ """Get the lengths of each list element."""
129
+ return self._partitioning.get_element_lengths()
130
+
131
+ def __iter__(self) -> Iterator[Any]:
132
+ """Iterate over list elements."""
133
+ for i in range(len(self)):
134
+ yield self[i]
135
+
136
+ ##########################
137
+ ######>> Printing <<######
138
+ ##########################
139
+
140
+ def __repr__(self) -> str:
141
+ """
142
+ Returns:
143
+ A string representation.
144
+ """
145
+ output = f"{type(self).__name__}(number_of_elements={len(self)}"
146
+ output += ", unlist_data=" + ut.print_truncated_list(self._unlist_data)
147
+ output += ", partitioning=" + self._partitioning.__repr__()
148
+ output += ", element_type=" + self._element_type
149
+
150
+ if len(self._element_metadata) > 0:
151
+ output += ", element_metadata=" + ut.print_truncated_dict(self._element_metadata)
152
+
153
+ if len(self._metadata) > 0:
154
+ output += ", metadata=" + ut.print_truncated_dict(self._metadata)
155
+
156
+ output += ")"
157
+ return output
158
+
159
+ def __str__(self) -> str:
160
+ """
161
+ Returns:
162
+ A pretty-printed string containing the contents of this object.
163
+ """
164
+ output = f"class: {type(self).__name__}\n"
165
+
166
+ output += f"number of elements: ({len(self)}) of type: {self._element_type}\n"
167
+
168
+ output += f"unlist_data: {ut.print_truncated_list(self._unlist_data)}\n"
169
+
170
+ output += f"partitioning: {ut.print_truncated_list(self._partitioning)}\n"
171
+
172
+ output += f"element_metadata({str(len(self._element_metadata))}): {ut.print_truncated_list(list(self._element_metadata.keys()), sep=' ', include_brackets=False, transform=lambda y: y)}\n"
173
+ output += f"metadata({str(len(self._metadata))}): {ut.print_truncated_list(list(self._metadata.keys()), sep=' ', include_brackets=False, transform=lambda y: y)}\n"
174
+
175
+ return output
176
+
177
+ #############################
178
+ ######>> element_type <<#####
179
+ #############################
180
+
181
+ def get_element_type(self) -> str:
182
+ """Return the element_type."""
183
+ return self._element_type
184
+
185
+ @property
186
+ def element_type(self) -> str:
187
+ """Alias for :py:attr:`~get_element_type`, provided for back-compatibility."""
188
+ return self.get_element_type()
189
+
190
+ ###########################
191
+ ######>> partitions <<#####
192
+ ###########################
193
+
194
+ def get_partitioning(self) -> Partitioning:
195
+ """Return the paritioning info."""
196
+ return self._partitioning
197
+
198
+ @property
199
+ def paritioning(self) -> Partitioning:
200
+ """Alias for :py:attr:`~get_paritioning`, provided for back-compatibility."""
201
+ return self.get_partitioning()
202
+
203
+ #######################
204
+ ######>> names <<######
205
+ #######################
206
+
207
+ def get_names(self) -> Optional[ut.NamedList]:
208
+ """Get the names of list elements."""
209
+ return self._partitioning.get_names()
210
+
211
+ def set_names(self, names: Sequence[str], in_place: bool = False) -> "CompressedList":
212
+ """Set the names of list elements.
213
+
214
+ names:
215
+ New names, same as the number of rows.
216
+
217
+ May be `None` to remove names.
218
+
219
+ in_place:
220
+ Whether to modify the ``CompressedList`` in place.
221
+
222
+ Returns:
223
+ A modified ``CompressedList`` object, either as a copy of the original
224
+ or as a reference to the (in-place-modified) original.
225
+ """
226
+ output = self._define_output(in_place)
227
+ output._partitioning = self._partitioning.set_names(names, in_place=False)
228
+ return output
229
+
230
+ @property
231
+ def names(self) -> Optional[ut.NamedList]:
232
+ """Alias for :py:attr:`~get_names`."""
233
+ return self._partitioning.get_names()
234
+
235
+ @names.setter
236
+ def names(self, names: Sequence[str]):
237
+ """Alias for :py:attr:`~set_names` with ``in_place = True``.
238
+
239
+ As this mutates the original object, a warning is raised.
240
+ """
241
+ warn(
242
+ "Setting property 'names' is an in-place operation, use 'set_names' instead",
243
+ UserWarning,
244
+ )
245
+ self.set_names(names=names, in_place=True)
246
+
247
+ #############################
248
+ ######>> unlist_data <<######
249
+ #############################
250
+
251
+ def get_unlist_data(self) -> Any:
252
+ """Get all elements."""
253
+ return self._unlist_data
254
+
255
+ def set_unlist_data(self, unlist_data: Any, in_place: bool = False) -> "CompressedList":
256
+ """Set new list elements.
257
+
258
+ Args:
259
+ unlist_data:
260
+ New vector-like object containing concatenated elements.
261
+
262
+ in_place:
263
+ Whether to modify the ``CompressedList`` in place.
264
+
265
+ Returns:
266
+ A modified ``CompressedList`` object, either as a copy of the original
267
+ or as a reference to the (in-place-modified) original.
268
+ """
269
+ output = self._define_output(in_place)
270
+
271
+ _validate_data_and_partitions(unlist_data=unlist_data, partition=self._partitioning)
272
+
273
+ output._unlist_data = unlist_data
274
+ return output
275
+
276
+ @property
277
+ def unlist_data(self) -> Any:
278
+ """Alias for :py:attr:`~get_unlist_data`."""
279
+ return self.get_unlist_data()
280
+
281
+ @unlist_data.setter
282
+ def unlist_data(self, unlist_data: Any):
283
+ """Alias for :py:attr:`~set_unlist_data` with ``in_place = True``.
284
+
285
+ As this mutates the original object, a warning is raised.
286
+ """
287
+ warn(
288
+ "Setting property 'unlist_data' is an in-place operation, use 'set_unlist_data' instead",
289
+ UserWarning,
290
+ )
291
+ self.set_unlist_data(unlist_data, in_place=True)
292
+
293
+ ###################################
294
+ ######>> element metadata <<#######
295
+ ###################################
296
+
297
+ def get_element_metadata(self) -> dict:
298
+ """
299
+ Returns:
300
+ Dictionary of metadata for each element in this object.
301
+ """
302
+ return self._element_metadata
303
+
304
+ def set_element_metadata(self, element_metadata: dict, in_place: bool = False) -> "CompressedList":
305
+ """Set new element metadata.
306
+
307
+ Args:
308
+ element_metadata:
309
+ New element metadata for this object.
310
+
311
+ in_place:
312
+ Whether to modify the ``CompressedList`` in place.
313
+
314
+ Returns:
315
+ A modified ``CompressedList`` object, either as a copy of the original
316
+ or as a reference to the (in-place-modified) original.
317
+ """
318
+ if not isinstance(element_metadata, dict):
319
+ raise TypeError(f"`element_metadata` must be a dictionary, provided {type(element_metadata)}.")
320
+ output = self._define_output(in_place)
321
+ output._element_metadata = element_metadata
322
+ return output
323
+
324
+ @property
325
+ def element_metadata(self) -> dict:
326
+ """Alias for :py:attr:`~get_element_metadata`."""
327
+ return self.get_element_metadata()
328
+
329
+ @element_metadata.setter
330
+ def element_metadata(self, element_metadata: dict):
331
+ """Alias for :py:attr:`~set_element_metadata` with ``in_place = True``.
332
+
333
+ As this mutates the original object, a warning is raised.
334
+ """
335
+ warn(
336
+ "Setting property 'element_metadata' is an in-place operation, use 'set_element_metadata' instead",
337
+ UserWarning,
338
+ )
339
+ self.set_element_metadata(element_metadata, in_place=True)
340
+
341
+ ###########################
342
+ ######>> metadata <<#######
343
+ ###########################
344
+
345
+ def get_metadata(self) -> dict:
346
+ """
347
+ Returns:
348
+ Dictionary of metadata for this object.
349
+ """
350
+ return self._metadata
351
+
352
+ def set_metadata(self, metadata: dict, in_place: bool = False) -> "CompressedList":
353
+ """Set additional metadata.
354
+
355
+ Args:
356
+ metadata:
357
+ New metadata for this object.
358
+
359
+ in_place:
360
+ Whether to modify the ``CompressedList`` in place.
361
+
362
+ Returns:
363
+ A modified ``CompressedList`` object, either as a copy of the original
364
+ or as a reference to the (in-place-modified) original.
365
+ """
366
+ if not isinstance(metadata, dict):
367
+ raise TypeError(f"`metadata` must be a dictionary, provided {type(metadata)}.")
368
+ output = self._define_output(in_place)
369
+ output._metadata = metadata
370
+ return output
371
+
372
+ @property
373
+ def metadata(self) -> dict:
374
+ """Alias for :py:attr:`~get_metadata`."""
375
+ return self.get_metadata()
376
+
377
+ @metadata.setter
378
+ def metadata(self, metadata: dict):
379
+ """Alias for :py:attr:`~set_metadata` with ``in_place = True``.
380
+
381
+ As this mutates the original object, a warning is raised.
382
+ """
383
+ warn(
384
+ "Setting property 'metadata' is an in-place operation, use 'set_metadata' instead",
385
+ UserWarning,
386
+ )
387
+ self.set_metadata(metadata, in_place=True)
388
+
389
+ ##########################
390
+ ######>> accessors <<#####
391
+ ##########################
392
+
393
+ def __getitem__(self, key: Union[int, str, slice]) -> Any:
394
+ """Get an element or slice of elements from the list.
395
+
396
+ Args:
397
+ key:
398
+ Integer index, string name, or slice.
399
+
400
+ Returns:
401
+ List element(s).
402
+ """
403
+ # string keys (names)
404
+ if isinstance(key, str):
405
+ if key not in self.names:
406
+ raise KeyError(f"No element named '{key}'.")
407
+ key = list(self.names).index(key)
408
+
409
+ # integer indices
410
+ if isinstance(key, int):
411
+ if key < 0:
412
+ key += len(self)
413
+ if key < 0 or key >= len(self):
414
+ raise IndexError(f"List index '{key}' out of range.")
415
+
416
+ start, end = self._partitioning.get_partition_range(key)
417
+ return self._extract_range(start, end)
418
+
419
+ # slices
420
+ elif isinstance(key, slice):
421
+ indices = range(*key.indices(len(self)))
422
+ result = []
423
+ for i in indices:
424
+ start, end = self._partitioning.get_partition_range(i)
425
+ result.append(self._extract_range(start, end))
426
+
427
+ # Create a new CompressedList from the result
428
+ return self.__class__.from_list(
429
+ result, names=[self.names[i] for i in indices] if self.names[0] is not None else None
430
+ )
431
+
432
+ else:
433
+ raise TypeError("Index must be int, str, or slice.")
434
+
435
+ ##################################
436
+ ######>> abstract methods <<######
437
+ ##################################
438
+
439
+ @abstractmethod
440
+ def _extract_range(self, start: int, end: int) -> Any:
441
+ """Extract a range from `unlist_data`.
442
+
443
+ This method must be implemented by subclasses to handle
444
+ type-specific extraction from `unlist_data`.
445
+
446
+ Args:
447
+ start:
448
+ Start index (inclusive).
449
+
450
+ end:
451
+ End index (exclusive).
452
+
453
+ Returns:
454
+ Extracted element.
455
+ """
456
+ pass
457
+
458
+ @classmethod
459
+ @abstractmethod
460
+ def from_list(
461
+ cls, lst: List[Any], names: Optional[Sequence[str]] = None, metadata: dict = None
462
+ ) -> "CompressedList[Any]":
463
+ """Create a CompressedList from a regular list.
464
+
465
+ This method must be implemented by subclasses to handle
466
+ type-specific conversion from list to unlist_data.
467
+
468
+ Args:
469
+ lst:
470
+ List to convert.
471
+
472
+ names:
473
+ Optional names for list elements.
474
+
475
+ metadata:
476
+ Optional metadata.
477
+
478
+ Returns:
479
+ A new `CompressedList`.
480
+ """
481
+ pass
482
+
483
+ ###########################
484
+ ######>> coercions <<######
485
+ ###########################
486
+
487
+ def to_list(self) -> List[Any]:
488
+ """Convert to a regular Python list.
489
+
490
+ Returns:
491
+ A regular Python list with all elements.
492
+ """
493
+ return list(self)
494
+
495
+ def unlist(self, use_names: bool = True) -> Any:
496
+ """Get the underlying unlisted data.
497
+
498
+ Args:
499
+ use_names:
500
+ Whether to include names in the result if applicable.
501
+
502
+ Currently not used.
503
+
504
+ Returns:
505
+ The unlisted data.
506
+ """
507
+ return self._unlist_data
508
+
509
+ def relist(self, unlist_data: Any) -> "CompressedList[Any]":
510
+ """Create a new `CompressedList` with the same partitioning but different data.
511
+
512
+ Args:
513
+ unlist_data:
514
+ New unlisted data.
515
+
516
+ Returns:
517
+ A new CompressedList.
518
+ """
519
+ _validate_data_and_partitions(unlist_data, self._partitioning)
520
+
521
+ return self.__class__(
522
+ unlist_data,
523
+ self._partitioning.copy(),
524
+ element_type=self._element_type,
525
+ element_metadata=self._element_metadata.copy(),
526
+ metadata=self._metadata.copy(),
527
+ )
528
+
529
+ def extract_subset(self, indices: Sequence[int]) -> "CompressedList[Any]":
530
+ """Extract a subset of elements by indices.
531
+
532
+ Args:
533
+ indices:
534
+ Sequence of indices to extract.
535
+
536
+ Returns:
537
+ A new CompressedList with only the selected elements.
538
+ """
539
+ # Validate indices
540
+ for i in indices:
541
+ if i < 0 or i >= len(self):
542
+ raise IndexError(f"Index {i} out of range")
543
+
544
+ # Extract element lengths and names
545
+ new_lengths = [self.get_element_lengths()[i] for i in indices]
546
+ new_names = [self.names[i] for i in indices] if self.names[0] is not None else None
547
+
548
+ # Create new partitioning
549
+ new_partitioning = Partitioning.from_lengths(new_lengths, new_names)
550
+
551
+ # Extract data
552
+ new_data = []
553
+ for i in indices:
554
+ start, end = self._partitioning.get_partition_range(i)
555
+ if isinstance(self._unlist_data, np.ndarray):
556
+ new_data.append(self._unlist_data[start:end])
557
+ else:
558
+ new_data.extend(self._unlist_data[start:end])
559
+
560
+ if isinstance(self._unlist_data, np.ndarray):
561
+ new_data = np.concatenate(new_data)
562
+
563
+ # Create new compressed list
564
+ return self.__class__(
565
+ new_data,
566
+ new_partitioning,
567
+ element_type=self._element_type,
568
+ element_metadata={k: v for k, v in self._element_metadata.items() if k in indices},
569
+ metadata=self._metadata.copy(),
570
+ )
571
+
572
+ def lapply(self, func: Callable) -> "CompressedList[Any]":
573
+ """Apply a function to each element.
574
+
575
+ Args:
576
+ func:
577
+ Function to apply to each element.
578
+
579
+ Returns:
580
+ A new CompressedList with the results.
581
+ """
582
+ result = [func(elem) for elem in self]
583
+ return self.__class__.from_list(result, self.names, self._metadata)
@@ -0,0 +1,86 @@
1
+ from typing import List, Optional, Sequence
2
+
3
+ from .CompressedList import CompressedList
4
+ from .partition import Partitioning
5
+
6
+ __author__ = "Jayaram Kancherla"
7
+ __copyright__ = "Jayaram Kancherla"
8
+ __license__ = "MIT"
9
+
10
+
11
+ class CompressedStringList(CompressedList):
12
+ """CompressedList implementation for lists of strings."""
13
+
14
+ def __init__(
15
+ self,
16
+ unlist_data: List[str],
17
+ partitioning: Partitioning,
18
+ element_metadata: dict = None,
19
+ metadata: dict = None,
20
+ **kwargs,
21
+ ):
22
+ """Initialize a CompressedStringList.
23
+
24
+ Args:
25
+ unlist_data:
26
+ List of strings.
27
+
28
+ partitioning:
29
+ Partitioning object defining element boundaries.
30
+
31
+ element_metadata:
32
+ Optional metadata for elements.
33
+
34
+ metadata:
35
+ Optional general metadata.
36
+
37
+ kwargs:
38
+ Additional arguments.
39
+ """
40
+ super().__init__(
41
+ unlist_data, partitioning, element_type="string", element_metadata=element_metadata, metadata=metadata
42
+ )
43
+
44
+ def _extract_range(self, start: int, end: int) -> List[str]:
45
+ """Extract a range from unlist_data.
46
+
47
+ Args:
48
+ start:
49
+ Start index (inclusive).
50
+
51
+ end:
52
+ End index (exclusive).
53
+
54
+ Returns:
55
+ List of strings.
56
+ """
57
+ return self._unlist_data[start:end]
58
+
59
+ @classmethod
60
+ def from_list(
61
+ cls, lst: List[List[str]], names: Optional[Sequence[str]] = None, metadata: dict = None
62
+ ) -> "CompressedStringList":
63
+ """Create a `CompressedStringList` from a list of string lists.
64
+
65
+ Args:
66
+ lst:
67
+ List of string lists.
68
+
69
+ names:
70
+ Optional names for list elements.
71
+
72
+ metadata:
73
+ Optional metadata.
74
+
75
+ Returns:
76
+ A new `CompressedStringList`.
77
+ """
78
+ # Flatten the list
79
+ flat_data = []
80
+ for sublist in lst:
81
+ flat_data.extend(sublist)
82
+
83
+ # Create partitioning
84
+ partitioning = Partitioning.from_list(lst, names)
85
+
86
+ return cls(flat_data, partitioning, metadata=metadata)
@@ -0,0 +1,21 @@
1
+ import sys
2
+
3
+ if sys.version_info[:2] >= (3, 8):
4
+ # TODO: Import directly (no need for conditional) when `python_requires = >= 3.8`
5
+ from importlib.metadata import PackageNotFoundError, version # pragma: no cover
6
+ else:
7
+ from importlib_metadata import PackageNotFoundError, version # pragma: no cover
8
+
9
+ try:
10
+ # Change here if project is renamed and does not equal the package name
11
+ dist_name = "compressed-lists"
12
+ __version__ = version(dist_name)
13
+ except PackageNotFoundError: # pragma: no cover
14
+ __version__ = "unknown"
15
+ finally:
16
+ del version, PackageNotFoundError
17
+
18
+ from .partition import Partitioning
19
+ from .CompressedList import CompressedList
20
+ from .CompressedIntegerList import CompressedIntegerList
21
+ from .CompressedStringList import CompressedStringList
@@ -0,0 +1,285 @@
1
+ from typing import List, Optional, Sequence, Union
2
+ from warnings import warn
3
+
4
+ import biocutils as ut
5
+ import numpy as np
6
+
7
+ __author__ = "Jayaram Kancherla"
8
+ __copyright__ = "Jayaram Kancherla"
9
+ __license__ = "MIT"
10
+
11
+
12
+ def _validate_names(names, expected_len):
13
+ if names is None:
14
+ return
15
+
16
+ if len(names) != expected_len:
17
+ raise ValueError("Length of names must match length of ends.")
18
+
19
+
20
+ class Partitioning:
21
+ """Represents partitioning information for a `CompressedList`.
22
+
23
+ This is similar to the ``PartitioningByEnd`` class in Bioconductor.
24
+ It keeps track of where each element begins and ends in the unlisted data.
25
+ """
26
+
27
+ def __init__(self, ends: Sequence[int], names: Optional[Sequence[str]] = None, validate: bool = True):
28
+ """Initialize a Partitioning object.
29
+
30
+ Args:
31
+ ends:
32
+ Sequence of ending positions for each partition.
33
+
34
+ names:
35
+ Optional names for each partition.
36
+
37
+ validate:
38
+ Internal use only.
39
+ """
40
+ self._ends = np.array(ends, dtype=np.int64)
41
+
42
+ # Calculate starts from ends
43
+ self._starts = np.zeros_like(self._ends)
44
+ if len(self._ends) > 0:
45
+ self._starts[1:] = self._ends[:-1]
46
+
47
+ self._names = None
48
+ if names is not None:
49
+ self._names = ut.NamedList(names)
50
+
51
+ if validate:
52
+ _validate_names(names, len(ends))
53
+
54
+ @classmethod
55
+ def from_lengths(cls, lengths: Sequence[int], names: Optional[Sequence[str]] = None) -> "Partitioning":
56
+ """Create a Partitioning from a sequence of lengths.
57
+
58
+ Args:
59
+ lengths:
60
+ Sequence of partition lengths.
61
+
62
+ names:
63
+ Optional names for each partition.
64
+
65
+ Returns:
66
+ A new Partitioning object.
67
+ """
68
+ ends = np.cumsum(lengths)
69
+ return cls(ends, names)
70
+
71
+ @classmethod
72
+ def from_list(cls, lst: List, names: Optional[Sequence[str]] = None) -> "Partitioning":
73
+ """Create a Partitioning from a list by using the lengths of each element.
74
+
75
+ Args:
76
+ lst:
77
+ A list to create partitioning from.
78
+
79
+ names:
80
+ Optional names for each partition.
81
+
82
+ Returns:
83
+ A new Partitioning object.
84
+ """
85
+ lengths = [len(item) if hasattr(item, "__len__") else 1 for item in lst]
86
+ return cls.from_lengths(lengths, names)
87
+
88
+ def _define_output(self, in_place: bool = False) -> "Partitioning":
89
+ if in_place is True:
90
+ return self
91
+ else:
92
+ return self.__copy__()
93
+
94
+ #########################
95
+ ######>> Copying <<######
96
+ #########################
97
+
98
+ def __deepcopy__(self, memo=None, _nil=[]):
99
+ """
100
+ Returns:
101
+ A deep copy of the current ``Partitioning``.
102
+ """
103
+ from copy import deepcopy
104
+
105
+ _ends_copy = deepcopy(self._ends)
106
+ _names_copy = deepcopy(self._names)
107
+
108
+ current_class_const = type(self)
109
+ return current_class_const(
110
+ ends=_ends_copy,
111
+ names=_names_copy,
112
+ )
113
+
114
+ def __copy__(self):
115
+ """
116
+ Returns:
117
+ A shallow copy of the current ``Partitioning``.
118
+ """
119
+ current_class_const = type(self)
120
+ return current_class_const(
121
+ ends=self._ends,
122
+ names=self._names,
123
+ )
124
+
125
+ def copy(self):
126
+ """Alias for :py:meth:`~__copy__`."""
127
+ return self.__copy__()
128
+
129
+ ######################################
130
+ ######>> length and iterators <<######
131
+ ######################################
132
+
133
+ def __len__(self) -> int:
134
+ """Return the number of partitions."""
135
+ return len(self._ends)
136
+
137
+ def get_nobj(self) -> int:
138
+ """Return the total number of objects across all partitions."""
139
+ return self._ends[-1] if len(self._ends) > 0 else 0
140
+
141
+ def nobj(self) -> int:
142
+ """Alias for :py:attr:`~get_nobj`."""
143
+ return self.get_nobj()
144
+
145
+ def get_element_lengths(self) -> np.ndarray:
146
+ """Return the lengths of each partition."""
147
+ return self._ends - self._starts
148
+
149
+ def element_lengths(self) -> int:
150
+ """Alias for :py:attr:`~get_element_lengths`."""
151
+ return self.get_element_lengths()
152
+
153
+ ##########################
154
+ ######>> Printing <<######
155
+ ##########################
156
+
157
+ def __repr__(self) -> str:
158
+ """
159
+ Returns:
160
+ A string representation.
161
+ """
162
+ output = f"{type(self).__name__}(number_of_elements={len(self)}"
163
+
164
+ if self._names is not None:
165
+ output += ", names=" + ut.print_truncated_list(self._names)
166
+
167
+ output += ")"
168
+ return output
169
+
170
+ def __str__(self) -> str:
171
+ """
172
+ Returns:
173
+ A pretty-printed string containing the contents of this object.
174
+ """
175
+ output = f"class: {type(self).__name__}\n"
176
+
177
+ output += f"num of elements: ({len(self)})\n"
178
+
179
+ output += f"names({0 if self._names is None else len(self._names)}): {' ' if self._names is None else ut.print_truncated_list(self._names)}\n"
180
+
181
+ return output
182
+
183
+ ##########################
184
+ ######>> accessors <<#####
185
+ ##########################
186
+
187
+ def get_partition_range(self, i: int) -> tuple:
188
+ """Get the start and end indices for partition ``i``."""
189
+ if i < 0 or i >= len(self):
190
+ raise IndexError(f"Partition index {i} out of range.")
191
+ return (self._starts[i], self._ends[i])
192
+
193
+ def __getitem__(self, key: Union[int, slice]) -> Union[tuple, List[tuple]]:
194
+ """Get partition range(s) by index or slice.
195
+
196
+ Args:
197
+ key:
198
+ Integer index or slice.
199
+
200
+ Returns:
201
+ Tuple of (start, end) or list of such tuples.
202
+ """
203
+ if isinstance(key, int):
204
+ return self.get_partition_range(key)
205
+ elif isinstance(key, slice):
206
+ indices = range(*key.indices(len(self)))
207
+ return [self.get_partition_range(i) for i in indices]
208
+ else:
209
+ raise TypeError("Index must be 'int' or 'slice'.")
210
+
211
+ ######################
212
+ ######>> names <<#####
213
+ ######################
214
+
215
+ def get_names(self) -> Optional[ut.NamedList]:
216
+ """Return the names of each partition."""
217
+ return self._names
218
+
219
+ def set_names(self, names: Optional[List[str]], in_place: bool = False) -> "Partitioning":
220
+ """Set the names of list elements.
221
+
222
+ Args:
223
+ names:
224
+ New names, same as the number of elements.
225
+
226
+ May be `None` to remove row names.
227
+
228
+ in_place:
229
+ Whether to modify the ``Partitioning`` in place.
230
+
231
+ Returns:
232
+ A modified ``Partitioning`` object, either as a copy of the original
233
+ or as a reference to the (in-place-modified) original.
234
+ """
235
+ if names is not None and not isinstance(names, ut.Names):
236
+ names = ut.Names(names)
237
+
238
+ _validate_names(names, len(self._ends))
239
+
240
+ output = self._define_output(in_place)
241
+ output._names = names
242
+ return output
243
+
244
+ @property
245
+ def names(self) -> Optional[ut.Names]:
246
+ """Alias for :py:attr:`~get_names`, provided for back-compatibility."""
247
+ return self.get_names()
248
+
249
+ @names.setter
250
+ def names(self, names: Optional[List[str]]):
251
+ """Alias for :py:meth:`~set_names` with ``in_place = True``.
252
+
253
+ As this mutates the original object, a warning is raised.
254
+ """
255
+ warn(
256
+ "Setting property 'row_names' is an in-place operation, use 'set_names' instead",
257
+ UserWarning,
258
+ )
259
+ self.set_names(names, in_place=True)
260
+
261
+ #####################
262
+ ######>> ends <<#####
263
+ #####################
264
+
265
+ def get_ends(self) -> Optional[ut.NamedList]:
266
+ """Return the names of each partition."""
267
+ return self._ends
268
+
269
+ @property
270
+ def ends(self) -> Optional[ut.Names]:
271
+ """Alias for :py:attr:`~get_ends`, provided for back-compatibility."""
272
+ return self.get_ends()
273
+
274
+ #######################
275
+ ######>> starts <<#####
276
+ #######################
277
+
278
+ def get_starts(self) -> Optional[ut.NamedList]:
279
+ """Return the starts of each partition."""
280
+ return self._starts
281
+
282
+ @property
283
+ def starts(self) -> Optional[ut.Names]:
284
+ """Alias for :py:attr:`~get_starts`, provided for back-compatibility."""
285
+ return self.get_starts()
@@ -0,0 +1,94 @@
1
+ Metadata-Version: 2.4
2
+ Name: compressed-lists
3
+ Version: 0.1.0
4
+ Summary: Memory-efficient container for list-like objects
5
+ Home-page: https://github.com/biocpy/compressed-lists
6
+ Author: Jayaram Kancherla
7
+ Author-email: jayaram.kancherla@gmail.com
8
+ License: MIT
9
+ Project-URL: Documentation, https://github.com/biocpy/compressed-lists
10
+ Platform: any
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Programming Language :: Python
13
+ Requires-Python: >=3.9
14
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
15
+ License-File: LICENSE.txt
16
+ Requires-Dist: importlib-metadata; python_version < "3.8"
17
+ Requires-Dist: biocutils
18
+ Provides-Extra: testing
19
+ Requires-Dist: setuptools; extra == "testing"
20
+ Requires-Dist: pytest; extra == "testing"
21
+ Requires-Dist: pytest-cov; extra == "testing"
22
+ Dynamic: license-file
23
+
24
+ [![PyPI-Server](https://img.shields.io/pypi/v/compressed-lists.svg)](https://pypi.org/project/compressed-lists/)
25
+ ![Unit tests](https://github.com/BiocPy/compressed-lists/actions/workflows/run-tests.yml/badge.svg)
26
+
27
+ # CompressedList Implementation in Python
28
+
29
+ A Python implementation of the `CompressedList` class from R/Bioconductor for memory-efficient list-like objects.
30
+
31
+ `CompressedList` is a memory-efficient container for list-like objects. Instead of storing each list element separately, it concatenates all elements into a single vector-like object and maintains information about where each original element begins and ends. This approach is significantly more memory-efficient than standard lists, especially when dealing with many list elements.
32
+
33
+ ## Install
34
+
35
+ To get started, install the package from [PyPI](https://pypi.org/project/compressed-lists/)
36
+
37
+ ```bash
38
+ pip install compressed-lists
39
+ ```
40
+
41
+ ## Usage
42
+
43
+
44
+ ```py
45
+ from compressed_lists import CompressedIntegerList, CompressedStringList
46
+
47
+ # Create a CompressedIntegerList
48
+ int_data = [[1, 2, 3], [4, 5], [6, 7, 8, 9]]
49
+ names = ["A", "B", "C"]
50
+ int_list = CompressedIntegerList.from_list(int_data, names)
51
+
52
+ # Access elements
53
+ print(int_list[0]) # [1, 2, 3]
54
+ print(int_list["B"]) # [4, 5]
55
+ print(int_list[1:3]) # Slice of elements
56
+
57
+ # Apply a function to each element
58
+ squared = int_list.lapply(lambda x: [i**2 for i in x])
59
+ print(squared[0]) # [1, 4, 9]
60
+
61
+ # Convert to a regular Python list
62
+ regular_list = int_list.to_list()
63
+
64
+ # Create a CompressedStringList
65
+ char_data = [["apple", "banana"], ["cherry", "date", "elderberry"], ["fig"]]
66
+ char_list = CompressedStringList.from_list(char_data)
67
+ ```
68
+
69
+ ### Partitioning
70
+
71
+ The `Partitioning` class handles the information about where each element begins and ends in the concatenated data. It allows for efficient extraction of elements without storing each element separately.
72
+
73
+ ```python
74
+ from compressed_lists import Partitioning
75
+
76
+ # Create partitioning from end positions
77
+ ends = [3, 5, 10]
78
+ names = ["A", "B", "C"]
79
+ part = Partitioning(ends, names)
80
+
81
+ # Get partition range for an element
82
+ start, end = part[1] # Returns (3, 5)
83
+ ```
84
+
85
+ > [!NOTE]
86
+ >
87
+ > Check out the [documentation](https://biocpy.github.io/compressed-lists) for extending CompressedLists to custom data types.
88
+
89
+ <!-- biocsetup-notes -->
90
+
91
+ ## Note
92
+
93
+ This project has been set up using [BiocSetup](https://github.com/biocpy/biocsetup)
94
+ and [PyScaffold](https://pyscaffold.org/).
@@ -0,0 +1,10 @@
1
+ compressed_lists/CompressedIntegerList.py,sha256=B4Eq3DgNeRbAL9Qpw3QQtwglMX4CT24dMSxGaIO12KE,2362
2
+ compressed_lists/CompressedList.py,sha256=7lIXMwW-451SgDMJ8pyMKi7kh1bjWDJb7n46ZkYQ8_c,18585
3
+ compressed_lists/CompressedStringList.py,sha256=pnz5C9WSeSkn9JgTclgYpf2ngUoaEfqw3lltf2LVMU0,2219
4
+ compressed_lists/__init__.py,sha256=u1mmk8IyiPBrzcT5j-78aQDsLW6Ssk73UrHysT1cLNg,779
5
+ compressed_lists/partition.py,sha256=tUeQY5CP3nb3n-iWMd5a4cJftxB3n7ptGKjmty0vbpM,8478
6
+ compressed_lists-0.1.0.dist-info/licenses/LICENSE.txt,sha256=qI2hRZobcUlj8gqFqXwqt522HeYyWvHLF00zCSZofHA,1084
7
+ compressed_lists-0.1.0.dist-info/METADATA,sha256=V5uNObHECSupJ_QumenbqO3BgbVXjh9hl2_HYYP7E28,3235
8
+ compressed_lists-0.1.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
9
+ compressed_lists-0.1.0.dist-info/top_level.txt,sha256=F89a770vqkz9CN7_voSVePD3jhhCwTQ51KalyJO4xpQ,17
10
+ compressed_lists-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (78.1.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Jayaram Kancherla
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 all
13
+ 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 THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ compressed_lists