-
Notifications
You must be signed in to change notification settings - Fork 26
/
Copy pathpath_pattern.py
183 lines (143 loc) · 5.03 KB
/
path_pattern.py
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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# Copyright 2023 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Path pattern matching utilities."""
from enum import Enum
import re
def path_parts(path: str) -> list[str]:
if not path or path == "" or path == "/":
return []
return path.strip("/").split("/")
def join_path(base: str, child: str) -> str:
return "/".join(path_parts(base) + path_parts(child))
def trim_param(param: str) -> str:
param_no_braces = param[1:-1]
if "=" in param_no_braces:
return param_no_braces[:param_no_braces.index("=")]
return param_no_braces
_WILDCARD_CAPTURE_REGEX = re.compile(r"{[^/{}]+}", re.IGNORECASE)
class SegmentName(Enum):
SEGMENT = "segment"
SINGLE_CAPTURE = "single-capture"
MULTI_CAPTURE = "multi-capture"
class PathSegment:
"""
A segment of a path pattern.
"""
name: SegmentName
value: str
trimmed: str
def __str__(self):
return self.value
@property
def is_single_segment_wildcard(self):
pass
@property
def is_multi_segment_wildcard(self):
pass
class Segment(PathSegment):
"""
A segment of a path pattern.
"""
def __init__(self, value: str):
self.value = value
self.trimmed = value
self.name = SegmentName.SEGMENT
@property
def is_single_segment_wildcard(self):
return "*" in self.value and not self.is_multi_segment_wildcard
@property
def is_multi_segment_wildcard(self):
return "**" in self.value
class SingleCaptureSegment(PathSegment):
"""
A segment of a path pattern that captures a single segment.
"""
name = SegmentName.SINGLE_CAPTURE
def __init__(self, value):
self.value = value
self.trimmed = trim_param(value)
@property
def is_single_segment_wildcard(self):
return True
@property
def is_multi_segment_wildcard(self):
return False
class MultiCaptureSegment(PathSegment):
"""
A segment of a path pattern that captures multiple segments.
"""
name = SegmentName.MULTI_CAPTURE
def __init__(self, value):
self.value = value
self.trimmed = trim_param(value)
@property
def is_single_segment_wildcard(self):
return False
@property
def is_multi_segment_wildcard(self):
return True
class PathPattern:
"""
Implements Eventarc's path pattern from the spec
https://cloud.google.com/eventarc/docs/path-patterns
"""
segments: list[PathSegment]
def __init__(self, raw_path: str):
normalized_path = raw_path.strip("/")
self.raw = normalized_path
self.segments = []
self.init_path_segments(normalized_path)
def init_path_segments(self, raw: str):
parts = raw.split("/")
for part in parts:
segment: PathSegment | None = None
capture = re.findall(_WILDCARD_CAPTURE_REGEX, part)
if capture is not None and len(capture) == 1:
if "**" in part:
segment = MultiCaptureSegment(part)
else:
segment = SingleCaptureSegment(part)
else:
segment = Segment(part)
self.segments.append(segment)
@property
def value(self) -> str:
return self.raw
@property
def has_wildcards(self) -> bool:
return any(segment.is_single_segment_wildcard or
segment.is_multi_segment_wildcard
for segment in self.segments)
@property
def has_captures(self) -> bool:
return any(segment.name in (SegmentName.SINGLE_CAPTURE,
SegmentName.MULTI_CAPTURE)
for segment in self.segments)
def extract_matches(self, path: str) -> dict[str, str]:
matches: dict[str, str] = {}
if not self.has_captures:
return matches
path_segments = path_parts(path)
path_ndx = 0
for segment_ndx in range(len(self.segments)):
segment = self.segments[segment_ndx]
remaining_segments = len(self.segments) - 1 - segment_ndx
next_path_ndx = len(path_segments) - remaining_segments
if segment.name == SegmentName.SINGLE_CAPTURE:
matches[segment.trimmed] = path_segments[path_ndx]
elif segment.name == SegmentName.MULTI_CAPTURE:
matches[segment.trimmed] = "/".join(
path_segments[path_ndx:next_path_ndx])
path_ndx = next_path_ndx if segment.is_multi_segment_wildcard else path_ndx + 1
return matches