ransacklib 0.1.10__py3-none-any.whl → 0.1.11__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.

Potentially problematic release.


This version of ransacklib might be problematic. Click here for more details.

ransack/__init__.py CHANGED
@@ -2,14 +2,14 @@ from .exceptions import EvaluationError, ParseError, RansackError, ShapeError
2
2
  from .parser import Parser
3
3
  from .transformer import Filter, get_values
4
4
 
5
- __version__ = "0.1.10"
5
+ __version__ = "0.1.11"
6
6
 
7
7
  __all__ = (
8
- "get_values",
9
- "Parser",
8
+ "EvaluationError",
10
9
  "Filter",
11
- "RansackError",
12
10
  "ParseError",
11
+ "Parser",
12
+ "RansackError",
13
13
  "ShapeError",
14
- "EvaluationError",
14
+ "get_values",
15
15
  )
ransack/exceptions.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """Custom exceptions for Ransack query parsing, evaluation, and shape validation."""
2
2
 
3
- from typing import Any, Optional
3
+ from typing import Any
4
4
 
5
5
 
6
6
  class RansackError(Exception):
@@ -14,11 +14,11 @@ class PositionInfoMixin:
14
14
  self,
15
15
  line: int,
16
16
  column: int,
17
- context: Optional[str] = None,
18
- start_pos: Optional[int] = None,
19
- end_pos: Optional[int] = None,
20
- end_line: Optional[int] = None,
21
- end_column: Optional[int] = None,
17
+ context: str | None = None,
18
+ start_pos: int | None = None,
19
+ end_pos: int | None = None,
20
+ end_line: int | None = None,
21
+ end_column: int | None = None,
22
22
  *args,
23
23
  **kwargs,
24
24
  ):
@@ -36,7 +36,9 @@ class ParseError(PositionInfoMixin, RansackError):
36
36
  """Raised when the input query cannot be parsed."""
37
37
 
38
38
  def __str__(self):
39
- return f"Syntax error at line {self.line}, column {self.column}.\n\n{self.context}" # noqa
39
+ return (
40
+ f"Syntax error at line {self.line}, column {self.column}.\n\n{self.context}"
41
+ )
40
42
 
41
43
 
42
44
  class ShapeError(PositionInfoMixin, RansackError):
ransack/operator.py CHANGED
@@ -1,16 +1,15 @@
1
- import re
2
- from collections.abc import MutableSequence
1
+ from collections.abc import Callable, MutableSequence
3
2
  from datetime import datetime, timedelta, timezone
4
3
  from functools import partial
5
4
  from numbers import Number
6
5
  from operator import add, eq, ge, gt, le, lt, mod, mul, sub, truediv
7
- from typing import Any, Callable, Union
6
+ from typing import Any
8
7
 
9
8
  from ipranges import IP4, IP6, IP4Net, IP4Range, IP6Net, IP6Range
10
9
 
11
10
  from .exceptions import OperatorNotFoundError
12
11
 
13
- IP = Union[IP4, IP4Net, IP4Range, IP6, IP6Net, IP6Range]
12
+ IP = IP4 | IP4Net | IP4Range | IP6 | IP6Net | IP6Range
14
13
  Operand = type | str
15
14
 
16
15
  commutative_operators = {"+", "*"}
@@ -189,7 +188,7 @@ def _comp_ip_ip(op: str, ip1: IP, ip2: IP) -> bool:
189
188
  case "<=":
190
189
  return ip1.low() <= ip2.high()
191
190
  case "=":
192
- return ip1.low() == ip2.low() and ip1.high() == ip2.high()
191
+ return ip1 in ip2 or ip2 in ip1
193
192
  case _:
194
193
  raise OperatorNotFoundError(op, ("ip", "ip"), (ip1, ip2))
195
194
 
