-
Notifications
You must be signed in to change notification settings - Fork 254
Expand file tree
/
Copy pathextract_yml.py
More file actions
123 lines (100 loc) · 3.91 KB
/
extract_yml.py
File metadata and controls
123 lines (100 loc) · 3.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import re
import typing as T
from pathlib import Path
from pydantic.v1 import Field, validator
from cumulusci.core.enums import StrEnum
from cumulusci.tasks.bulkdata.utils import DataApi
from cumulusci.utils.yaml.model_parser import CCIDictModel, HashableBaseModel
object_decl = re.compile(r"objects\((\w+)\)", re.IGNORECASE)
field_decl = re.compile(r"fields\((\w+)\)", re.IGNORECASE)
class SFObjectGroupTypes(StrEnum):
all = "all"
custom = "custom"
standard = "standard"
class SFFieldGroupTypes(StrEnum):
all = "all"
custom = "custom"
standard = "standard"
required = "required"
class ExtractDeclaration(HashableBaseModel):
where: T.Optional[str] = None
fields_: T.Union[T.List[str], str] = Field(["FIELDS(ALL)"], alias="fields")
api: DataApi = DataApi.SMART
sf_object: T.Optional[str] = None # injected, not declared explicitly
@staticmethod
def parse_field_complex_type(fieldspec):
"""If it's something like FIELDS(...), parse it out"""
if match := field_decl.match(fieldspec):
matching_group = match.groups()[0].lower()
field_group_type = getattr(SFFieldGroupTypes, matching_group, None)
return field_group_type
else:
return None
def assert_sf_object_fits_pattern(self):
if self.is_group:
assert (
self.group_type in SFObjectGroupTypes
), f"Expected OBJECTS(ALL), OBJECTS(CUSTOM) or OBJECTS(STANDARD), not `{self.group_type.upper()}`"
else:
assert self.sf_object.isidentifier(), (
"Value should start with OBJECTS( or be a simple alphanumeric field name"
f" (underscores allowed) not {self.sf_object}"
)
return self.sf_object
def assert_check_where_against_complex(self):
"""Check that a where clause was not used with a group declaration."""
assert not (
self.where and self.is_group
), "Cannot specify a `where` clause on a declaration for multiple kinds of objects."
@validator("fields_")
def normalize_fields(cls, vals):
if isinstance(vals, str):
vals = [vals]
for val in vals:
cls.validate_field(val)
return vals
@classmethod
def validate_field(cls, val):
assert cls.parse_field_complex_type(val) or val.isidentifier(), val
if group_type := cls.parse_field_complex_type(val):
assert group_type, (
"group_type in OBJECT(group_type) should be one of "
f"{tuple(SFFieldGroupTypes.__members__.keys())}, "
f"not {val}"
)
return val
@property
def is_group(self):
return bool(self.group_type)
@property
def group_type(self):
"""If it's something like OBJECT(...), parse it out"""
val = self._parse_group_type(self.sf_object)
if val:
group_type = getattr(SFObjectGroupTypes, val, None)
assert group_type, (
"group_type in OBJECT(group_type) should be one of "
f"{tuple(SFObjectGroupTypes.__members__.keys())}, "
f"not {val}"
)
return group_type
@staticmethod
def _parse_group_type(val):
if "(" in val:
return object_decl.match(val)[1].lower()
else:
return None
class ExtractRulesFile(CCIDictModel):
version: int = 1
extract: T.Dict[str, ExtractDeclaration]
@validator("extract")
def inject_sf_object_name(cls, val):
for sf_object, decl in val.items():
decl.sf_object = sf_object
decl.assert_sf_object_fits_pattern()
decl.assert_check_where_against_complex()
return val
@classmethod
def parse_extract(cls, source: T.Union[str, Path, T.IO]):
"""Return the extract key after parsing an extract file."""
return super().parse_from_yaml(source).extract