data-filter-mcp 0.2.6__tar.gz → 0.2.7__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.
- {data_filter_mcp-0.2.6 → data_filter_mcp-0.2.7}/PKG-INFO +1 -1
- {data_filter_mcp-0.2.6 → data_filter_mcp-0.2.7}/data_filter_mcp/_version.py +3 -3
- {data_filter_mcp-0.2.6 → data_filter_mcp-0.2.7}/data_filter_mcp/validator.py +266 -18
- {data_filter_mcp-0.2.6 → data_filter_mcp-0.2.7}/data_filter_mcp.egg-info/PKG-INFO +1 -1
- {data_filter_mcp-0.2.6 → data_filter_mcp-0.2.7}/tests/test_validator.py +128 -0
- {data_filter_mcp-0.2.6 → data_filter_mcp-0.2.7}/.github/workflows/ci.yml +0 -0
- {data_filter_mcp-0.2.6 → data_filter_mcp-0.2.7}/.github/workflows/release.yml +0 -0
- {data_filter_mcp-0.2.6 → data_filter_mcp-0.2.7}/.gitignore +0 -0
- {data_filter_mcp-0.2.6 → data_filter_mcp-0.2.7}/LICENSE +0 -0
- {data_filter_mcp-0.2.6 → data_filter_mcp-0.2.7}/README.md +0 -0
- {data_filter_mcp-0.2.6 → data_filter_mcp-0.2.7}/data_filter_mcp/__init__.py +0 -0
- {data_filter_mcp-0.2.6 → data_filter_mcp-0.2.7}/data_filter_mcp/loaders/__init__.py +0 -0
- {data_filter_mcp-0.2.6 → data_filter_mcp-0.2.7}/data_filter_mcp/loaders/base.py +0 -0
- {data_filter_mcp-0.2.6 → data_filter_mcp-0.2.7}/data_filter_mcp/loaders/factory.py +0 -0
- {data_filter_mcp-0.2.6 → data_filter_mcp-0.2.7}/data_filter_mcp/loaders/json_loader.py +0 -0
- {data_filter_mcp-0.2.6 → data_filter_mcp-0.2.7}/data_filter_mcp/loaders/txt_loader.py +0 -0
- {data_filter_mcp-0.2.6 → data_filter_mcp-0.2.7}/data_filter_mcp/loaders/yaml_loader.py +0 -0
- {data_filter_mcp-0.2.6 → data_filter_mcp-0.2.7}/data_filter_mcp/models.py +0 -0
- {data_filter_mcp-0.2.6 → data_filter_mcp-0.2.7}/data_filter_mcp/registry.py +0 -0
- {data_filter_mcp-0.2.6 → data_filter_mcp-0.2.7}/data_filter_mcp/server.py +0 -0
- {data_filter_mcp-0.2.6 → data_filter_mcp-0.2.7}/data_filter_mcp.egg-info/SOURCES.txt +0 -0
- {data_filter_mcp-0.2.6 → data_filter_mcp-0.2.7}/data_filter_mcp.egg-info/dependency_links.txt +0 -0
- {data_filter_mcp-0.2.6 → data_filter_mcp-0.2.7}/data_filter_mcp.egg-info/entry_points.txt +0 -0
- {data_filter_mcp-0.2.6 → data_filter_mcp-0.2.7}/data_filter_mcp.egg-info/requires.txt +0 -0
- {data_filter_mcp-0.2.6 → data_filter_mcp-0.2.7}/data_filter_mcp.egg-info/top_level.txt +0 -0
- {data_filter_mcp-0.2.6 → data_filter_mcp-0.2.7}/pyproject.toml +0 -0
- {data_filter_mcp-0.2.6 → data_filter_mcp-0.2.7}/server.py +0 -0
- {data_filter_mcp-0.2.6 → data_filter_mcp-0.2.7}/setup.cfg +0 -0
- {data_filter_mcp-0.2.6 → data_filter_mcp-0.2.7}/tests/test_loaders.py +0 -0
- {data_filter_mcp-0.2.6 → data_filter_mcp-0.2.7}/tests/test_mcp_tools.py +0 -0
- {data_filter_mcp-0.2.6 → data_filter_mcp-0.2.7}/tests/test_registry.py +0 -0
- {data_filter_mcp-0.2.6 → data_filter_mcp-0.2.7}/tests/test_server_flow.py +0 -0
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.2.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 2,
|
|
21
|
+
__version__ = version = '0.2.7'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 7)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g7eb330227'
|
|
@@ -60,6 +60,39 @@ SAFE_DUNDER_ATTRIBUTES = {
|
|
|
60
60
|
"__name__",
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
SAFE_PROPERTY_READS = {
|
|
64
|
+
"broadcast_address",
|
|
65
|
+
"compressed",
|
|
66
|
+
"exploded",
|
|
67
|
+
"hostmask",
|
|
68
|
+
"ip",
|
|
69
|
+
"ipv4_mapped",
|
|
70
|
+
"ipv6_mapped",
|
|
71
|
+
"is_global",
|
|
72
|
+
"is_link_local",
|
|
73
|
+
"is_loopback",
|
|
74
|
+
"is_multicast",
|
|
75
|
+
"is_private",
|
|
76
|
+
"is_reserved",
|
|
77
|
+
"is_site_local",
|
|
78
|
+
"is_unspecified",
|
|
79
|
+
"max_prefixlen",
|
|
80
|
+
"netmask",
|
|
81
|
+
"network",
|
|
82
|
+
"network_address",
|
|
83
|
+
"num_addresses",
|
|
84
|
+
"packed",
|
|
85
|
+
"prefixlen",
|
|
86
|
+
"reverse_pointer",
|
|
87
|
+
"scope_id",
|
|
88
|
+
"sixtofour",
|
|
89
|
+
"teredo",
|
|
90
|
+
"version",
|
|
91
|
+
"with_hostmask",
|
|
92
|
+
"with_netmask",
|
|
93
|
+
"with_prefixlen",
|
|
94
|
+
}
|
|
95
|
+
|
|
63
96
|
SAFE_BUILTINS: dict[str, Any] = {
|
|
64
97
|
"all": all,
|
|
65
98
|
"any": any,
|
|
@@ -95,6 +128,7 @@ SAFE_METHODS = {
|
|
|
95
128
|
"SequenceMatcher",
|
|
96
129
|
"append",
|
|
97
130
|
"appendleft",
|
|
131
|
+
"address_exclude",
|
|
98
132
|
"abs",
|
|
99
133
|
"accumulate",
|
|
100
134
|
"add",
|
|
@@ -116,6 +150,7 @@ SAFE_METHODS = {
|
|
|
116
150
|
"chain",
|
|
117
151
|
"ChainMap",
|
|
118
152
|
"cmp_to_key",
|
|
153
|
+
"collapse_addresses",
|
|
119
154
|
"comb",
|
|
120
155
|
"combinations",
|
|
121
156
|
"combinations_with_replacement",
|
|
@@ -169,6 +204,7 @@ SAFE_METHODS = {
|
|
|
169
204
|
"gcd",
|
|
170
205
|
"ge",
|
|
171
206
|
"geometric_mean",
|
|
207
|
+
"get_mixed_type_key",
|
|
172
208
|
"get",
|
|
173
209
|
"get_close_matches",
|
|
174
210
|
"get_matching_blocks",
|
|
@@ -303,6 +339,7 @@ SAFE_METHODS = {
|
|
|
303
339
|
"subnets",
|
|
304
340
|
"subn",
|
|
305
341
|
"subtract",
|
|
342
|
+
"summarize_address_range",
|
|
306
343
|
"supernet",
|
|
307
344
|
"supernet_of",
|
|
308
345
|
"swapcase",
|
|
@@ -328,6 +365,8 @@ SAFE_METHODS = {
|
|
|
328
365
|
"utcnow",
|
|
329
366
|
"values",
|
|
330
367
|
"variance",
|
|
368
|
+
"v4_int_to_packed",
|
|
369
|
+
"v6_int_to_packed",
|
|
331
370
|
"weekday",
|
|
332
371
|
"wrap",
|
|
333
372
|
"wraps",
|
|
@@ -412,9 +451,66 @@ ALLOWED_NODE_TYPES = {
|
|
|
412
451
|
}
|
|
413
452
|
|
|
414
453
|
|
|
454
|
+
def _source_fragment(node: ast.AST | None, max_length: int = 200) -> str | None:
|
|
455
|
+
if node is None:
|
|
456
|
+
return None
|
|
457
|
+
try:
|
|
458
|
+
fragment = ast.unparse(node)
|
|
459
|
+
except Exception:
|
|
460
|
+
return None
|
|
461
|
+
if len(fragment) > max_length:
|
|
462
|
+
return f"{fragment[: max_length - 3]}..."
|
|
463
|
+
return fragment
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _node_line(node: ast.AST | None) -> int | None:
|
|
467
|
+
return getattr(node, "lineno", None)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _node_column(node: ast.AST | None) -> int | None:
|
|
471
|
+
return getattr(node, "col_offset", None)
|
|
472
|
+
|
|
473
|
+
|
|
415
474
|
class FilterValidationError(ValueError):
|
|
416
475
|
"""Raised when submitted filter code breaks the policy."""
|
|
417
476
|
|
|
477
|
+
def __init__(
|
|
478
|
+
self,
|
|
479
|
+
message: str,
|
|
480
|
+
*,
|
|
481
|
+
blocked_kind: str | None = None,
|
|
482
|
+
blocked_field: str | None = None,
|
|
483
|
+
blocked_value: str | None = None,
|
|
484
|
+
source_fragment: str | None = None,
|
|
485
|
+
source_line: int | None = None,
|
|
486
|
+
source_column: int | None = None,
|
|
487
|
+
) -> None:
|
|
488
|
+
self.blocked_kind = blocked_kind
|
|
489
|
+
self.blocked_field = blocked_field
|
|
490
|
+
self.blocked_value = blocked_value
|
|
491
|
+
self.source_fragment = source_fragment
|
|
492
|
+
self.source_line = source_line
|
|
493
|
+
self.source_column = source_column
|
|
494
|
+
|
|
495
|
+
details = []
|
|
496
|
+
if blocked_kind is not None:
|
|
497
|
+
details.append(f"blocked_kind={blocked_kind!r}")
|
|
498
|
+
if blocked_field is not None:
|
|
499
|
+
details.append(f"blocked_field={blocked_field!r}")
|
|
500
|
+
if blocked_value is not None:
|
|
501
|
+
details.append(f"blocked_value={blocked_value!r}")
|
|
502
|
+
if source_fragment is not None:
|
|
503
|
+
details.append(f"source_fragment={source_fragment!r}")
|
|
504
|
+
if source_line is not None:
|
|
505
|
+
details.append(f"source_line={source_line}")
|
|
506
|
+
if source_column is not None:
|
|
507
|
+
details.append(f"source_column={source_column}")
|
|
508
|
+
|
|
509
|
+
if details:
|
|
510
|
+
message = f"{message} ({', '.join(details)})"
|
|
511
|
+
|
|
512
|
+
super().__init__(message)
|
|
513
|
+
|
|
418
514
|
|
|
419
515
|
class FilterValidator(ast.NodeVisitor):
|
|
420
516
|
def __init__(self) -> None:
|
|
@@ -433,27 +529,66 @@ class FilterValidator(ast.NodeVisitor):
|
|
|
433
529
|
def generic_visit(self, node: ast.AST) -> None:
|
|
434
530
|
if type(node) not in ALLOWED_NODE_TYPES:
|
|
435
531
|
raise FilterValidationError(
|
|
436
|
-
f"{type(node).__name__} is not allowed in filter code"
|
|
532
|
+
f"{type(node).__name__} is not allowed in filter code",
|
|
533
|
+
blocked_kind="disallowed_node_type",
|
|
534
|
+
blocked_field="node_type",
|
|
535
|
+
blocked_value=type(node).__name__,
|
|
536
|
+
source_fragment=_source_fragment(node),
|
|
537
|
+
source_line=_node_line(node),
|
|
538
|
+
source_column=_node_column(node),
|
|
437
539
|
)
|
|
438
540
|
super().generic_visit(node)
|
|
439
541
|
|
|
440
542
|
def visit_Module(self, node: ast.Module) -> None:
|
|
441
543
|
if len(node.body) != 1 or not isinstance(node.body[0], ast.FunctionDef):
|
|
442
544
|
raise FilterValidationError(
|
|
443
|
-
"Filter code must contain exactly one top-level function definition"
|
|
545
|
+
"Filter code must contain exactly one top-level function definition",
|
|
546
|
+
blocked_kind="invalid_module_body",
|
|
547
|
+
blocked_field="top_level_nodes",
|
|
548
|
+
blocked_value=str(len(node.body)),
|
|
549
|
+
source_fragment=_source_fragment(node),
|
|
444
550
|
)
|
|
445
551
|
self.generic_visit(node)
|
|
446
552
|
|
|
447
553
|
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
|
448
554
|
if node.decorator_list:
|
|
449
|
-
raise FilterValidationError(
|
|
555
|
+
raise FilterValidationError(
|
|
556
|
+
"Decorators are not allowed",
|
|
557
|
+
blocked_kind="disallowed_decorator",
|
|
558
|
+
blocked_field="decorator",
|
|
559
|
+
blocked_value=", ".join(
|
|
560
|
+
fragment
|
|
561
|
+
for fragment in (
|
|
562
|
+
_source_fragment(decorator)
|
|
563
|
+
for decorator in node.decorator_list
|
|
564
|
+
)
|
|
565
|
+
if fragment is not None
|
|
566
|
+
),
|
|
567
|
+
source_fragment=_source_fragment(node),
|
|
568
|
+
source_line=_node_line(node),
|
|
569
|
+
source_column=_node_column(node),
|
|
570
|
+
)
|
|
450
571
|
if node.returns is not None:
|
|
451
|
-
raise FilterValidationError(
|
|
572
|
+
raise FilterValidationError(
|
|
573
|
+
"Return annotations are not allowed",
|
|
574
|
+
blocked_kind="disallowed_return_annotation",
|
|
575
|
+
blocked_field="return_annotation",
|
|
576
|
+
blocked_value=_source_fragment(node.returns),
|
|
577
|
+
source_fragment=_source_fragment(node),
|
|
578
|
+
source_line=_node_line(node),
|
|
579
|
+
source_column=_node_column(node),
|
|
580
|
+
)
|
|
452
581
|
|
|
453
582
|
if isinstance(self._parents.get(id(node)), ast.Module):
|
|
454
583
|
if node.name != "filter_item":
|
|
455
584
|
raise FilterValidationError(
|
|
456
|
-
"Filter function must be named filter_item"
|
|
585
|
+
"Filter function must be named filter_item",
|
|
586
|
+
blocked_kind="invalid_function_name",
|
|
587
|
+
blocked_field="function_name",
|
|
588
|
+
blocked_value=node.name,
|
|
589
|
+
source_fragment=_source_fragment(node),
|
|
590
|
+
source_line=_node_line(node),
|
|
591
|
+
source_column=_node_column(node),
|
|
457
592
|
)
|
|
458
593
|
|
|
459
594
|
args = node.args
|
|
@@ -467,25 +602,53 @@ class FilterValidator(ast.NodeVisitor):
|
|
|
467
602
|
or args.kw_defaults
|
|
468
603
|
):
|
|
469
604
|
raise FilterValidationError(
|
|
470
|
-
"filter_item must have exactly one parameter named data"
|
|
605
|
+
"filter_item must have exactly one parameter named data",
|
|
606
|
+
blocked_kind="invalid_function_signature",
|
|
607
|
+
blocked_field="parameters",
|
|
608
|
+
blocked_value=self._signature_summary(args),
|
|
609
|
+
source_fragment=_source_fragment(node.args),
|
|
610
|
+
source_line=_node_line(node),
|
|
611
|
+
source_column=_node_column(node),
|
|
471
612
|
)
|
|
472
613
|
else:
|
|
473
614
|
if node.name.startswith("__"):
|
|
474
615
|
raise FilterValidationError(
|
|
475
|
-
f"Nested function name is not allowed: {node.name}"
|
|
616
|
+
f"Nested function name is not allowed: {node.name}",
|
|
617
|
+
blocked_kind="disallowed_nested_function_name",
|
|
618
|
+
blocked_field="function_name",
|
|
619
|
+
blocked_value=node.name,
|
|
620
|
+
source_fragment=_source_fragment(node),
|
|
621
|
+
source_line=_node_line(node),
|
|
622
|
+
source_column=_node_column(node),
|
|
476
623
|
)
|
|
477
624
|
|
|
478
625
|
self.generic_visit(node)
|
|
479
626
|
|
|
480
627
|
def visit_Name(self, node: ast.Name) -> None:
|
|
481
628
|
if node.id in DISALLOWED_NAMES or node.id.startswith("__"):
|
|
482
|
-
raise FilterValidationError(
|
|
629
|
+
raise FilterValidationError(
|
|
630
|
+
f"Name is not allowed: {node.id}",
|
|
631
|
+
blocked_kind="disallowed_name",
|
|
632
|
+
blocked_field="name",
|
|
633
|
+
blocked_value=node.id,
|
|
634
|
+
source_fragment=_source_fragment(node),
|
|
635
|
+
source_line=_node_line(node),
|
|
636
|
+
source_column=_node_column(node),
|
|
637
|
+
)
|
|
483
638
|
self.generic_visit(node)
|
|
484
639
|
|
|
485
640
|
def visit_Attribute(self, node: ast.Attribute) -> None:
|
|
486
641
|
is_safe_dunder = node.attr in SAFE_DUNDER_ATTRIBUTES
|
|
487
642
|
if node.attr.startswith("_") and not is_safe_dunder:
|
|
488
|
-
raise FilterValidationError(
|
|
643
|
+
raise FilterValidationError(
|
|
644
|
+
f"Attribute access is not allowed: {node.attr}",
|
|
645
|
+
blocked_kind="disallowed_attribute",
|
|
646
|
+
blocked_field="attribute",
|
|
647
|
+
blocked_value=node.attr,
|
|
648
|
+
source_fragment=_source_fragment(node),
|
|
649
|
+
source_line=_node_line(node),
|
|
650
|
+
source_column=_node_column(node),
|
|
651
|
+
)
|
|
489
652
|
|
|
490
653
|
parent = self._parents.get(id(node))
|
|
491
654
|
if self._is_safe_attribute_read(node):
|
|
@@ -498,11 +661,28 @@ class FilterValidator(ast.NodeVisitor):
|
|
|
498
661
|
return
|
|
499
662
|
|
|
500
663
|
if not is_call_target:
|
|
664
|
+
if node.attr in SAFE_PROPERTY_READS:
|
|
665
|
+
self.visit(node.value)
|
|
666
|
+
return
|
|
501
667
|
raise FilterValidationError(
|
|
502
|
-
"Attribute access is restricted to approved method calls"
|
|
668
|
+
"Attribute access is restricted to approved method calls",
|
|
669
|
+
blocked_kind="disallowed_attribute_read",
|
|
670
|
+
blocked_field="attribute",
|
|
671
|
+
blocked_value=node.attr,
|
|
672
|
+
source_fragment=_source_fragment(node),
|
|
673
|
+
source_line=_node_line(node),
|
|
674
|
+
source_column=_node_column(node),
|
|
503
675
|
)
|
|
504
676
|
if node.attr not in SAFE_METHODS:
|
|
505
|
-
raise FilterValidationError(
|
|
677
|
+
raise FilterValidationError(
|
|
678
|
+
f"Method is not allowed: {node.attr}",
|
|
679
|
+
blocked_kind="disallowed_method",
|
|
680
|
+
blocked_field="method",
|
|
681
|
+
blocked_value=node.attr,
|
|
682
|
+
source_fragment=_source_fragment(node),
|
|
683
|
+
source_line=_node_line(node),
|
|
684
|
+
source_column=_node_column(node),
|
|
685
|
+
)
|
|
506
686
|
|
|
507
687
|
self.visit(node.value)
|
|
508
688
|
|
|
@@ -512,32 +692,87 @@ class FilterValidator(ast.NodeVisitor):
|
|
|
512
692
|
node.func.id not in SAFE_BUILTINS
|
|
513
693
|
and node.func.id not in self._defined_function_names
|
|
514
694
|
):
|
|
695
|
+
blocked_kind = (
|
|
696
|
+
"disallowed_name"
|
|
697
|
+
if node.func.id in DISALLOWED_NAMES
|
|
698
|
+
or node.func.id.startswith("__")
|
|
699
|
+
else "disallowed_function"
|
|
700
|
+
)
|
|
701
|
+
blocked_node = node.func if blocked_kind == "disallowed_name" else node
|
|
515
702
|
raise FilterValidationError(
|
|
516
|
-
f"Calling this function is not allowed: {node.func.id}"
|
|
703
|
+
f"Calling this function is not allowed: {node.func.id}",
|
|
704
|
+
blocked_kind=blocked_kind,
|
|
705
|
+
blocked_field=(
|
|
706
|
+
"name" if blocked_kind == "disallowed_name" else "function"
|
|
707
|
+
),
|
|
708
|
+
blocked_value=node.func.id,
|
|
709
|
+
source_fragment=_source_fragment(blocked_node),
|
|
710
|
+
source_line=_node_line(blocked_node),
|
|
711
|
+
source_column=_node_column(blocked_node),
|
|
517
712
|
)
|
|
518
713
|
self.visit(node.func)
|
|
519
714
|
elif isinstance(node.func, ast.Attribute):
|
|
520
715
|
self.visit(node.func)
|
|
521
716
|
else:
|
|
522
|
-
raise FilterValidationError(
|
|
717
|
+
raise FilterValidationError(
|
|
718
|
+
"Only whitelisted function calls are allowed",
|
|
719
|
+
blocked_kind="disallowed_call_target",
|
|
720
|
+
blocked_field="function",
|
|
721
|
+
blocked_value=_source_fragment(node.func) or type(node.func).__name__,
|
|
722
|
+
source_fragment=_source_fragment(node),
|
|
723
|
+
source_line=_node_line(node),
|
|
724
|
+
source_column=_node_column(node),
|
|
725
|
+
)
|
|
523
726
|
|
|
524
727
|
for arg in node.args:
|
|
525
728
|
if isinstance(arg, ast.Starred):
|
|
526
|
-
raise FilterValidationError(
|
|
729
|
+
raise FilterValidationError(
|
|
730
|
+
"Starred arguments are not allowed",
|
|
731
|
+
blocked_kind="disallowed_argument",
|
|
732
|
+
blocked_field="argument",
|
|
733
|
+
blocked_value=_source_fragment(arg),
|
|
734
|
+
source_fragment=_source_fragment(node),
|
|
735
|
+
source_line=_node_line(arg),
|
|
736
|
+
source_column=_node_column(arg),
|
|
737
|
+
)
|
|
527
738
|
self.visit(arg)
|
|
528
739
|
|
|
529
740
|
for keyword in node.keywords:
|
|
530
741
|
if keyword.arg is None:
|
|
531
742
|
raise FilterValidationError(
|
|
532
|
-
"Double-star keyword arguments are not allowed"
|
|
743
|
+
"Double-star keyword arguments are not allowed",
|
|
744
|
+
blocked_kind="disallowed_keyword_argument",
|
|
745
|
+
blocked_field="keyword",
|
|
746
|
+
blocked_value="**",
|
|
747
|
+
source_fragment=_source_fragment(node),
|
|
748
|
+
source_line=_node_line(keyword.value),
|
|
749
|
+
source_column=_node_column(keyword.value),
|
|
533
750
|
)
|
|
534
751
|
self.visit(keyword)
|
|
535
752
|
|
|
536
753
|
def visit_comprehension(self, node: ast.comprehension) -> None:
|
|
537
754
|
if node.is_async:
|
|
538
|
-
raise FilterValidationError(
|
|
755
|
+
raise FilterValidationError(
|
|
756
|
+
"Async comprehensions are not allowed",
|
|
757
|
+
blocked_kind="disallowed_async_comprehension",
|
|
758
|
+
blocked_field="is_async",
|
|
759
|
+
blocked_value=str(node.is_async),
|
|
760
|
+
source_fragment=_source_fragment(node),
|
|
761
|
+
)
|
|
539
762
|
self.generic_visit(node)
|
|
540
763
|
|
|
764
|
+
@staticmethod
|
|
765
|
+
def _signature_summary(args: ast.arguments) -> str:
|
|
766
|
+
positional = [arg.arg for arg in args.args]
|
|
767
|
+
keyword_only = [arg.arg for arg in args.kwonlyargs]
|
|
768
|
+
vararg = args.vararg.arg if args.vararg is not None else None
|
|
769
|
+
kwarg = args.kwarg.arg if args.kwarg is not None else None
|
|
770
|
+
return (
|
|
771
|
+
f"args={positional!r}, vararg={vararg!r}, "
|
|
772
|
+
f"kwonlyargs={keyword_only!r}, kwarg={kwarg!r}, "
|
|
773
|
+
f"defaults={len(args.defaults)}, kw_defaults={len(args.kw_defaults)}"
|
|
774
|
+
)
|
|
775
|
+
|
|
541
776
|
@staticmethod
|
|
542
777
|
def _build_parent_map(tree: ast.AST) -> dict[int, ast.AST]:
|
|
543
778
|
parents: dict[int, ast.AST] = {}
|
|
@@ -570,7 +805,15 @@ def _parse_filter(source_code: str) -> ast.Module:
|
|
|
570
805
|
try:
|
|
571
806
|
return ast.parse(source_code, mode="exec")
|
|
572
807
|
except SyntaxError as exc:
|
|
573
|
-
raise FilterValidationError(
|
|
808
|
+
raise FilterValidationError(
|
|
809
|
+
f"Invalid Python syntax: {exc.msg}",
|
|
810
|
+
blocked_kind="invalid_syntax",
|
|
811
|
+
blocked_field="syntax",
|
|
812
|
+
blocked_value=exc.msg,
|
|
813
|
+
source_fragment=(exc.text or "").strip() or None,
|
|
814
|
+
source_line=exc.lineno,
|
|
815
|
+
source_column=exc.offset,
|
|
816
|
+
) from exc
|
|
574
817
|
|
|
575
818
|
|
|
576
819
|
def _compile_tree(tree: ast.Module) -> CodeType:
|
|
@@ -590,6 +833,11 @@ def compile_filter(source_code: str):
|
|
|
590
833
|
|
|
591
834
|
filter_fn = execution_globals.get("filter_item")
|
|
592
835
|
if not callable(filter_fn):
|
|
593
|
-
raise FilterValidationError(
|
|
836
|
+
raise FilterValidationError(
|
|
837
|
+
"filter_item was not created successfully",
|
|
838
|
+
blocked_kind="missing_filter_function",
|
|
839
|
+
blocked_field="function_name",
|
|
840
|
+
blocked_value="filter_item",
|
|
841
|
+
)
|
|
594
842
|
|
|
595
843
|
return filter_fn
|
|
@@ -556,6 +556,61 @@ def test_compile_filter_allows_re_compile_chain() -> None:
|
|
|
556
556
|
"10.0.0.0/24",
|
|
557
557
|
"10.0.0.0/23",
|
|
558
558
|
),
|
|
559
|
+
(
|
|
560
|
+
"""
|
|
561
|
+
def filter_item(data):
|
|
562
|
+
addr = ipaddress.ip_address(data)
|
|
563
|
+
return str(addr.is_private)
|
|
564
|
+
""",
|
|
565
|
+
"192.168.0.1",
|
|
566
|
+
"True",
|
|
567
|
+
),
|
|
568
|
+
(
|
|
569
|
+
"""
|
|
570
|
+
def filter_item(data):
|
|
571
|
+
net = ipaddress.ip_network(data)
|
|
572
|
+
return str(net.network_address)
|
|
573
|
+
""",
|
|
574
|
+
"10.0.0.0/24",
|
|
575
|
+
"10.0.0.0",
|
|
576
|
+
),
|
|
577
|
+
(
|
|
578
|
+
"""
|
|
579
|
+
def filter_item(data):
|
|
580
|
+
net = ipaddress.ip_network(data)
|
|
581
|
+
return net.with_prefixlen
|
|
582
|
+
""",
|
|
583
|
+
"10.0.0.0/24",
|
|
584
|
+
"10.0.0.0/24",
|
|
585
|
+
),
|
|
586
|
+
(
|
|
587
|
+
"""
|
|
588
|
+
def filter_item(data):
|
|
589
|
+
outer = ipaddress.ip_network(data)
|
|
590
|
+
inner = ipaddress.ip_network("10.0.0.0/25")
|
|
591
|
+
return str(list(outer.address_exclude(inner))[0])
|
|
592
|
+
""",
|
|
593
|
+
"10.0.0.0/24",
|
|
594
|
+
"10.0.0.128/25",
|
|
595
|
+
),
|
|
596
|
+
(
|
|
597
|
+
"""
|
|
598
|
+
def filter_item(data):
|
|
599
|
+
first = ipaddress.ip_network("10.0.0.0/25")
|
|
600
|
+
second = ipaddress.ip_network("10.0.0.128/25")
|
|
601
|
+
return str(list(ipaddress.collapse_addresses([first, second]))[0])
|
|
602
|
+
""",
|
|
603
|
+
None,
|
|
604
|
+
"10.0.0.0/24",
|
|
605
|
+
),
|
|
606
|
+
(
|
|
607
|
+
"""
|
|
608
|
+
def filter_item(data):
|
|
609
|
+
return str(ipaddress.ip_address(data).version)
|
|
610
|
+
""",
|
|
611
|
+
"::1",
|
|
612
|
+
"6",
|
|
613
|
+
),
|
|
559
614
|
(
|
|
560
615
|
"""
|
|
561
616
|
def filter_item(data):
|
|
@@ -666,3 +721,76 @@ def test_compile_filter_rejects_methodcaller_in_json_object_hook() -> None:
|
|
|
666
721
|
FilterValidationError, match="Method is not allowed: methodcaller"
|
|
667
722
|
):
|
|
668
723
|
compile_filter(code)
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def test_validation_error_reports_blocked_name_details() -> None:
|
|
727
|
+
code = textwrap.dedent(
|
|
728
|
+
"""
|
|
729
|
+
def filter_item(data):
|
|
730
|
+
return eval(data)
|
|
731
|
+
"""
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
with pytest.raises(FilterValidationError, match="blocked_value='eval'") as exc_info:
|
|
735
|
+
compile_filter(code)
|
|
736
|
+
|
|
737
|
+
exc = exc_info.value
|
|
738
|
+
assert exc.blocked_kind == "disallowed_name"
|
|
739
|
+
assert exc.blocked_field == "name"
|
|
740
|
+
assert exc.blocked_value == "eval"
|
|
741
|
+
assert exc.source_fragment == "eval"
|
|
742
|
+
assert exc.source_line == 3
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
def test_validation_error_reports_blocked_attribute_read_details() -> None:
|
|
746
|
+
code = textwrap.dedent(
|
|
747
|
+
"""
|
|
748
|
+
def filter_item(data):
|
|
749
|
+
return str(data.secret)
|
|
750
|
+
"""
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
with pytest.raises(FilterValidationError, match="blocked_value='secret'") as exc_info:
|
|
754
|
+
compile_filter(code)
|
|
755
|
+
|
|
756
|
+
exc = exc_info.value
|
|
757
|
+
assert exc.blocked_kind == "disallowed_attribute_read"
|
|
758
|
+
assert exc.blocked_field == "attribute"
|
|
759
|
+
assert exc.blocked_value == "secret"
|
|
760
|
+
assert exc.source_fragment == "data.secret"
|
|
761
|
+
assert exc.source_line == 3
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def test_validation_error_reports_signature_details() -> None:
|
|
765
|
+
code = textwrap.dedent(
|
|
766
|
+
"""
|
|
767
|
+
def filter_item(item, extra):
|
|
768
|
+
return "bad"
|
|
769
|
+
"""
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
with pytest.raises(FilterValidationError, match="blocked_field='parameters'") as exc_info:
|
|
773
|
+
compile_filter(code)
|
|
774
|
+
|
|
775
|
+
exc = exc_info.value
|
|
776
|
+
assert exc.blocked_kind == "invalid_function_signature"
|
|
777
|
+
assert exc.blocked_field == "parameters"
|
|
778
|
+
assert "item" in (exc.blocked_value or "")
|
|
779
|
+
assert "extra" in (exc.blocked_value or "")
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
def test_validation_error_reports_double_star_keyword_details() -> None:
|
|
783
|
+
code = textwrap.dedent(
|
|
784
|
+
"""
|
|
785
|
+
def filter_item(data):
|
|
786
|
+
return dict(**data)
|
|
787
|
+
"""
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
with pytest.raises(FilterValidationError, match=r"blocked_value='\*\*'") as exc_info:
|
|
791
|
+
compile_filter(code)
|
|
792
|
+
|
|
793
|
+
exc = exc_info.value
|
|
794
|
+
assert exc.blocked_kind == "disallowed_keyword_argument"
|
|
795
|
+
assert exc.blocked_field == "keyword"
|
|
796
|
+
assert exc.blocked_value == "**"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{data_filter_mcp-0.2.6 → data_filter_mcp-0.2.7}/data_filter_mcp.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|