@@ -349,10 +348,12 @@ def _get_comp_dict(op: str, comp: Callable) -> dict[tuple[Operand, Operand], Cal
349
348
  (datetime, tuple): partial(_comp_scalar_range, op),
350
349
  (tuple, Number): partial(_comp_range_scalar, op),
351
350
  (tuple, datetime): partial(_comp_range_scalar, op),
351
+ (str, MutableSequence): partial(_comp_scalar_list, op),
352
352
  ("ip", MutableSequence): partial(_comp_scalar_list, op),
353
353
  (Number, MutableSequence): partial(_comp_scalar_list, op),
354
354
  (datetime, MutableSequence): partial(_comp_scalar_list, op),
355
355
  (timedelta, MutableSequence): partial(_comp_scalar_list, op),
356
+ (MutableSequence, str): partial(_comp_list_scalar, op),
356
357
  (MutableSequence, "ip"): partial(_comp_list_scalar, op),
357
358
  (MutableSequence, Number): partial(_comp_list_scalar, op),
358
359
  (MutableSequence, datetime): partial(_comp_list_scalar, op),
@@ -421,7 +422,7 @@ _operator_map: dict[str, dict[tuple[Operand, Operand], Callable]] = {
421
422
  ">=": _get_comp_dict(">=", ge),
422
423
  "<=": _get_comp_dict("<=", le),
423
424
  "<": _get_comp_dict("<", lt),
424
- "=": _get_comp_dict("=", eq),
425
+ "=": _get_comp_dict("=", eq) | {(str, str): eq},
425
426
  ".": {
426
427
  (str, str): _concat,
427
428
  (MutableSequence, MutableSequence): _concat,
@@ -430,7 +431,6 @@ _operator_map: dict[str, dict[tuple[Operand, Operand], Callable]] = {
430
431
  (str, str): lambda value, pattern: pattern in value,
431
432
  (MutableSequence, str): lambda t, x: any(x in elem for elem in t),
432
433
  },
433
- "like": {(str, str): lambda data, pattern: re.match(pattern, data) is not None},
434
434
  "in": {
435
435
  ("ip", "ip"): lambda left, right: left in right,
436
436
  (str, MutableSequence): _in_scalar_list,
ransack/parser.py CHANGED
@@ -11,6 +11,8 @@ Classes:
11
11
  input data into an abstract syntax tree (AST).
12
12
  """
13
13
 
14
+ import os
15
+ from pathlib import Path
14
16
  from typing import Any, no_type_check
15
17
 
16
18
  from lark import Lark, Token, Tree
@@ -37,24 +39,23 @@ class Parser:
37
39
  ?start: or_expr
38
40
 
39
41
  ?or_expr: and_expr
40
- | or_expr ("or" | "||") and_expr -> or_op
42
+ | or_expr ("or"i | "||") and_expr -> or_op
41
43
 
42
44
  ?and_expr: not_expr
43
- | and_expr ("and" | "&&") not_expr -> and_op
45
+ | and_expr ("and"i | "&&") not_expr -> and_op
44
46
 
45
47
  ?not_expr: comparison
46
- | ("not" | "!") comparison -> not_op
48
+ | ("not"i | "!") comparison -> not_op
47
49
 
48
50
  ?comparison: sum
49
- | sum ">" sum -> gt
50
- | sum ">=" sum -> gte
51
- | sum "<" sum -> lt
52
- | sum "<=" sum -> lte
53
- | sum "=" sum -> any_eq
54
- | sum "==" sum -> eq
55
- | sum ("like" | "LIKE") sum -> like_op
56
- | sum ("in" | "IN") sum -> in_op
57
- | sum ("contains" | "CONTAINS") sum -> contains_op
51
+ | sum ">" sum -> gt
52
+ | sum ">=" sum -> gte
53
+ | sum "<" sum -> lt
54
+ | sum "<=" sum -> lte
55
+ | sum "=" sum -> any_eq
56
+ | sum "==" sum -> eq
57
+ | sum "in"i sum -> in_op
58
+ | sum "contains"i sum -> contains_op
58
59
 
59
60
  ?sum: product
60
61
  | sum "+" product -> add
@@ -93,11 +94,11 @@ class Parser:
93
94
  | IPV6_RANGE -> ipv6_range
94
95
  | IPV6_CIDR -> ipv6_cidr
95
96
 
96
- datetime: DATE ("T" | "t")? TIME -> datetime_full
97
- | DATE -> datetime_only_date
97
+ datetime: DATE "T"i? TIME -> datetime_full
98
+ | DATE -> datetime_only_date
98
99
 
99
100
  function: FUNCTION [args] ")"
100
- args: atom ("," atom)*
101
+ args: or_expr ("," or_expr)*
101
102
 
102
103
  list: "[" [atom ("," atom)*] "]"
103
104
 
@@ -112,7 +113,7 @@ class Parser:
112
113
  TIME.2: /[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]+)?(?:[Zz]|(?:[+-][0-9]{2}:[0-9]{2}))?/
113
114
  TIMEDELTA.2: /([0-9]+[D|d])?[0-9]{2}:[0-9]{2}:[0-9]{2}/
114
115
  STRING: /"([^"]+)"|\'([^\']+)\'/
115
- VARIABLE: /\.?[_a-zA-Z][-_a-zA-Z0-9]*(?:\.?[a-zA-Z][-_a-zA-Z0-9]*)*/
116
+ VARIABLE: /\.?[_a-zA-Z][-_a-zA-Z0-9]*(?:\.?[_a-zA-Z][-_a-zA-Z0-9]*)*/
116
117
  FUNCTION.2: /[_a-zA-Z][_a-zA-Z0-9]*\(/
117
118
 
118
119
  %import common.WS
@@ -128,7 +129,19 @@ class Parser:
128
129
  be referenced in queries. Context variables override data variables unless
129
130
  the data variable is explicitly accessed with a leading dot.
130
131
  """
131
- self.parser = Lark(self.grammar, parser="lalr", propagate_positions=True)
132
+ # Determine the cache file of the grammar. By default, it's in the home
133
+ # directory. But when the environment variable is set, use the value
134
+ # from that variable. This is useful for GitLab CI/CD.
135
+ cache_path = os.getenv("RANSACK_CACHE_PATH")
136
+ if not cache_path:
137
+ cache_path = str(Path("~/.cache/ransack_grammar_cache").expanduser())
138
+
139
+ self.parser = Lark(
140
+ self.grammar,
141
+ parser="lalr",
142
+ propagate_positions=True,
143
+ cache=cache_path,
144
+ )
132
145
  self.shaper = ExpressionTransformer(context)
133
146
 
134
147
  @no_type_check
ransack/transformer.py CHANGED
@@ -21,14 +21,14 @@ Classes:
21
21
  import re
22
22
  from collections.abc import Mapping, MutableSequence
23
23
  from datetime import datetime, timedelta, timezone
24
- from typing import Any, Optional, Tuple
24
+ from typing import Any, cast
25
25
 
26
26
  from ipranges import IP4, IP6, IP4Net, IP4Range, IP6Net, IP6Range
27
27
  from lark import Token, Transformer, Tree, v_args
28
28
  from lark.tree import Meta
29
29
  from lark.visitors import Interpreter
30
30
 
31
- from .exceptions import EvaluationError
31
+ from .exceptions import EvaluationError, RansackError
32
32
  from .function import predefined_functions
33
33
  from .operator import binary_operation
34
34
 
@@ -106,8 +106,8 @@ def _create_meta(token: Token | TokenWrapper) -> Meta:
106
106
 
107
107
 
108
108
  def _get_data_value(
109
- path: str, data: Optional[Mapping | MutableSequence]
110
- ) -> Tuple[Any, bool]:
109
+ path: str, data: Mapping | MutableSequence | None
110
+ ) -> tuple[Any, bool]:
111
111
  """
112
112
  Retrieves a value from the data structure (dictionary-like or list-like)
113
113
  based on a dotted path.
@@ -149,7 +149,7 @@ def _get_data_value(
149
149
  return None, False
150
150
  return _get_data_value(remaining_path, data[key])
151
151
 
152
- elif isinstance(data, MutableSequence):
152
+ if isinstance(data, MutableSequence):
153
153
  # Aggregate results from all list elements
154
154
  aggregated: Any = []
155
155
  for item in data:
@@ -166,7 +166,7 @@ def _get_data_value(
166
166
  return None, False
167
167
 
168
168
 
169
- def get_values(data: Optional[Mapping | MutableSequence], path: str) -> list:
169
+ def get_values(data: Mapping | MutableSequence | None, path: str) -> list:
170
170
  """
171
171
  Public API method to retrieve values from a nested data structure
172
172
  using a dotted path. Returns a flat list of values or an empty list
@@ -435,48 +435,6 @@ class ExpressionTransformer(Transformer):
435
435
  return Tree("var_from_context", [self.context[var]], _create_meta(var))
436
436
  return Tree("var_from_data", [TokenWrapper(var, var)], _create_meta(var))
437
437
 
438
- def range_op(self, start: TokenWrapper, end: TokenWrapper):
439
- """
440
- Transforms a range operation (start to end) into an appropriate range object.
441
-
442
- Parameters:
443
- start: The start token of the range.
444
- end: The end token of the range.
445
-
446
- Returns:
447
- A tree node wrapping a range object. If the tokens represent
448
- IP addresses, the range will be transformed into an IP4Range
449
- or IP6Range object, depending on the IP version; otherwise,
450
- it will return a generic range operation node.
451
- """
452
-
453
- def _create_ip_node(range_class) -> Tree[TokenWrapper]:
454
- """
455
- Helper function to create a tree node for IP ranges.
456
-
457
- Parameters:
458
- range_class: The class (IP4Range or IP6Range) to instantiate.
459
-
460
- Returns:
461
- A tree node wrapping the created IP range object.
462
- """
463
- return Tree(
464
- "ip",
465
- [
466
- TokenWrapper(
467
- _add_tokens(_start.token, _end.token),
468
- range_class(f"{_start.token}-{_end.token}"),
469
- )
470
- ],
471
- )
472
-
473
- _start, _end = start.children[0], end.children[0]
474
- if isinstance(_start.real_value, IP4) and isinstance(_end.real_value, IP4):
475
- return _create_ip_node(IP4Range)
476
- elif isinstance(_start.real_value, IP6) and isinstance(_end.real_value, IP6):
477
- return _create_ip_node(IP6Range)
478
- return Tree("range_op", [start, end])
479
-
480
438
 
481
439
  @v_args(inline=True)
482
440
  class Filter(Interpreter):
@@ -489,7 +447,7 @@ class Filter(Interpreter):
489
447
 
490
448
  data = None
491
449
 
492
- def eval(self, tree: Tree, data: Optional[dict] = None):
450
+ def eval(self, tree: Tree, data: dict | None = None):
493
451
  """
494
452
  Evaluates the given tree using the provided data.
495
453
 
@@ -501,6 +459,7 @@ class Filter(Interpreter):
501
459
  The result of evaluating the tree.
502
460
  """
503
461
  self.data = data if data is not None else {}
462
+ self.function_calls: dict[tuple[str, Tree | None], Any] = {}
504
463
 
505
464
  res = self.visit(tree)
506
465
 
@@ -570,6 +529,8 @@ class Filter(Interpreter):
570
529
  def _binary_operation(self, op: str, l_tree: Tree, r_tree: Tree) -> Any:
571
530
  try:
572
531
  return binary_operation(op, self.visit(l_tree), self.visit(r_tree))
532
+ except RansackError:
533
+ raise
573
534
  except Exception as e:
574
535
  raise EvaluationError(
575
536
  str(e),
@@ -659,19 +620,6 @@ class Filter(Interpreter):
659
620
  """
660
621
  return not self.visit(tree)
661
622
 
662
- def like_op(self, l_tree: Tree, r_tree: Tree) -> bool:
663
- """
664
- Checks if a string matches a regular expression pattern.
665
-
666
- Parameters:
667
- l_tree: Subtree representing the string to check.
668
- r_tree: Subtree representing the regex pattern.
669
-
670
- Returns:
671
- True if the string matches the pattern, otherwise False.
672
- """
673
- return self._binary_operation("like", l_tree, r_tree)
674
-
675
623
  def in_op(self, l_tree: Tree, r_tree: Tree) -> bool:
676
624
  """
677
625
  Checks if a value exists within a data structure.
@@ -762,18 +710,25 @@ class Filter(Interpreter):
762
710
  """
763
711
  return [self.visit(x) for x in data.children if x is not None]
764
712
 
765
- def range_op(self, l_tree: Tree, r_tree: Tree) -> tuple:
713
+ def range_op(self, l_tree: Tree, r_tree: Tree) -> tuple | IP4Range | IP6Range:
766
714
  """
767
- Creates a tuple representing a range.
715
+ Creates a tuple representing a range, or IP4Range/IP6Range.
768
716
 
769
717
  Parameters:
770
718
  l_tree: Subtree representing the start of the range.
771
719
  r_tree: Subtree representing the end of the range.
772
720
 
773
721
  Returns:
774
- A tuple containing the start and end values.
722
+ A tuple containing the start and end values or IP4Range/IP6Range
723
+ object for ip ranges.
775
724
  """
776
- return (self.visit(l_tree), self.visit(r_tree))
725
+ start = self.visit(l_tree)
726
+ end = self.visit(r_tree)
727
+ if isinstance(start, IP4) and isinstance(end, IP4):
728
+ return IP4Range(f"{start}-{end}")
729
+ if isinstance(start, IP6) and isinstance(end, IP6):
730
+ return IP6Range(f"{start}-{end}")
731
+ return (start, end)
777
732
 
778
733
  def exists_op(self, path: Token) -> bool:
779
734
  """
@@ -809,6 +764,9 @@ class Filter(Interpreter):
809
764
  """
810
765
  Calls a predefined function with the given arguments.
811
766
 
767
+ Caches the returned value for a given tuple - function name and
768
+ its arguments - so it can return the same value for subsequent calls.
769
+
812
770
  Parameters:
813
771
  name: The name of the function as a TokenWrapper.
814
772
  args: A Tree object containing function arguments or None.
@@ -819,12 +777,17 @@ class Filter(Interpreter):
819
777
  Raises:
820
778
  ValueError: If the function name is not found in predefined functions.
821
779
  """
822
- function_name = name.real_value
780
+ function_name = cast("str", name.real_value)
781
+ # If called again with the same args, return the cached value.
782
+ if (key := (function_name, args)) in self.function_calls:
783
+ return self.function_calls[key]
823
784
  if function_name in predefined_functions:
824
785
  try:
825
- return predefined_functions[function_name](
786
+ res = predefined_functions[function_name](
826
787
  *(self.visit(x) for x in (args.children if args else [])) # type: ignore
827
788
  )
789
+ self.function_calls[key] = res
790
+ return res
828
791
  except TypeError as e:
829
792
  raise EvaluationError(
830
793
  str(e),
@@ -834,7 +797,7 @@ class Filter(Interpreter):
834
797
  end_line=args.meta.end_line if args else name.end_line,
835
798
  end_column=args.meta.end_column if args else name.end_column,
836
799
  end_pos=args.meta.end_pos if args else name.end_pos,
837
- )
800
+ ) from None
838
801
  raise EvaluationError(
839
802
  f"Function '{name.real_value}' was not found.",
840
803
  line=name.line,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ransacklib
3
- Version: 0.1.10
3
+ Version: 0.1.11
4
4
  Summary: A modern, extensible language for manipulation with structured data
5
5
  Author-email: "Rajmund H. Hruška" <rajmund.hruska@cesnet.cz>
6
6
  License-Expression: MIT
@@ -11,7 +11,7 @@ Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
12
12
  Classifier: Intended Audience :: Developers
13
13
  Classifier: Operating System :: OS Independent
14
- Requires-Python: >=3.9
14
+ Requires-Python: >=3.10
15
15
  Description-Content-Type: text/x-rst
16
16
  License-File: LICENSE
17
17
  Requires-Dist: lark<1.3.0,>=1.2.2
@@ -19,14 +19,10 @@ Requires-Dist: ipranges<0.2,>=0.1.12
19
19
  Provides-Extra: dev
20
20
  Requires-Dist: pytest; extra == "dev"
21
21
  Requires-Dist: pytest-cov; extra == "dev"
22
- Requires-Dist: flake8; extra == "dev"
23
- Requires-Dist: flake8-pytest-style; extra == "dev"
24
- Requires-Dist: flake8-bugbear; extra == "dev"
25
22
  Requires-Dist: sphinx; extra == "dev"
26
23
  Requires-Dist: sphinx_rtd_theme; extra == "dev"
27
- Requires-Dist: black; extra == "dev"
28
24
  Requires-Dist: mypy; extra == "dev"
29
- Requires-Dist: isort; extra == "dev"
25
+ Requires-Dist: ruff; extra == "dev"
30
26
  Dynamic: license-file
31
27
 
32
28
  Welcome to ransack
@@ -0,0 +1,12 @@
1
+ ransack/__init__.py,sha256=l6a6YK8gm6OkFofd_d0VZp3Gxkb7IAmLlqa83oFZ5J8,313
2
+ ransack/exceptions.py,sha256=Ca-r6uQf7Ou-zsQwV3JDqsrEvRVx6Em2Y0ZwZwa1YkI,3732
3
+ ransack/function.py,sha256=u2WR0ZpwETR1MsZCpur680PExeWqH6phQLSTI0DloGI,1080
4
+ ransack/operator.py,sha256=82azEryAMDAwQmCdMPuxdEKFJY46DpuRi639ccuKlLM,16217
5
+ ransack/parser.py,sha256=RuL1whRgm5AhzXX0PrjNevti9fzPL60p_jeC7R15dUs,7230
6
+ ransack/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ ransack/transformer.py,sha256=WaIdcZ9xXIbzkOc2v92JdzbmMPKTjxC0ijcTKDjhSfk,27128
8
+ ransacklib-0.1.11.dist-info/licenses/LICENSE,sha256=mUbyu3f_FVAKt9MzuOjDggjsvV-WtwgSPlKPe_xKRBo,1074
9
+ ransacklib-0.1.11.dist-info/METADATA,sha256=kmcFprjAYOXOUVzoIO6I8PG-TK9z-40WgpeluDi0cRI,2715
10
+ ransacklib-0.1.11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
+ ransacklib-0.1.11.dist-info/top_level.txt,sha256=qVBXu4XHhFMobvS9sDB-r6EQB6hwLQUEiSvX0N7DelM,8
12
+ ransacklib-0.1.11.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.4.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,12 +0,0 @@
1
- ransack/__init__.py,sha256=QLp0go5FdQXrASgnBNaNKTxEEst54OBWYVG3KgTkVCU,313
2
- ransack/exceptions.py,sha256=GWV2XlgGYeuGr5I3gPL3vlmakacOg--lAtFCy5iQbsw,3741
3
- ransack/function.py,sha256=u2WR0ZpwETR1MsZCpur680PExeWqH6phQLSTI0DloGI,1080
4
- ransack/operator.py,sha256=hpdPNZdSxTqCl6il6j6PZnqMH6HniBCiAdhjqL-dbik,16201
5
- ransack/parser.py,sha256=aovOrJ4ZKcb0rSn8LJm2GiQZkn9JBaWssAfQ0pj9gUo,7010
6
- ransack/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- ransack/transformer.py,sha256=Ku9DjD3zy4Z1JJduwR_IqqLxmk9BvMct2mmxjzxNaR0,28272
8
- ransacklib-0.1.10.dist-info/licenses/LICENSE,sha256=mUbyu3f_FVAKt9MzuOjDggjsvV-WtwgSPlKPe_xKRBo,1074
9
- ransacklib-0.1.10.dist-info/METADATA,sha256=yDcMa86-7G-gME6hHei7GlIzeGRIdENF0NN4tDwhAkk,2887
10
- ransacklib-0.1.10.dist-info/WHEEL,sha256=DnLRTWE75wApRYVsjgc6wsVswC54sMSJhAEd4xhDpBk,91
11
- ransacklib-0.1.10.dist-info/top_level.txt,sha256=qVBXu4XHhFMobvS9sDB-r6EQB6hwLQUEiSvX0N7DelM,8
12
- ransacklib-0.1.10.dist-info/RECORD,,