lkj 0.1.46__py3-none-any.whl → 0.1.48__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.
lkj/__init__.py CHANGED
@@ -15,7 +15,12 @@ from lkj.dicts import (
15
15
  merge_dicts, # Merge multiple dictionaries recursively
16
16
  compare_field_values, # Compare two dictionaries' values
17
17
  )
18
- from lkj.filesys import get_app_data_dir, get_watermarked_dir, enable_sourcing_from_file
18
+ from lkj.filesys import (
19
+ get_app_data_dir,
20
+ get_watermarked_dir,
21
+ enable_sourcing_from_file,
22
+ search_folder_fast, # Fast search for a term in files under a folder (uses rg command)
23
+ )
19
24
  from lkj.strings import (
20
25
  print_list, # Print a list in a nice format (or get a string to process yourself)
21
26
  FindReplaceTool, # Tool for finding and replacing substrings in a string
lkj/chunking.py CHANGED
@@ -3,29 +3,26 @@
3
3
  from itertools import zip_longest, chain, islice
4
4
 
5
5
  from typing import (
6
- Iterable,
7
6
  Union,
8
7
  Dict,
9
8
  List,
10
9
  Tuple,
11
- Mapping,
12
10
  TypeVar,
13
- Iterator,
14
- Callable,
15
11
  Optional,
16
12
  T,
17
13
  )
14
+ from collections.abc import Iterable, Mapping, Iterator, Callable
18
15
 
19
16
  KT = TypeVar("KT") # there's a typing.KT, but pylance won't allow me to use it!
20
17
  VT = TypeVar("VT") # there's a typing.VT, but pylance won't allow me to use it!
21
18
 
22
19
 
23
20
  def chunk_iterable(
24
- iterable: Union[Iterable[T], Mapping[KT, VT]],
21
+ iterable: Iterable[T] | Mapping[KT, VT],
25
22
  chk_size: int,
26
23
  *,
27
- chunk_type: Optional[Callable[..., Union[Iterable[T], Mapping[KT, VT]]]] = None,
28
- ) -> Iterator[Union[List[T], Tuple[T, ...], Dict[KT, VT]]]:
24
+ chunk_type: Callable[..., Iterable[T] | Mapping[KT, VT]] | None = None,
25
+ ) -> Iterator[list[T] | tuple[T, ...] | dict[KT, VT]]:
29
26
  """
30
27
  Divide an iterable into chunks/batches of a specific size.
31
28
 
@@ -78,7 +75,7 @@ def chunk_iterable(
78
75
 
79
76
  def chunker(
80
77
  a: Iterable[T], chk_size: int, *, include_tail: bool = True
81
- ) -> Iterator[Tuple[T, ...]]:
78
+ ) -> Iterator[tuple[T, ...]]:
82
79
  """
83
80
  Chunks an iterable into non-overlapping chunks of size `chk_size`.
84
81
 
lkj/dicts.py CHANGED
@@ -45,8 +45,8 @@ def exclusive_subdict(d, exclude):
45
45
  def truncate_dict_values(
46
46
  d: dict,
47
47
  *,
48
- max_list_size: Optional[int] = 2,
49
- max_string_size: Optional[int] = 66,
48
+ max_list_size: int | None = 2,
49
+ max_string_size: int | None = 66,
50
50
  middle_marker: str = "...",
51
51
  ) -> dict:
52
52
  """
@@ -106,7 +106,8 @@ def truncate_dict_values(
106
106
  return d
107
107
 
108
108
 
109
- from typing import Mapping, Callable, TypeVar, Iterable, Tuple
109
+ from typing import TypeVar, Tuple
110
+ from collections.abc import Mapping, Callable, Iterable
110
111
 
111
112
  KT = TypeVar("KT") # Key type
112
113
  VT = TypeVar("VT") # Value type
@@ -119,7 +120,7 @@ def merge_dicts(
119
120
  *mappings: Mapping[KT, VT],
120
121
  recursive_condition: Callable[[VT], bool] = lambda v: isinstance(v, Mapping),
121
122
  conflict_resolver: Callable[[VT, VT], VT] = lambda x, y: y,
122
- mapping_constructor: Callable[[Iterable[Tuple[KT, VT]]], Mapping[KT, VT]] = dict,
123
+ mapping_constructor: Callable[[Iterable[tuple[KT, VT]]], Mapping[KT, VT]] = dict,
123
124
  ) -> Mapping[KT, VT]:
124
125
  """
