pycarlo 0.10.210__py3-none-any.whl → 0.12.57__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 pycarlo might be problematic. Click here for more details.
- pycarlo/features/metadata/__init__.py +20 -3
- pycarlo/features/metadata/asset_allow_block_list.py +22 -0
- pycarlo/features/metadata/asset_filters_container.py +79 -0
- pycarlo/features/metadata/base_allow_block_list.py +137 -0
- pycarlo/features/metadata/metadata_allow_block_list.py +94 -0
- pycarlo/features/metadata/metadata_filters_container.py +25 -16
- pycarlo/lib/README.md +34 -2
- pycarlo/lib/schema.json +63285 -50245
- pycarlo/lib/schema.py +6090 -1654
- pycarlo/lib/types.py +68 -0
- {pycarlo-0.10.210.dist-info → pycarlo-0.12.57.dist-info}/METADATA +107 -36
- {pycarlo-0.10.210.dist-info → pycarlo-0.12.57.dist-info}/RECORD +15 -11
- {pycarlo-0.10.210.dist-info → pycarlo-0.12.57.dist-info}/WHEEL +1 -1
- pycarlo/features/metadata/allow_block_list.py +0 -159
- {pycarlo-0.10.210.dist-info → pycarlo-0.12.57.dist-info}/LICENSE +0 -0
- {pycarlo-0.10.210.dist-info → pycarlo-0.12.57.dist-info}/top_level.txt +0 -0
|
@@ -1,15 +1,32 @@
|
|
|
1
|
-
from pycarlo.features.metadata.
|
|
2
|
-
|
|
1
|
+
from pycarlo.features.metadata.asset_allow_block_list import AssetAllowBlockList
|
|
2
|
+
from pycarlo.features.metadata.asset_filters_container import AssetFiltersContainer
|
|
3
|
+
from pycarlo.features.metadata.base_allow_block_list import (
|
|
4
|
+
BaseAllowBlockList,
|
|
5
|
+
ComparisonType,
|
|
3
6
|
FilterEffectType,
|
|
7
|
+
FilterRule,
|
|
4
8
|
FilterType,
|
|
9
|
+
RuleEffect,
|
|
10
|
+
)
|
|
11
|
+
from pycarlo.features.metadata.metadata_allow_block_list import (
|
|
12
|
+
MetadataAllowBlockList,
|
|
5
13
|
MetadataFilter,
|
|
6
14
|
)
|
|
7
15
|
from pycarlo.features.metadata.metadata_filters_container import MetadataFiltersContainer
|
|
8
16
|
|
|
9
17
|
__all__ = [
|
|
18
|
+
# Base classes
|
|
19
|
+
"FilterRule",
|
|
20
|
+
"BaseAllowBlockList",
|
|
10
21
|
"FilterEffectType",
|
|
22
|
+
"RuleEffect",
|
|
11
23
|
"FilterType",
|
|
24
|
+
"ComparisonType",
|
|
25
|
+
# Metadata filtering classes
|
|
12
26
|
"MetadataFilter",
|
|
13
|
-
"
|
|
27
|
+
"MetadataAllowBlockList",
|
|
14
28
|
"MetadataFiltersContainer",
|
|
29
|
+
# Asset filtering classes
|
|
30
|
+
"AssetAllowBlockList",
|
|
31
|
+
"AssetFiltersContainer",
|
|
15
32
|
]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
|
|
4
|
+
from dataclasses_json import DataClassJsonMixin
|
|
5
|
+
|
|
6
|
+
from pycarlo.common import get_logger
|
|
7
|
+
from pycarlo.features.metadata.base_allow_block_list import BaseAllowBlockList, FilterRule
|
|
8
|
+
|
|
9
|
+
logger = get_logger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class AssetAllowBlockList(BaseAllowBlockList[FilterRule], DataClassJsonMixin):
|
|
14
|
+
# JSON deserialization fails without this ugly override
|
|
15
|
+
rules: Optional[List[FilterRule]] = field(default_factory=list)
|
|
16
|
+
|
|
17
|
+
asset_type: Optional[str] = None
|
|
18
|
+
|
|
19
|
+
def __post_init__(self):
|
|
20
|
+
# We can't remove the default value because of properties with defaults in the parent class.
|
|
21
|
+
if not self.asset_type:
|
|
22
|
+
raise ValueError("asset_type is required")
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Dict, List
|
|
3
|
+
|
|
4
|
+
from dataclasses_json import DataClassJsonMixin
|
|
5
|
+
|
|
6
|
+
from .asset_allow_block_list import AssetAllowBlockList
|
|
7
|
+
from .base_allow_block_list import FilterEffectType
|
|
8
|
+
|
|
9
|
+
# Mapping of resource types to their supported asset types for collection preferences.
|
|
10
|
+
# This is used for validating asset collection preferences.
|
|
11
|
+
# When support for filtering an asset type is implemented in the DC, it should be added here.
|
|
12
|
+
# The reason it is here instead of in Monolith, is so that it can be referenced by the CLI.
|
|
13
|
+
# The pycarlo version in CLI and monolith should be updated after updating this and releasing a
|
|
14
|
+
# new version.
|
|
15
|
+
ASSET_TYPE_ATTRIBUTES = {"tableau": {"project": ["name"], "workbook": ["name", "luid"]}}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class AssetFiltersContainer(DataClassJsonMixin):
|
|
20
|
+
"""
|
|
21
|
+
Simple container for asset filtering that focuses on in-memory filtering for REST APIs.
|
|
22
|
+
|
|
23
|
+
This class provides basic asset filtering functionality without SQL generation complexity.
|
|
24
|
+
It's designed for the initial phase where assets are collected via REST APIs rather than
|
|
25
|
+
SQL queries.
|
|
26
|
+
|
|
27
|
+
Example usage:
|
|
28
|
+
# Block all external assets
|
|
29
|
+
filters = AssetAllowBlockList(
|
|
30
|
+
filters=[AssetFilter(asset_type="external", effect=FilterEffectType.BLOCK)]
|
|
31
|
+
)
|
|
32
|
+
container = AssetFiltersContainer(asset_filters=filters)
|
|
33
|
+
|
|
34
|
+
# Check if an asset is blocked
|
|
35
|
+
is_blocked = container.is_asset_blocked("external", "my_table") # True
|
|
36
|
+
is_blocked = container.is_asset_blocked("table", "users") # False
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
asset_filters: List[AssetAllowBlockList] = field(default_factory=list)
|
|
40
|
+
|
|
41
|
+
def is_asset_type_filtered(self, asset_type: str) -> bool:
|
|
42
|
+
"""Returns True if any filters are configured for the given asset type."""
|
|
43
|
+
return bool(self._get_asset_filters(asset_type))
|
|
44
|
+
|
|
45
|
+
def is_asset_blocked(self, asset_type: str, attributes: Dict[str, str]) -> bool:
|
|
46
|
+
"""
|
|
47
|
+
Returns True if the specified asset is blocked by the current filters.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
asset_type: The type of asset (e.g., 'tableau_workbook_v2', 'jobs', 'power_bi_workspace')
|
|
51
|
+
attributes: A dictionary representing the attributes of the asset
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
True if the asset is blocked, False if it's allowed
|
|
55
|
+
"""
|
|
56
|
+
asset_filters = self._get_asset_filters(asset_type)
|
|
57
|
+
|
|
58
|
+
is_blocked = False
|
|
59
|
+
|
|
60
|
+
for asset_filter in asset_filters:
|
|
61
|
+
default_effect_matches = asset_filter.get_default_effect_rules(
|
|
62
|
+
lambda f: f.matches(force_regexp=False, **attributes)
|
|
63
|
+
)
|
|
64
|
+
if default_effect_matches:
|
|
65
|
+
is_blocked = asset_filter.default_effect == FilterEffectType.BLOCK
|
|
66
|
+
else:
|
|
67
|
+
other_effect_matches = asset_filter.get_other_effect_rules(
|
|
68
|
+
lambda f: f.matches(force_regexp=False, **attributes)
|
|
69
|
+
)
|
|
70
|
+
if other_effect_matches:
|
|
71
|
+
is_blocked = asset_filter.other_effect == FilterEffectType.BLOCK
|
|
72
|
+
else:
|
|
73
|
+
# No matches, use default effect
|
|
74
|
+
is_blocked = asset_filter.default_effect == FilterEffectType.BLOCK
|
|
75
|
+
|
|
76
|
+
return is_blocked
|
|
77
|
+
|
|
78
|
+
def _get_asset_filters(self, asset_type: str) -> List[AssetAllowBlockList]:
|
|
79
|
+
return [f for f in self.asset_filters if f.asset_type == asset_type]
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
import re
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Callable, Generic, List, Optional, TypeVar
|
|
5
|
+
|
|
6
|
+
from dataclasses_json import DataClassJsonMixin
|
|
7
|
+
|
|
8
|
+
from pycarlo.common import get_logger
|
|
9
|
+
|
|
10
|
+
logger = get_logger(__name__)
|
|
11
|
+
|
|
12
|
+
# For documentation and samples check the link below:
|
|
13
|
+
# https://www.notion.so/montecarlodata/Catalog-Schema-Filtering-59edd6eff7f74c94ab6bfca75d2e3ff1
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _exclude_none_values(value: Any) -> bool:
|
|
17
|
+
return value is None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FilterEffectType(enum.Enum):
|
|
21
|
+
BLOCK = "block"
|
|
22
|
+
ALLOW = "allow"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
RuleEffect = FilterEffectType
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class FilterType(enum.Enum):
|
|
29
|
+
EXACT_MATCH = "exact_match"
|
|
30
|
+
PREFIX = "prefix"
|
|
31
|
+
SUFFIX = "suffix"
|
|
32
|
+
SUBSTRING = "substring"
|
|
33
|
+
REGEXP = "regexp"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
ComparisonType = FilterType
|
|
37
|
+
|
|
38
|
+
# Type variable for the filter class
|
|
39
|
+
FilterRuleT = TypeVar("FilterRuleT", bound="FilterRule")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class RuleCondition(DataClassJsonMixin):
|
|
44
|
+
attribute_name: str
|
|
45
|
+
value: str
|
|
46
|
+
comparison_type: ComparisonType = ComparisonType.EXACT_MATCH
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class FilterRule(DataClassJsonMixin):
|
|
51
|
+
"""
|
|
52
|
+
Base class for all filter types. Provides common filtering logic that can be
|
|
53
|
+
shared between different filter implementations (e.g., metadata filters, asset filters).
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
conditions: Optional[List[RuleCondition]] = field(default_factory=list)
|
|
57
|
+
effect: RuleEffect = RuleEffect.BLOCK
|
|
58
|
+
|
|
59
|
+
def matches(self, force_regexp: bool = False, **kwargs: Any) -> bool:
|
|
60
|
+
"""
|
|
61
|
+
Returns True if all properties specified in kwargs match the conditions specified in
|
|
62
|
+
properties of the same name in this object.
|
|
63
|
+
If any of the conditions (for example self.field) is None, that condition will be matched.
|
|
64
|
+
"""
|
|
65
|
+
if not kwargs:
|
|
66
|
+
raise ValueError("At least one field needs to be specified for matching")
|
|
67
|
+
|
|
68
|
+
# kwargs must match the field names in this class, if any of them do not,
|
|
69
|
+
# invalidate the filter.
|
|
70
|
+
try:
|
|
71
|
+
return all(
|
|
72
|
+
condition.attribute_name not in kwargs
|
|
73
|
+
or self._match(
|
|
74
|
+
condition=condition,
|
|
75
|
+
value=kwargs.get(condition.attribute_name),
|
|
76
|
+
force_regexp=force_regexp,
|
|
77
|
+
)
|
|
78
|
+
for condition in self.conditions or []
|
|
79
|
+
)
|
|
80
|
+
except AttributeError:
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def _match(cls, condition: RuleCondition, value: Optional[str], force_regexp: bool) -> bool:
|
|
85
|
+
# Field not specified on this object, e.g. self.field=None, which matches everything
|
|
86
|
+
if value is None:
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
# The comparison is performed case-insensitive (check BaseFilter._safe_match)
|
|
90
|
+
# We can use LOWER here since it is part of standard SQL (like AND/OR/NOT), so including it
|
|
91
|
+
# here is a way to make sure that all comparisons are case-insensitive in the SQL sentences
|
|
92
|
+
# for all engines. Added option to not always LOWER since customers do have lower/upper case
|
|
93
|
+
# databases logged in MC
|
|
94
|
+
filter_value = condition.value.lower()
|
|
95
|
+
value = value.lower()
|
|
96
|
+
|
|
97
|
+
if force_regexp or condition.comparison_type == FilterType.REGEXP:
|
|
98
|
+
regexp = f"^{filter_value}$"
|
|
99
|
+
return re.match(regexp, value) is not None
|
|
100
|
+
elif condition.comparison_type == FilterType.PREFIX:
|
|
101
|
+
return value.startswith(filter_value)
|
|
102
|
+
elif condition.comparison_type == FilterType.SUFFIX:
|
|
103
|
+
return value.endswith(filter_value)
|
|
104
|
+
elif condition.comparison_type == FilterType.SUBSTRING:
|
|
105
|
+
return filter_value in value
|
|
106
|
+
else: # filter_type == FilterType.EXACT_MATCH
|
|
107
|
+
return filter_value == value
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass
|
|
111
|
+
class BaseAllowBlockList(Generic[FilterRuleT], DataClassJsonMixin):
|
|
112
|
+
rules: Optional[List[FilterRuleT]] = field(default_factory=list)
|
|
113
|
+
default_effect: RuleEffect = RuleEffect.ALLOW
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def other_effect(self) -> RuleEffect:
|
|
117
|
+
return RuleEffect.ALLOW if self.default_effect == RuleEffect.BLOCK else RuleEffect.BLOCK
|
|
118
|
+
|
|
119
|
+
def get_default_effect_rules(
|
|
120
|
+
self, condition: Optional[Callable[[FilterRuleT], bool]] = None
|
|
121
|
+
) -> List[FilterRuleT]:
|
|
122
|
+
return list(
|
|
123
|
+
filter(
|
|
124
|
+
lambda f: f.effect == self.default_effect and (condition is None or condition(f)),
|
|
125
|
+
self.rules or [],
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def get_other_effect_rules(
|
|
130
|
+
self, condition: Optional[Callable[[FilterRuleT], bool]] = None
|
|
131
|
+
) -> List[FilterRuleT]:
|
|
132
|
+
return list(
|
|
133
|
+
filter(
|
|
134
|
+
lambda f: f.effect != self.default_effect and (condition is None or condition(f)),
|
|
135
|
+
self.rules or [],
|
|
136
|
+
)
|
|
137
|
+
)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
|
|
4
|
+
from dataclasses_json import config, dataclass_json
|
|
5
|
+
|
|
6
|
+
from pycarlo.common import get_logger
|
|
7
|
+
from pycarlo.features.metadata.base_allow_block_list import (
|
|
8
|
+
BaseAllowBlockList,
|
|
9
|
+
ComparisonType,
|
|
10
|
+
FilterRule,
|
|
11
|
+
FilterType,
|
|
12
|
+
RuleCondition,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
logger = get_logger(__name__)
|
|
16
|
+
|
|
17
|
+
# For documentation and samples check the link below:
|
|
18
|
+
# https://www.notion.so/montecarlodata/Catalog-Schema-Filtering-59edd6eff7f74c94ab6bfca75d2e3ff1
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass_json
|
|
22
|
+
@dataclass
|
|
23
|
+
class MetadataFilter(FilterRule):
|
|
24
|
+
type: FilterType = FilterType.EXACT_MATCH
|
|
25
|
+
|
|
26
|
+
# we're using exclude=_exclude_none_values to prevent these properties to be serialized to json
|
|
27
|
+
# when None, to keep the json doc simpler
|
|
28
|
+
project: Optional[str] = field(metadata=config(exclude=lambda x: x is None), default=None)
|
|
29
|
+
dataset: Optional[str] = field(metadata=config(exclude=lambda x: x is None), default=None)
|
|
30
|
+
table_type: Optional[str] = field(metadata=config(exclude=lambda x: x is None), default=None)
|
|
31
|
+
table_name: Optional[str] = field(metadata=config(exclude=lambda x: x is None), default=None)
|
|
32
|
+
|
|
33
|
+
def __post_init__(self):
|
|
34
|
+
# For backwards compatibility, we now create a set of conditions based on the
|
|
35
|
+
# metadata-specific fields.
|
|
36
|
+
self.conditions = self.conditions or []
|
|
37
|
+
if self.table_name is not None:
|
|
38
|
+
is_target_field = self.filter_type_target_field() == "table_name"
|
|
39
|
+
condition = RuleCondition(
|
|
40
|
+
comparison_type=self.type if is_target_field else ComparisonType.EXACT_MATCH,
|
|
41
|
+
attribute_name="table_name",
|
|
42
|
+
value=self.table_name,
|
|
43
|
+
)
|
|
44
|
+
self.conditions.append(condition)
|
|
45
|
+
|
|
46
|
+
if self.dataset is not None:
|
|
47
|
+
is_target_field = self.filter_type_target_field() == "dataset"
|
|
48
|
+
condition = RuleCondition(
|
|
49
|
+
comparison_type=self.type if is_target_field else ComparisonType.EXACT_MATCH,
|
|
50
|
+
attribute_name="dataset",
|
|
51
|
+
value=self.dataset,
|
|
52
|
+
)
|
|
53
|
+
self.conditions.append(condition)
|
|
54
|
+
|
|
55
|
+
if self.project is not None:
|
|
56
|
+
is_target_field = self.filter_type_target_field() == "project"
|
|
57
|
+
condition = RuleCondition(
|
|
58
|
+
comparison_type=self.type if is_target_field else ComparisonType.EXACT_MATCH,
|
|
59
|
+
attribute_name="project",
|
|
60
|
+
value=self.project,
|
|
61
|
+
)
|
|
62
|
+
self.conditions.append(condition)
|
|
63
|
+
|
|
64
|
+
if self.table_type is not None:
|
|
65
|
+
condition = RuleCondition(
|
|
66
|
+
comparison_type=ComparisonType.EXACT_MATCH,
|
|
67
|
+
attribute_name="table_type",
|
|
68
|
+
value=self.table_type,
|
|
69
|
+
)
|
|
70
|
+
self.conditions.append(condition)
|
|
71
|
+
|
|
72
|
+
def filter_type_target_field(self) -> str:
|
|
73
|
+
"""
|
|
74
|
+
The field that is evaluated using filter type. Other fields should be
|
|
75
|
+
compared using exact match.
|
|
76
|
+
"""
|
|
77
|
+
if self.table_name is not None:
|
|
78
|
+
return "table_name"
|
|
79
|
+
if self.dataset is not None:
|
|
80
|
+
return "dataset"
|
|
81
|
+
if self.project is not None:
|
|
82
|
+
return "project"
|
|
83
|
+
|
|
84
|
+
logger.exception("Invalid filter, missing target values")
|
|
85
|
+
return ""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass_json
|
|
89
|
+
@dataclass
|
|
90
|
+
class MetadataAllowBlockList(BaseAllowBlockList[MetadataFilter]):
|
|
91
|
+
filters: List[MetadataFilter] = field(default_factory=list)
|
|
92
|
+
|
|
93
|
+
def __post_init__(self):
|
|
94
|
+
self.rules = self.filters
|
|
@@ -3,7 +3,12 @@ from typing import Any, Callable, Dict, Optional
|
|
|
3
3
|
|
|
4
4
|
from dataclasses_json import dataclass_json
|
|
5
5
|
|
|
6
|
-
from pycarlo.features.metadata import
|
|
6
|
+
from pycarlo.features.metadata import (
|
|
7
|
+
FilterEffectType,
|
|
8
|
+
FilterType,
|
|
9
|
+
MetadataAllowBlockList,
|
|
10
|
+
MetadataFilter,
|
|
11
|
+
)
|
|
7
12
|
|
|
8
13
|
|
|
9
14
|
@dataclass_json
|
|
@@ -62,7 +67,7 @@ class MetadataFiltersContainer:
|
|
|
62
67
|
- allowed: (project_3, dataset_1), (project_4, dataset_4)
|
|
63
68
|
"""
|
|
64
69
|
|
|
65
|
-
metadata_filters:
|
|
70
|
+
metadata_filters: MetadataAllowBlockList = field(default_factory=MetadataAllowBlockList)
|
|
66
71
|
|
|
67
72
|
@property
|
|
68
73
|
def is_metadata_filtered(self) -> bool:
|
|
@@ -129,7 +134,7 @@ class MetadataFiltersContainer:
|
|
|
129
134
|
|
|
130
135
|
@staticmethod
|
|
131
136
|
def _get_effect(
|
|
132
|
-
metadata_filters:
|
|
137
|
+
metadata_filters: MetadataAllowBlockList,
|
|
133
138
|
force_regexp: bool,
|
|
134
139
|
condition: Optional[Callable[[MetadataFilter], bool]] = None,
|
|
135
140
|
**kwargs: Any,
|
|
@@ -145,13 +150,13 @@ class MetadataFiltersContainer:
|
|
|
145
150
|
"""
|
|
146
151
|
if not metadata_filters.filters or any(
|
|
147
152
|
f.matches(force_regexp, **kwargs)
|
|
148
|
-
for f in metadata_filters.
|
|
153
|
+
for f in metadata_filters.get_default_effect_rules(condition=condition)
|
|
149
154
|
):
|
|
150
155
|
return metadata_filters.default_effect
|
|
151
156
|
|
|
152
157
|
if any(
|
|
153
158
|
f.matches(force_regexp, **kwargs)
|
|
154
|
-
for f in metadata_filters.
|
|
159
|
+
for f in metadata_filters.get_other_effect_rules(condition=condition)
|
|
155
160
|
):
|
|
156
161
|
return metadata_filters.other_effect
|
|
157
162
|
|
|
@@ -198,10 +203,10 @@ class MetadataFiltersContainer:
|
|
|
198
203
|
return not project or f.matches(project=project)
|
|
199
204
|
|
|
200
205
|
default_effect = self.metadata_filters.default_effect
|
|
201
|
-
default_effect_filters = self.metadata_filters.
|
|
206
|
+
default_effect_filters = self.metadata_filters.get_default_effect_rules(
|
|
202
207
|
condition=project_condition
|
|
203
208
|
)
|
|
204
|
-
other_effect_filters = self.metadata_filters.
|
|
209
|
+
other_effect_filters = self.metadata_filters.get_other_effect_rules(
|
|
205
210
|
condition=project_condition
|
|
206
211
|
)
|
|
207
212
|
default_effect_op = " OR " if default_effect == FilterEffectType.ALLOW else " AND "
|
|
@@ -209,7 +214,7 @@ class MetadataFiltersContainer:
|
|
|
209
214
|
|
|
210
215
|
default_effect_conditions = default_effect_op.join(
|
|
211
216
|
[
|
|
212
|
-
|
|
217
|
+
self._get_sql_field_condition(f, column_mapping, encoder, force_lowercase)
|
|
213
218
|
for f in default_effect_filters
|
|
214
219
|
]
|
|
215
220
|
)
|
|
@@ -219,13 +224,15 @@ class MetadataFiltersContainer:
|
|
|
219
224
|
for f in other_effect_filters
|
|
220
225
|
]
|
|
221
226
|
)
|
|
222
|
-
|
|
223
|
-
if
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
227
|
+
|
|
228
|
+
if default_effect_conditions and other_effect_conditions:
|
|
229
|
+
return f"(({default_effect_conditions}){default_effect_op}({other_effect_conditions}))"
|
|
230
|
+
elif default_effect_conditions:
|
|
231
|
+
return f"({default_effect_conditions})"
|
|
232
|
+
elif other_effect_conditions:
|
|
233
|
+
return f"({other_effect_conditions})"
|
|
234
|
+
else:
|
|
235
|
+
return None
|
|
229
236
|
|
|
230
237
|
@staticmethod
|
|
231
238
|
def _get_sql_field_condition(
|
|
@@ -233,7 +240,7 @@ class MetadataFiltersContainer:
|
|
|
233
240
|
column_mapping: Dict,
|
|
234
241
|
encoder: Callable[[str, str, FilterType], str],
|
|
235
242
|
force_lowercase: Optional[bool] = True,
|
|
236
|
-
):
|
|
243
|
+
) -> str:
|
|
237
244
|
# The comparison is performed case-insensitive (check MetadataFilter._safe_match)
|
|
238
245
|
# We can use LOWER here since it is part of standard SQL (like AND/OR/NOT), so including it
|
|
239
246
|
# here is a way to make sure that all comparisons are case-insensitive in the SQL sentences
|
|
@@ -250,4 +257,6 @@ class MetadataFiltersContainer:
|
|
|
250
257
|
if getattr(mf, field) is not None
|
|
251
258
|
]
|
|
252
259
|
)
|
|
260
|
+
if not conditions:
|
|
261
|
+
return ""
|
|
253
262
|
return f"NOT({conditions})" if mf.effect == FilterEffectType.BLOCK else f"({conditions})"
|
pycarlo/lib/README.md
CHANGED
|
@@ -1,3 +1,35 @@
|
|
|
1
|
-
|
|
1
|
+
# Monte Carlo GraphQL Schema Library
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The `schema.json` and `schema.py` files are auto-generated. **Do not edit them directly**!
|
|
4
|
+
|
|
5
|
+
If you need to customize the schema, see below. Refer to the
|
|
6
|
+
[CONTRIBUTING.md](../../CONTRIBUTING.md) for general development guidelines.
|
|
7
|
+
|
|
8
|
+
## Schema Customizations
|
|
9
|
+
|
|
10
|
+
The generated `schema.py` is automatically modified during the build process to apply the following
|
|
11
|
+
customizations. This is done via `sed` commands in the [Makefile](../../Makefile), but if we need to
|
|
12
|
+
get fancier, we just can update the `customize-schema` target there to call whatever we need to do.
|
|
13
|
+
|
|
14
|
+
### Connection Type Fix
|
|
15
|
+
|
|
16
|
+
The `Connection` class is changed from `sgqlc.types.relay.Connection` to `sgqlc.types.Type`.
|
|
17
|
+
|
|
18
|
+
**Why:** sgqlc automatically makes all types ending in "Connection" inherit from `relay.Connection`,
|
|
19
|
+
which makes `Connection` not a valid field type. This causes requests to fail when attempting to
|
|
20
|
+
resolve it. Changing it to inherit from `sgqlc.types.Type` fixes this issue.
|
|
21
|
+
|
|
22
|
+
[Related PR](https://github.com/monte-carlo-data/python-sdk/pull/63)
|
|
23
|
+
|
|
24
|
+
### Backward-Compatible Enums
|
|
25
|
+
|
|
26
|
+
All GraphQL enum types use `pycarlo.lib.types.Enum` instead of `sgqlc.types.Enum`. This custom enum
|
|
27
|
+
class gracefully handles unknown enum values by returning them as strings instead of raising errors.
|
|
28
|
+
|
|
29
|
+
**Why:** When new enum values are added to the Monte Carlo API, older SDK versions would crash when
|
|
30
|
+
deserializing responses containing these new values. Our custom Enum prevents this by:
|
|
31
|
+
|
|
32
|
+
- Returning unknown values as plain strings (same type as known values)
|
|
33
|
+
- Logging a warning when unknown values are encountered
|
|
34
|
+
|
|
35
|
+
See [pycarlo/lib/types.py](types.py) for implementation details.
|