Skip to content

Commit 0c77915

Browse files
committed
Webhooks support
1 parent e576212 commit 0c77915

File tree

14 files changed

+649
-271
lines changed

14 files changed

+649
-271
lines changed

openapi_core/security/providers.py

+8-8
Original file line numberDiff line numberDiff line change
@@ -3,37 +3,37 @@
33

44
from openapi_core.security.exceptions import SecurityError
55
from openapi_core.spec import Spec
6-
from openapi_core.validation.request.protocols import Request
6+
from openapi_core.validation.request.protocols import RequestParameters
77

88

99
class BaseProvider:
1010
def __init__(self, scheme: Spec):
1111
self.scheme = scheme
1212

13-
def __call__(self, request: Request) -> Any:
13+
def __call__(self, parameters: RequestParameters) -> Any:
1414
raise NotImplementedError
1515

1616

1717
class UnsupportedProvider(BaseProvider):
18-
def __call__(self, request: Request) -> Any:
18+
def __call__(self, parameters: RequestParameters) -> Any:
1919
warnings.warn("Unsupported scheme type")
2020

2121

2222
class ApiKeyProvider(BaseProvider):
23-
def __call__(self, request: Request) -> Any:
23+
def __call__(self, parameters: RequestParameters) -> Any:
2424
name = self.scheme["name"]
2525
location = self.scheme["in"]
26-
source = getattr(request.parameters, location)
26+
source = getattr(parameters, location)
2727
if name not in source:
2828
raise SecurityError("Missing api key parameter.")
2929
return source[name]
3030

3131

3232
class HttpProvider(BaseProvider):
33-
def __call__(self, request: Request) -> Any:
34-
if "Authorization" not in request.parameters.header:
33+
def __call__(self, parameters: RequestParameters) -> Any:
34+
if "Authorization" not in parameters.header:
3535
raise SecurityError("Missing authorization header.")
36-
auth_header = request.parameters.header["Authorization"]
36+
auth_header = parameters.header["Authorization"]
3737
try:
3838
auth_type, encoded_credentials = auth_header.split(" ", 1)
3939
except ValueError:

openapi_core/templating/paths/datatypes.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
from collections import namedtuple
33