125
126
  Merge multiple mappings into a single mapping, recursively if needed,
@@ -218,7 +219,8 @@ def merge_dicts(
218
219
 
219
220
 
220
221
  import operator
221
- from typing import Callable, Dict, Any
222
+ from typing import Dict, Any
223
+ from collections.abc import Callable
222
224
 
223
225
  Comparison = Any
224
226
  Comparator = Callable[[dict, dict], Comparison]
@@ -232,10 +234,10 @@ def compare_field_values(
232
234
  dict1,
233
235
  dict2,
234
236
  *,
235
- field_comparators: Dict[KT, Comparator] = {},
237
+ field_comparators: dict[KT, Comparator] = {},
236
238
  default_comparator: Comparator = operator.eq,
237
239
  aggregator: Callable[
238
- [Dict[KT, Comparison]], Any
240
+ [dict[KT, Comparison]], Any
239
241
  ] = lambda d: d, # lambda d: np.mean(list(d.values())),
240
242
  get_comparison_fields: Callable[[dict], Iterable[KT]] = _common_keys_list,
241
243
  ):
lkj/filesys.py CHANGED
@@ -1,7 +1,160 @@
1
1
  """File system utils"""
2
2
 
3
+ # -------------------------------------------------------------------------------------
4
+ # Search (on linux and macOS) using rg (ripgrep)
5
+ import subprocess
6
+ import json
7
+ import shutil
8
+ from typing import Callable, Any, Optional
9
+
10
+ # simple parser that returns list of dicts objects from a JSONL string
11
+ simple_jsonl_parser = lambda string: list(map(json.loads, string.splitlines()))
12
+
13
+ # --- Default Egress Function ---
14
+
15
+
16
+ def _ripgrep_json_parser(rg_output: str) -> list[dict]:
17
+ """
18
+ Parses the line-by-line JSON stream output from ripgrep (rg --json).
19
+
20
+ This output is NOT a single valid JSON object, but a stream of JSON lines.
21
+ We only care about 'match' entries for the results.
22
+ """
23
+ results = []
24
+
25
+ # ripgrep outputs one JSON object per line, so we iterate line by line
26
+ for line in rg_output.strip().split("\n"):
27
+ if not line:
28
+ continue
29
+
30
+ try:
31
+ data = json.loads(line)
32
+ except json.JSONDecodeError:
33
+ # Skip any lines that aren't valid JSON (shouldn't happen with --json)
34
+ continue
35
+
36
+ if data.get("type") == "match":
37
+ match_data = data.get("data", {})
38
+
39
+ # Extract key information from the match
40
+ result = {
41
+ "path": match_data.get("path", {}).get("text"),
42
+ "line_number": match_data.get("line_number"),
43
+ # The text of the matched line, decoded
44
+ "line_text": match_data.get("lines", {}).get("text"),
45
+ "submatches": [
46
+ {
47
+ "match_text": sub.get("match", {}).get("text"),
48
+ "start": sub.get("start"),
49
+ "end": sub.get("end"),
50
+ }
51
+ for sub in match_data.get("submatches", [])
52
+ ],
53
+ }
54
+ results.append(result)
55
+
56
+ return results
57
+
58
+
59
+ def search_folder_fast(
60
+ search_term: str,
61
+ path_to_search: str = ".",
62
+ *, # Enforce 'egress' as a keyword-only argument
63
+ egress: Optional[Callable[[str], Any]] = _ripgrep_json_parser,
64
+ ) -> Any:
65
+ """
66
+ Executes a fast, recursive search using ripgrep and processes the results.
67
+
68
+ :param search_term: The regex pattern or text string to search for.
69
+ :param path_to_search: The folder path to start searching from. Defaults to current directory.
70
+ :param egress: A callable function to process the raw ripgrep output string.
71
+ Defaults to a parser that returns a list of dictionaries for matches.
72
+ If set to None, it defaults to a lambda returning the raw output (string).
73
+ You can also give it simple_jsonl_parser to get a list of all JSON objects in the output.
74
+ :return: The output of the 'egress' function.
75
+
76
+ Example usage:
77
+ -------------
78
+ >>> results = search_folder_fast("my_function_name", path_to_search='/path/to/project') # doctest: +SKIP
79
+ >>> for match in results: # doctest: +SKIP
80
+ ... print(f"Found in {match['path']} at line {match['line_number']}: {match['line_text']}") # doctest: +SKIP
81
+
82
+
83
+ """
84
+
85
+ if shutil.which("rg") is None:
86
+ print("=" * 60)
87
+ print(
88
+ "🚨 Error: The 'rg' (ripgrep) command was not found in your system's PATH."
89
+ )
90
+ print("\nTo install ripgrep:")
91
+ print(" - Linux (Debian/Ubuntu): sudo apt install ripgrep")
92
+ print(" - macOS (Homebrew): brew install ripgrep")
93
+ print(" - Windows (Chocolatey): choco install ripgrep")
94
+ print(f"\nMore details: https://github.com/BurntSushi/ripgrep#installation")
95
+ print("=" * 60)
96
+ return None
97
+
98
+ # Standard ripgrep command with JSON output
99
+ # -i: case-insensitive, -r: recursive, -n: show line numbers
100
+ # --json: outputs machine-readable JSON format
101
+ # --color never: ensures no ANSI color codes in the output stream
102
+ command = [
103
+ "rg",
104
+ "-i",
105
+ "-r",
106
+ "-n",
107
+ "--json",
108
+ "--color",
109
+ "never",
110
+ search_term,
111
+ path_to_search,
112
+ ]
113
+
114
+ # Handle the default None case for egress (return raw string)
115
+ if egress is None:
116
+ egress = lambda x: x
117
+
118
+ try:
119
+ # Use text=True and capture_output=True for string output and capturing stdout/stderr
120
+ result = subprocess.run(command, capture_output=True, text=True, check=True)
121
+
122
+ # Call the egress function on the raw standard output
123
+ return egress(result.stdout)
124
+
125
+ except subprocess.CalledProcessError as e:
126
+ # ripgrep returns a non-zero code if no matches are found.
127
+ # This is expected behavior and should not be treated as an error
128
+ # unless stderr indicates a true issue.
129
+ # Check stderr for real errors
130
+ if e.stderr:
131
+ # Handle genuine errors like permissions or file issues
132
+ print(f"A genuine ripgrep error occurred: {e.stderr.strip()}")
133
+ return None
134
+
135
+ # If check=True and return code is 1, it usually means "no matches found."
136
+ # If there's no stderr, return the egress of an empty string (or handle as no results)
137
+ return egress("")
138
+
139
+ except FileNotFoundError:
140
+ print(
141
+ "Error: ripgrep ('rg') command not found. Please ensure it is installed and in your PATH."
142
+ )
143
+ return None
144
+
145
+
146
+ # Example usage will require 'rg' installed and a file system to search
147
+ # For demonstration purposes, assume 'rg' is installed and you are searching for 'wrap_kvs'
148
+ # search_results = search_folder_fast("wrap_kvs", path_to_search='/path/to/your/project')
149
+ #
150
+ # print(search_results)
151
+
152
+ # -------------------------------------------------------------------------------------
153
+ # General utils for file system operations
154
+
3
155
  import os
4
- from typing import Callable, Any
156
+ from typing import Any
157
+ from collections.abc import Callable
5
158
  from pathlib import Path
6
159
  from functools import wraps, partial
7
160
 
@@ -32,7 +185,7 @@ def enable_sourcing_from_file(func=None, *, write_output=False):
32
185
  if args and isinstance(args[0], str) and os.path.isfile(args[0]):
33
186
  file_path = args[0]
34
187
  # Read the file content
35
- with open(file_path, "r") as file:
188
+ with open(file_path) as file:
36
189
  file_content = file.read()
37
190
  # Call the function with the file content and other arguments
38
191
  new_args = (file_content,) + args[1:]
lkj/iterables.py CHANGED
@@ -1,6 +1,7 @@
1
1
  """Tools with iterables (dicts, lists, tuples, sets, etc.)."""
2
2
 
3
- from typing import Sequence, Mapping, KT, VT, Iterable, Iterable, NamedTuple
3
+ from typing import KT, VT, NamedTuple
4
+ from collections.abc import Sequence, Mapping, Iterable, Iterable
4
5
 
5
6
 
6
7
  class SetsComparisonResult(NamedTuple):
lkj/loggers.py CHANGED
@@ -1,6 +1,7 @@
1
1
  """Utils for logging."""
2
2
 
3
- from typing import Callable, Tuple, Any, Optional, Union, Iterable
3
+ from typing import Tuple, Any, Optional, Union
4
+ from collections.abc import Callable, Iterable
4
5
  from functools import partial, wraps
5
6
  from operator import attrgetter
6
7
 
@@ -9,7 +10,7 @@ from operator import attrgetter
9
10
  # TODO: Merge with wrap_text_with_exact_spacing
10
11
  # TODO: Add doctests for string
11
12
  def wrapped_print(
12
- items: Union[str, Iterable],
13
+ items: str | Iterable,
13
14
  sep=", ",
14
15
  max_width=80,
15
16
  *,
@@ -166,15 +167,15 @@ def clog(condition, *args, log_func=print, **kwargs):
166
167
  return log_func(*args, **kwargs)
167
168
 
168
169
 
169
- def _calling_name(func_name: str, args: Tuple, kwargs: dict) -> str:
170
+ def _calling_name(func_name: str, args: tuple, kwargs: dict) -> str:
170
171
  return f"Calling {func_name}..."
171
172
 
172
173
 
173
- def _done_calling_name(func_name: str, args: Tuple, kwargs: dict, result: Any) -> str:
174
+ def _done_calling_name(func_name: str, args: tuple, kwargs: dict, result: Any) -> str:
174
175
  return f".... Done calling {func_name}"
175
176
 
176
177
 
177
- def _always_log(func: Callable, args: Tuple, kwargs: dict) -> bool:
178
+ def _always_log(func: Callable, args: tuple, kwargs: dict) -> bool:
178
179
  """Return True no matter what"""
179
180
  return True
180
181
 
@@ -183,10 +184,10 @@ def log_calls(
183
184
  func: Callable = None,
184
185
  *,
185
186
  logger: Callable[[str], None] = print,
186
- ingress_msg: Callable[[str, Tuple, dict], str] = _calling_name,
187
- egress_msg: Callable[[str, Tuple, dict, Any], str] = _done_calling_name,
187
+ ingress_msg: Callable[[str, tuple, dict], str] = _calling_name,
188
+ egress_msg: Callable[[str, tuple, dict, Any], str] = _done_calling_name,
188
189
  func_name: Callable[[Callable], str] = attrgetter("__name__"),
189
- log_condition: Callable[[Callable, Tuple, dict], bool] = _always_log,
190
+ log_condition: Callable[[Callable, tuple, dict], bool] = _always_log,
190
191
  ) -> Callable:
191
192
  """
192
193
  Decorator that adds logging before and after the function's call.
@@ -337,10 +338,12 @@ log_calls.instance_flag_is_set = instance_flag_is_set
337
338
  # Error handling
338
339
 
339
340
 
340
- from typing import Callable, Tuple, Any
341
+ from typing import Tuple, Any
342
+ from collections.abc import Callable
341
343
  from dataclasses import dataclass
342
344
  import traceback
343
- from typing import Callable, Any, Tuple
345
+ from typing import Any, Tuple
346
+ from collections.abc import Callable
344
347
  from functools import partial, wraps
345
348
  from operator import attrgetter
346
349
 
@@ -371,7 +374,7 @@ def dflt_error_info_processor(
371
374
  def return_error_info_on_error(
372
375
  func,
373
376
  *,
374
- caught_error_types: Tuple[Exception] = (Exception,),
377
+ caught_error_types: tuple[Exception] = (Exception,),
375
378
  error_info_processor: Callable[[ErrorInfo], Any] = dflt_error_info_processor,
376
379
  ):
377
380
  """Decorator that returns traceback and local variables on error.
lkj/misc.py CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  from operator import attrgetter, ge, gt, le, lt
4
4
  from functools import partial
5
- from typing import Callable, T, Optional, Any
5
+ from typing import T, Optional, Any
6
+ from collections.abc import Callable
6
7
 
7
8
 
8
9
  def identity(x):
@@ -14,8 +15,8 @@ def value_in_interval(
14
15
  /,
15
16
  *,
16
17
  get_val: Callable[[Any], T] = identity,
17
- min_val: Optional[T] = None,
18
- max_val: Optional[T] = None,
18
+ min_val: T | None = None,
19
+ max_val: T | None = None,
19
20
  is_minimum: Callable[[T, T], bool] = ge,
20
21
  is_maximum: Callable[[T, T], bool] = lt,
21
22
  ):
lkj/strings.py CHANGED
@@ -22,7 +22,8 @@ These utilities are designed to make it easier to display, format, and manipulat
22
22
  """
23
23
 
24
24
  import re
25
- from typing import Iterable, Sequence, Callable, Optional, Any, Literal
25
+ from typing import Optional, Any, Literal
26
+ from collections.abc import Iterable, Sequence, Callable
26
27
  from functools import partial
27
28
 
28
29
 
@@ -327,7 +328,7 @@ def regex_based_substitution(replacements: dict, regex=None, s: str = None):
327
328
  )
328
329
 
329
330
 
330
- from typing import Callable, Iterable, Sequence
331
+ from collections.abc import Callable, Iterable, Sequence
331
332
 
332
333
 
333
334
  class TrieNode:
@@ -453,12 +454,13 @@ def unique_affixes(
453
454
  return affixes
454
455
 
455
456
 
456
- from typing import Union, Callable, Dict, Any
457
+ from typing import Union, Dict, Any
458
+ from collections.abc import Callable
457
459
 
458
460
  # A match is represented as a dictionary (keys like "start", "end", etc.)
459
461
  # and the replacement is either a static string or a callable that takes that
460
462
  # dictionary and returns a string.
461
- Replacement = Union[str, Callable[[Dict[str, Any]], str]]
463
+ Replacement = Union[str, Callable[[dict[str, Any]], str]]
462
464
 
463
465
 
464
466
  class FindReplaceTool:
@@ -633,21 +635,59 @@ class FindReplaceTool:
633
635
  print(highlight)
634
636
  print("-" * 40)
635
637
  else:
638
+ # In non-line mode, a naive snippet (characters around the match) can
639
+ # include newlines. If we simply print that snippet and then print the
640
+ # highlight line relative to the snippet start, the visual caret
641
+ # markers may appear under a different printed line. To avoid this,
642
+ # find the full line that contains the match and print the context as
643
+ # lines: preceding context lines, the matched line, then the highlight
644
+ # directly under the matched line, then the following context lines.
636
645
  snippet_radius = 20
637
646
  for idx, m in enumerate(self._matches):
638
647
  start, end = m["start"], m["end"]
639
- if self.line_mode:
640
- snippet_start = current_text.rfind("\n", 0, start) + 1
641
- else:
642
- snippet_start = max(0, start - snippet_radius)
643
- snippet_end = min(len(current_text), end + snippet_radius)
644
- snippet = current_text[snippet_start:snippet_end]
648
+ # Find the boundaries of the line containing the match
649
+ line_start = current_text.rfind("\n", 0, start) + 1
650
+ line_end = current_text.find("\n", end)
651
+ if line_end == -1:
652
+ line_end = len(current_text)
653
+
654
+ # Context window (characters) around the matched line
655
+ context_start = max(0, line_start - snippet_radius)
656
+ context_end = min(len(current_text), line_end + snippet_radius)
657
+ context_text = current_text[context_start:context_end]
658
+
659
+ # Split into lines while preserving newlines so output looks natural
660
+ context_lines = context_text.splitlines(True)
661
+
662
+ # Determine which line in context_lines contains the match
663
+ acc = 0
664
+ match_line_idx = 0
665
+ rel_pos_in_context = line_start - context_start
666
+ for i, line in enumerate(context_lines):
667
+ if acc + len(line) > rel_pos_in_context:
668
+ match_line_idx = i
669
+ break
670
+ acc += len(line)
671
+
645
672
  print(f"Match {idx} (around line {m['line_number']+1}):")
646
- print(snippet)
647
- highlight = " " * (start - snippet_start) + self.highlight_char * (
648
- end - start
649
- )
650
- print(highlight)
673
+ # Print each context line. For the match line, print a highlight
674
+ # line immediately after it so the caret markers line up under
675
+ # the matched text.
676
+ for i, line in enumerate(context_lines):
677
+ # Print the context line as-is (it may or may not contain a newline)
678
+ print(line, end="")
679
+ if i == match_line_idx:
680
+ # If the printed line did not end with a newline, ensure the
681
+ # caret highlight appears on the next line so it lines up
682
+ # visually beneath the matched characters.
683
+ if not line.endswith("\n"):
684
+ print()
685
+ # position of match within the printed line
686
+ pos_in_line = start - line_start
687
+ highlight = " " * pos_in_line + self.highlight_char * (
688
+ end - start
689
+ )
690
+ print(highlight)
651
691
  print("-" * 40)
652
692
 
653
693
  def replace_one(self, match_index: int, replacement: Replacement) -> None:
@@ -732,7 +772,7 @@ class FindReplaceTool:
732
772
 
733
773
 
734
774
  def print_list(
735
- items: Optional[Iterable[Any]] = None,
775
+ items: Iterable[Any] | None = None,
736
776
  *,
737
777
  style: Literal[
738
778
  "wrapped", "columns", "numbered", "bullet", "table", "compact"
@@ -741,7 +781,7 @@ def print_list(
741
781
  sep: str = ", ",
742
782
  line_prefix: str = "",
743
783
  items_per_line=None,
744
- show_count: Union[bool, Callable[[int], str]] = False,
784
+ show_count: bool | Callable[[int], str] = False,
745
785
  title=None,
746
786
  print_func=print,
747
787
  ):
lkj/tests/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Tests for lkj"""
@@ -0,0 +1,48 @@
1
+ import io
2
+ import sys
3
+ import re
4
+
5
+ from lkj.strings import FindReplaceTool
6
+
7
+
8
+ def capture_print(func, *args, **kwargs):
9
+ old_stdout = sys.stdout
10
+ try:
11
+ sys.stdout = io.StringIO()
12
+ func(*args, **kwargs)
13
+ return sys.stdout.getvalue()
14
+ finally:
15
+ sys.stdout = old_stdout
16
+
17
+
18
+ def test_find_and_print_matches_highlight_under_match():
19
+ text = "apple banana apple\nsome other line\n"
20
+ tool = FindReplaceTool(text, line_mode=False)
21
+ out = capture_print(tool.find_and_print_matches, r"apple")
22
+
23
+ # We expect for the first match that the matched line appears, then the
24
+ # highlight line directly under it, then following context lines. Ensure the
25
+ # highlight caret appears on its own line immediately after the matched line.
26
+ # Locate the first occurrence of the matched line and the caret line that follows.
27
+ lines = out.splitlines()
28
+
29
+ # Find the index of the line that contains the first printed snippet for match 0
30
+ # It should contain 'apple banana apple'
31
+ match0_idx = None
32
+ for i, line in enumerate(lines):
33
+ if "apple banana apple" in line:
34
+ # Ensure the next non-empty line is the highlight
35
+ match0_idx = i
36
+ break
37
+
38
+ assert match0_idx is not None, "Did not find the matched line in output"
39
+
40
+ # The next line should be the caret highlight (contains at least one '^')
41
+ assert any(
42
+ c == "^" for c in lines[match0_idx + 1]
43
+ ), "Highlight not directly under matched line"
44
+
45
+ # For completeness, ensure that the text 'some other line' appears after the caret
46
+ assert "some other line" in "\n".join(
47
+ lines[match0_idx + 2 :]
48
+ ), "Following context not printed after highlight"
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: lkj
3
- Version: 0.1.46
3
+ Version: 0.1.48
4
4
  Summary: A dump of homeless useful utils
5
5
  Home-page: https://github.com/thorwhalen/lkj
6
6
  Author: Thor Whalen
@@ -8,6 +8,8 @@ License: apache-2.0
8
8
  Platform: any
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
+ Provides-Extra: testing
12
+ Dynamic: license-file
11
13
 
12
14
  # lkj
13
15
 
@@ -0,0 +1,17 @@
1
+ lkj/__init__.py,sha256=z8IZi5ql-bCM0xRgcEERg48oi2SRkgX9qSZdyaKb62Y,7300
2
+ lkj/chunking.py,sha256=K7yakDuTgyn6t2rvFeDIWS5pO636BKGOe1spKk7v4Mc,3746
3
+ lkj/dicts.py,sha256=0Ow8AgMHx2Og7hAAkBjFd3f2YTBhRbSvZ9fO-xkGkpU,10430
4
+ lkj/filesys.py,sha256=hYWztDoJUvVcPMD5AxRyYKkTB-sy5hx6_RDw4OyRcos,14628
5
+ lkj/funcs.py,sha256=LXJlj3gMMsbD0t2gn2NZZ6mOqmW5bxM-94uGoYgrhzI,8930
6
+ lkj/importing.py,sha256=TcW3qUDmw7jqswpxXnksjlHkkbOJq70NbUk1ZyaafT0,6658
7
+ lkj/iterables.py,sha256=JSuTm7XpaTjIlv_9XZTrApLBohu5hRu7ZHi0wDT_ky0,2828
8
+ lkj/loggers.py,sha256=eCudnqFaQhWz95qZtB57DDhvhXYvwl1JVCVernMhE5c,13781
9
+ lkj/misc.py,sha256=RqJg4HgcT1ZZRBtxOppwYibvvLIyn6pNqI-WMg0uKgI,1457
10
+ lkj/strings.py,sha256=JukQ4pvLlwGajBEK0PXyqsumEFKm8i1rx6SgASz7tqo,43798
11
+ lkj/tests/__init__.py,sha256=kReYfWiyz1T79AuZAy5m4PIBj3Oj_Dlc0E-T8HVQ504,20
12
+ lkj/tests/test_strings.py,sha256=Ix54io8WqWmYTY4guBgZShtKabtjd5uVpdxzxwJkgNA,1707
13
+ lkj-0.1.48.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
14
+ lkj-0.1.48.dist-info/METADATA,sha256=qACdUbDx6cImMq9DmVvGqYiFFO2mxH3ImUwQfTGjY5U,8267
15
+ lkj-0.1.48.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
16
+ lkj-0.1.48.dist-info/top_level.txt,sha256=3DZOUwYmyurJFBXQCvCmEIVm8z2b42O5Sx3RDQyePfg,4
17
+ lkj-0.1.48.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.45.1)
2
+ Generator: setuptools (79.0.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,15 +0,0 @@
1
- lkj/__init__.py,sha256=IbBzGqZp9oXRt5oFyZwRFh9MHcFy8HW30S7Ox15RxEs,7191
2
- lkj/chunking.py,sha256=RpNdx5jEuO4mFg2qoTkD47iL35neBneuZ5xgQ_cBkiM,3755
3
- lkj/dicts.py,sha256=z2o7njvLNJkh1ZgSH-SLtz13SdW_YfUsTA1yTY-kVLE,10382
4
- lkj/filesys.py,sha256=NbWDuc848h8O42gwX7d9yNJkrWBgzSFnkoEdSRgvBAg,8883
5
- lkj/funcs.py,sha256=LXJlj3gMMsbD0t2gn2NZZ6mOqmW5bxM-94uGoYgrhzI,8930
6
- lkj/importing.py,sha256=TcW3qUDmw7jqswpxXnksjlHkkbOJq70NbUk1ZyaafT0,6658
7
- lkj/iterables.py,sha256=9jeO36w-IGiZryge7JKgXZOGZAgehUvhwKV3nHPcZWk,2801
8
- lkj/loggers.py,sha256=ImmBdacz89Lvb3dg_xI5jOct_44rSRv0hNI_CVehy60,13706
9
- lkj/misc.py,sha256=IZf05tkl0cgiMgBwCMH5cLSC59fRXwnemPRo8G0OxQg,1436
10
- lkj/strings.py,sha256=J3MD8sbXoaV7uACtue5v9QhAhFLjUCksf-cCTf9Ez5o,41509
11
- lkj-0.1.46.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
12
- lkj-0.1.46.dist-info/METADATA,sha256=Q7e2mCXrROS5o5HcDNEnsJO3WZQnW-AnR2-0eiOKLfY,8221
13
- lkj-0.1.46.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
14
- lkj-0.1.46.dist-info/top_level.txt,sha256=3DZOUwYmyurJFBXQCvCmEIVm8z2b42O5Sx3RDQyePfg,4
15
- lkj-0.1.46.dist-info/RECORD,,