44
Path = namedtuple("Path", ["path", "path_result"])
5-
OperationPath = namedtuple(
6-
"OperationPath", ["path", "operation", "path_result"]
5+
PathOperation = namedtuple(
6+
"PathOperation", ["path", "operation", "path_result"]
77
)
8-
ServerOperationPath = namedtuple(
9-
"ServerOperationPath",
8+
PathOperationServer = namedtuple(
9+
"PathOperationServer",
1010
["path", "operation", "server", "path_result", "server_result"],
1111
)

openapi_core/templating/paths/finders.py

+62-33
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
from openapi_core.schema.servers import is_absolute
1111
from openapi_core.spec import Spec
1212
from openapi_core.templating.datatypes import TemplateResult
13-
from openapi_core.templating.paths.datatypes import OperationPath
1413
from openapi_core.templating.paths.datatypes import Path
15-
from openapi_core.templating.paths.datatypes import ServerOperationPath
14+
from openapi_core.templating.paths.datatypes import PathOperation
15+
from openapi_core.templating.paths.datatypes import PathOperationServer
1616
from openapi_core.templating.paths.exceptions import OperationNotFound
1717
from openapi_core.templating.paths.exceptions import PathNotFound
1818
from openapi_core.templating.paths.exceptions import ServerNotFound
@@ -21,79 +21,87 @@
2121
from openapi_core.templating.util import search
2222

2323

24-
class PathFinder:
24+
class BasePathFinder:
2525
def __init__(self, spec: Spec, base_url: Optional[str] = None):
2626
self.spec = spec
2727
self.base_url = base_url
2828

29-
def find(
30-
self,
31-
method: str,
32-
full_url: str,
33-
) -> ServerOperationPath:
34-
paths_iter = self._get_paths_iter(full_url)
29+
def find(self, method: str, name: str) -> PathOperationServer:
30+
paths_iter = self._get_paths_iter(name)
3531
paths_iter_peek = peekable(paths_iter)
3632

3733
if not paths_iter_peek:
38-
raise PathNotFound(full_url)
34+
raise PathNotFound(name)
3935

40-
operations_iter = self._get_operations_iter(paths_iter_peek, method)
36+
operations_iter = self._get_operations_iter(method, paths_iter_peek)
4137
operations_iter_peek = peekable(operations_iter)
4238

4339
if not operations_iter_peek:
44-
raise OperationNotFound(full_url, method)
40+
raise OperationNotFound(name, method)
4541

4642
servers_iter = self._get_servers_iter(
43+
name,
4744
operations_iter_peek,
48-
full_url,
4945
)
5046

5147
try:
5248
return next(servers_iter)
5349
except StopIteration:
54-
raise ServerNotFound(full_url)
50+
raise ServerNotFound(name)
5551

56-
def _get_paths_iter(self, full_url: str) -> Iterator[Path]:
52+
def _get_paths_iter(self, name: str) -> Iterator[Path]:
53+
raise NotImplementedError
54+
55+
def _get_operations_iter(
56+
self, method: str, paths_iter: Iterator[Path]
57+
) -> Iterator[PathOperation]:
58+
for path, path_result in paths_iter:
59+
if method not in path:
60+
continue
61+
operation = path / method
62+
yield PathOperation(path, operation, path_result)
63+
64+
def _get_servers_iter(
65+
self, name: str, operations_iter: Iterator[PathOperation]
66+
) -> Iterator[PathOperationServer]:
67+
raise NotImplementedError
68+
69+
70+
class APICallPathFinder(BasePathFinder):
71+
def __init__(self, spec: Spec, base_url: Optional[str] = None):
72+
self.spec = spec
73+
self.base_url = base_url
74+
75+
def _get_paths_iter(self, name: str) -> Iterator[Path]:
5776
template_paths: List[Path] = []
5877
paths = self.spec / "paths"
5978
for path_pattern, path in list(paths.items()):
6079
# simple path.
6180
# Return right away since it is always the most concrete
62-
if full_url.endswith(path_pattern):
81+
if name.endswith(path_pattern):
6382
path_result = TemplateResult(path_pattern, {})
6483
yield Path(path, path_result)
6584
# template path
6685
else:
67-
result = search(path_pattern, full_url)
86+
result = search(path_pattern, name)
6887
if result:
6988
path_result = TemplateResult(path_pattern, result.named)
7089
template_paths.append(Path(path, path_result))
7190

7291
# Fewer variables -> more concrete path
7392
yield from sorted(template_paths, key=template_path_len)
7493

75-
def _get_operations_iter(
76-
self, paths_iter: Iterator[Path], request_method: str
77-
) -> Iterator[OperationPath]:
78-
for path, path_result in paths_iter:
79-
if request_method not in path:
80-
continue
81-
operation = path / request_method
82-
yield OperationPath(path, operation, path_result)
83-
8494
def _get_servers_iter(
85-
self, operations_iter: Iterator[OperationPath], full_url: str
86-
) -> Iterator[ServerOperationPath]:
95+
self, name: str, operations_iter: Iterator[PathOperation]
96+
) -> Iterator[PathOperationServer]:
8797
for path, operation, path_result in operations_iter:
8898
servers = (
8999
path.get("servers", None)
90100
or operation.get("servers", None)
91101
or self.spec.get("servers", [{"url": "/"}])
92102
)
93103
for server in servers:
94-
server_url_pattern = full_url.rsplit(path_result.resolved, 1)[
95-
0
96-
]
104+
server_url_pattern = name.rsplit(path_result.resolved, 1)[0]
97105
server_url = server["url"]
98106
if not is_absolute(server_url):
99107
# relative to absolute url
@@ -107,7 +115,7 @@ def _get_servers_iter(
107115
# simple path
108116
if server_url_pattern == server_url:
109117
server_result = TemplateResult(server["url"], {})
110-
yield ServerOperationPath(
118+
yield PathOperationServer(
111119
path,
112120
operation,
113121
server,
@@ -121,10 +129,31 @@ def _get_servers_iter(
121129
server_result = TemplateResult(
122130
server["url"], result.named
123131
)
124-
yield ServerOperationPath(
132+
yield PathOperationServer(
125133
path,
126134
operation,
127135
server,
128136
path_result,
129137
server_result,
130138
)
139+
140+
141+
class WebhookPathFinder(BasePathFinder):
142+
def _get_paths_iter(self, name: str) -> Iterator[Path]:
143+
webhooks = self.spec / "webhooks"
144+
for webhook_name, path in list(webhooks.items()):
145+
if name == webhook_name:
146+
path_result = TemplateResult(webhook_name, {})
147+
yield Path(path, path_result)
148+
149+
def _get_servers_iter(
150+
self, name: str, operations_iter: Iterator[PathOperation]
151+
) -> Iterator[PathOperationServer]:
152+
for path, operation, path_result in operations_iter:
153+
yield PathOperationServer(
154+
path,
155+
operation,
156+
None,
157+
path_result,
158+
{},
159+
)

openapi_core/validation/request/__init__.py

+18
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,18 @@
3333
V31RequestSecurityValidator,
3434
)
3535
from openapi_core.validation.request.validators import V31RequestValidator
36+
from openapi_core.validation.request.validators import (
37+
V31WebhookRequestBodyValidator,
38+
)
39+
from openapi_core.validation.request.validators import (
40+
V31WebhookRequestParametersValidator,
41+
)
42+
from openapi_core.validation.request.validators import (
43+
V31WebhookRequestSecurityValidator,
44+
)
45+
from openapi_core.validation.request.validators import (
46+
V31WebhookRequestValidator,
47+
)
3648

3749
__all__ = [
3850
"V30RequestBodyValidator",
@@ -43,7 +55,12 @@
4355
"V31RequestParametersValidator",
4456
"V31RequestSecurityValidator",
4557
"V31RequestValidator",
58+
"V31WebhookRequestBodyValidator",
59+
"V31WebhookRequestParametersValidator",
60+
"V31WebhookRequestSecurityValidator",
61+
"V31WebhookRequestValidator",
4662
"V3RequestValidator",
63+
"V3WebhookRequestValidator",
4764
"openapi_v30_request_body_validator",
4865
"openapi_v30_request_parameters_validator",
4966
"openapi_v30_request_security_validator",
@@ -64,6 +81,7 @@
6481

6582
# alias to the latest v3 version
6683
V3RequestValidator = V31RequestValidator
84+
V3WebhookRequestValidator = V31WebhookRequestValidator
6785

6886
# spec validators
6987
openapi_v30_request_body_validator = SpecRequestValidatorProxy(

openapi_core/validation/request/exceptions.py

-6
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,11 @@ class MissingRequestBodyError(OpenAPIRequestBodyError):
3030
"""Missing request body error"""
3131

3232

33-
@dataclass
3433
class MissingRequestBody(MissingRequestBodyError):
35-
request: Request
36-
3734
def __str__(self) -> str:
3835
return "Missing request body"
3936

4037

41-
@dataclass
4238
class MissingRequiredRequestBody(MissingRequestBodyError):
43-
request: Request
44-
4539
def __str__(self) -> str:
4640
return "Missing required request body"

openapi_core/validation/request/protocols.py

+52-10
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,25 @@
1515

1616

1717
@runtime_checkable
18-
class Request(Protocol):
18+
class BaseRequest(Protocol):
19+
20+
parameters: RequestParameters
21+
22+
@property
23+
def method(self) -> str:
24+
...
25+
26+
@property
27+
def body(self) -> Optional[str]:
28+
...
29+
30+
@property
31+
def mimetype(self) -> str:
32+
...
33+
34+
35+
@runtime_checkable
36+
class Request(BaseRequest, Protocol):
1937
"""Request attributes protocol.
2038
2139
Attributes:
@@ -44,8 +62,6 @@ class Request(Protocol):
4462
the mimetype would be "text/html".
4563
"""
4664

47-
parameters: RequestParameters
48-
4965
@property
5066
def host_url(self) -> str:
5167
...
@@ -54,16 +70,30 @@ def host_url(self) -> str:
5470
def path(self) -> str:
5571
...
5672

57-
@property
58-
def method(self) -> str:
59-
...
6073

61-
@property
62-
def body(self) -> Optional[str]:
63-
...
74+
@runtime_checkable
75+
class WebhookRequest(BaseRequest, Protocol):
76+
"""Webhook request attributes protocol.
77+
78+
Attributes:
79+
name
80+
Webhook name
81+
method
82+
The request method, as lowercase string.
83+
parameters
84+
A RequestParameters object. Needs to supports path attribute setter
85+
to write resolved path parameters.
86+
body
87+
The request body, as string.
88+
mimetype
89+
Like content type, but without parameters (eg, without charset,
90+
type etc.) and always lowercase.
91+
For example if the content type is "text/HTML; charset=utf-8"
92+
the mimetype would be "text/html".
93+
"""
6494

6595
@property
66-
def mimetype(self) -> str:
96+
def name(self) -> str:
6797
...
6898

6999

@@ -95,3 +125,15 @@ def validate(
95125
request: Request,
96126
) -> RequestValidationResult:
97127
...
128+
129+
130+
@runtime_checkable
131+
class WebhookRequestValidator(Protocol):
132+
def __init__(self, spec: Spec, base_url: Optional[str] = None):
133+
...
134+
135+
def validate(
136+
self,
137+
request: WebhookRequest,
138+
) -> RequestValidationResult:
139+
...

0 commit comments

Comments
 (0)