Skip to content

Commit 9bd5069

Browse files
committed
WIP
1 parent 943d80f commit 9bd5069

File tree

2 files changed

+156
-98
lines changed

2 files changed

+156
-98
lines changed

pulp-glue/pulp_glue/common/openapi.py

Lines changed: 77 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
from pulp_glue.common.exceptions import (
2222
OpenAPIError,
2323
PulpAuthenticationFailed,
24-
PulpException,
2524
PulpHTTPError,
2625
PulpNotAutorized,
2726
UnsafeCallError,
@@ -41,9 +40,14 @@
4140

4241
@dataclass
4342
class _Request:
43+
operation_id: str
4444
method: str
45+
url: str
4546
headers: MutableMultiMapping[str] | CIMultiDictProxy[str] | t.MutableMapping[str, str]
46-
body: bytes | None
47+
params: dict[str, str]
48+
data: dict[str, t.Any] | str | None
49+
files: list[tuple[str, tuple[str, UploadType, str]]] | None
50+
security: list[dict[str, list[str]]] | None
4751

4852

4953
@dataclass
@@ -515,7 +519,7 @@ def _render_request_body(
515519

516520
return content_type, data, files
517521

518-
async def _send_request(
522+
def _render_request(
519523
self,
520524
path_spec: dict[str, t.Any],
521525
method: str,
@@ -524,11 +528,11 @@ async def _send_request(
524528
headers: dict[str, str],
525529
body: dict[str, t.Any] | None = None,
526530
validate_body: bool = True,
527-
) -> _Response:
531+
) -> _Request:
528532
method_spec = path_spec[method]
529533
_headers = CIMultiDict(self._headers)
530534
_headers.update(headers)
531-
content_type, data, files = self._render_request_body(method_spec, body, validate_body)
535+
532536
security: list[dict[str, list[str]]] | None
533537
if self._auth_provider and "Authorization" not in self._headers:
534538
security = method_spec.get("security", self.api_spec.get("security"))
@@ -538,34 +542,60 @@ async def _send_request(
538542
# Authorization header present? You wanted it that way...
539543
security = None
540544

545+
content_type, data, files = self._render_request_body(method_spec, body, validate_body)
541546
# For we encode the json on our side.
542547
# Somehow this does not work properly for multipart...
543548
if content_type is not None and content_type.startswith("application/json"):
544549
_headers["Content-Type"] = content_type
545550

546-
for key, value in headers.items():
547-
self._debug_callback(2, f" {key}: {value}")
548-
if data is not None:
549-
self._debug_callback(3, f"{data!r}")
551+
return _Request(
552+
operation_id=method_spec["operationId"],
553+
method=method,
554+
url=url,
555+
headers=_headers,
556+
params=params,
557+
data=data,
558+
files=files,
559+
security=security,
560+
)
550561

551-
if self._dry_run and method.upper() not in SAFE_METHODS:
552-
raise UnsafeCallError(_("Call aborted due to safe mode"))
562+
def _log_request(self, request: _Request) -> None:
563+
if request.params:
564+
qs = urlencode(request.params)
565+
log_msg = f"{request.operation_id} : {request.method} {request.url}?{qs}"
566+
else:
567+
log_msg = f"{request.operation_id} : {request.method} {request.url}"
568+
self._debug_callback(1, log_msg)
569+
self._debug_callback(
570+
2, "\n".join([f" {key}=={value}" for key, value in request.params.items()])
571+
)
572+
for key, value in request.headers.items():
573+
self._debug_callback(2, f" {key}: {value}")
574+
if request.data is not None:
575+
self._debug_callback(3, f"{request.data!r}")
553576

577+
async def _send_request(
578+
self,
579+
request: _Request,
580+
) -> _Response:
554581
try:
555582
async with aiohttp.ClientSession() as session:
556583
async with session.request(
557-
method,
558-
url,
559-
params=params,
560-
headers=_headers,
561-
data=data,
562-
# files=files,
584+
request.method,
585+
request.url,
586+
params=request.params,
587+
headers=request.headers,
588+
data=request.data,
589+
# files=request.files,
563590
ssl=self.ssl_context,
564591
max_redirects=0,
565-
middlewares=[_Middleware(self, security)],
566-
) as response:
592+
middlewares=[_Middleware(self, request.security)],
593+
) as r:
567594
# response.raise_for_status()
568-
response_body = await response.read()
595+
response_body = await r.read()
596+
response = _Response(
597+
status_code=r.status, headers=r.headers, body=response_body
598+
)
569599
except aiohttp.TooManyRedirects as e:
570600
# We could handle that in the middleware...
571601
assert e.history[-1] is not None
@@ -577,25 +607,25 @@ async def _send_request(
577607
except aiohttp.ClientResponseError as e:
578608
raise OpenAPIError(str(e))
579609

580-
self._debug_callback(1, _("Response: {status_code}").format(status_code=response.status))
610+
return response
611+
612+
def _log_response(self, response: _Response) -> None:
613+
self._debug_callback(
614+
1, _("Response: {status_code}").format(status_code=response.status_code)
615+
)
581616
for key, value in response.headers.items():
582617
self._debug_callback(2, f" {key}: {value}")
583-
if response_body:
584-
self._debug_callback(3, f"{response_body!r}")
618+
if response.body:
619+
self._debug_callback(3, f"{response.body!r}")
585620

586-
if response.status == 401:
621+
def _parse_response(self, method_spec: dict[str, t.Any], response: _Response) -> t.Any:
622+
if response.status_code == 401:
587623
raise PulpAuthenticationFailed(method_spec["operationId"])
588-
if response.status == 403:
624+
elif response.status_code == 403:
589625
raise PulpNotAutorized(method_spec["operationId"])
590-
try:
591-
response.raise_for_status()
592-
except aiohttp.ClientResponseError as e:
593-
raise PulpHTTPError(str(e), e.status)
594-
except aiohttp.ClientError as e:
595-
raise PulpException(str(e))
596-
return _Response(status_code=response.status, headers=response.headers, body=response_body)
626+
elif response.status_code >= 300:
627+
raise PulpHTTPError(response.body.decode(), response.status_code)
597628

598-
def _parse_response(self, method_spec: dict[str, t.Any], response: _Response) -> t.Any:
599629
if response.status_code == 204:
600630
return {}
601631

@@ -668,26 +698,21 @@ def call(
668698
)
669699
url = urljoin(self._base_url, path)
670700

671-
if query_params:
672-
qs = urlencode(query_params)
673-
log_msg = f"{operation_id} : {method} {url}?{qs}"
674-
else:
675-
log_msg = f"{operation_id} : {method} {url}"
676-
self._debug_callback(1, log_msg)
677-
self._debug_callback(
678-
2, "\n".join([f" {key}=={value}" for key, value in query_params.items()])
701+
request = self._render_request(
702+
path_spec,
703+
method,
704+
url,
705+
query_params,
706+
headers,
707+
body,
708+
validate_body=validate_body,
679709
)
710+
self._log_request(request)
680711

681-
response = asyncio.run(
682-
self._send_request(
683-
path_spec,
684-
method,
685-
url,
686-
query_params,
687-
headers,
688-
body,
689-
validate_body=validate_body,
690-
)
691-
)
712+
if self._dry_run and request.method.upper() not in SAFE_METHODS:
713+
raise UnsafeCallError(_("Call aborted due to safe mode"))
714+
715+
response = asyncio.run(self._send_request(request))
692716

717+
self._log_response(response)
693718
return self._parse_response(method_spec, response)
Lines changed: 79 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,102 @@
11
import json
22
import logging
3-
import typing as t
43

54
import pytest
65

7-
from pulp_glue.common.openapi import OpenAPI
6+
from pulp_glue.common.openapi import OpenAPI, _Request, _Response
87

98
pytestmark = pytest.mark.glue
109

1110
TEST_SCHEMA = json.dumps(
1211
{
1312
"openapi": "3.0.3",
14-
"paths": {"test/": {"get": {"operationId": "test_id", "responses": {200: {}}}}},
15-
"components": {"schemas": {}},
13+
"paths": {
14+
"test/": {
15+
"get": {"operationId": "get_test_id", "responses": {200: {}}},
16+
"post": {
17+
"operationId": "post_test_id",
18+
"requestBody": {
19+
"required": True,
20+
"content": {
21+
"application/json": {
22+
"schema": {"$ref": "#/components/schemas/testBody"}
23+
}
24+
},
25+
},
26+
"responses": {200: {}},
27+
},
28+
}
29+
},
30+
"components": {
31+
"schemas": {
32+
"testBody": {
33+
"type": "object",
34+
"properties": {"text": {"type": "string"}},
35+
"required": ["text"],
36+
}
37+
}
38+
},
1639
}
1740
).encode()
1841

1942

20-
class MockRequest:
21-
headers: dict[str, str] = {}
22-
body: dict[str, t.Any] = {}
23-
24-
25-
class MockResponse:
26-
status_code = 200
27-
headers: dict[str, str] = {}
28-
text = "{}"
29-
content: dict[str, t.Any] = {}
30-
31-
def raise_for_status(self) -> None:
32-
pass
33-
34-
35-
class MockSession:
36-
def prepare_request(self, *args: t.Any, **kwargs: t.Any) -> MockRequest:
37-
return MockRequest()
38-
39-
def send(self, request: MockRequest) -> MockResponse:
40-
return MockResponse()
43+
async def mock_send_request(request: _Request) -> _Response:
44+
return _Response(status_code=200, headers={}, body=b"{}")
4145

4246

4347
@pytest.fixture
44-
def openapi(monkeypatch: pytest.MonkeyPatch) -> OpenAPI:
48+
def mock_openapi(monkeypatch: pytest.MonkeyPatch) -> OpenAPI:
4549
monkeypatch.setattr(OpenAPI, "load_api", lambda self, refresh_cache: TEST_SCHEMA)
46-
openapi = OpenAPI("base_url", "doc_path")
50+
openapi = OpenAPI("base_url", "doc_path", user_agent="test agent")
4751
openapi._parse_api(TEST_SCHEMA)
48-
monkeypatch.setattr(openapi, "_session", MockSession())
52+
monkeypatch.setattr(
53+
openapi,
54+
"_send_request",
55+
mock_send_request,
56+
)
4957
return openapi
5058

5159

52-
def test_openapi_logs_nothing_from_info(openapi: OpenAPI, caplog: pytest.LogCaptureFixture) -> None:
53-
caplog.set_level(logging.INFO)
54-
openapi.call("test_id")
55-
assert caplog.record_tuples == []
56-
57-
58-
def test_openapi_logs_operation_info_to_debug(
59-
openapi: OpenAPI, caplog: pytest.LogCaptureFixture
60-
) -> None:
61-
caplog.set_level(logging.DEBUG)
62-
openapi.call("test_id")
63-
assert caplog.record_tuples == [
64-
("pulp_glue.openapi", logging.DEBUG + 3, "test_id : get test/"),
65-
("pulp_glue.openapi", logging.DEBUG + 2, ""),
66-
("pulp_glue.openapi", logging.DEBUG + 1, "{}"),
67-
("pulp_glue.openapi", logging.DEBUG + 3, "Response: 200"),
68-
("pulp_glue.openapi", logging.DEBUG + 1, "{}"),
69-
]
60+
class TestOpenAPILogs:
61+
def test_nothing_with_level_info(
62+
self,
63+
mock_openapi: OpenAPI,
64+
caplog: pytest.LogCaptureFixture,
65+
) -> None:
66+
caplog.set_level(logging.INFO)
67+
mock_openapi.call("get_test_id")
68+
assert caplog.record_tuples == []
69+
70+
def test_get_operation_to_debug(
71+
self,
72+
mock_openapi: OpenAPI,
73+
caplog: pytest.LogCaptureFixture,
74+
) -> None:
75+
caplog.set_level(logging.DEBUG, logger="pulp_glue.openapi")
76+
mock_openapi.call("get_test_id")
77+
assert caplog.record_tuples == [
78+
("pulp_glue.openapi", logging.DEBUG + 3, "get_test_id : get test/"),
79+
("pulp_glue.openapi", logging.DEBUG + 2, ""),
80+
("pulp_glue.openapi", logging.DEBUG + 2, " User-Agent: test agent"),
81+
("pulp_glue.openapi", logging.DEBUG + 2, " Accept: application/json"),
82+
("pulp_glue.openapi", logging.DEBUG + 3, "Response: 200"),
83+
("pulp_glue.openapi", logging.DEBUG + 1, "b'{}'"),
84+
]
85+
86+
def test_post_operation_to_debug(
87+
self,
88+
mock_openapi: OpenAPI,
89+
caplog: pytest.LogCaptureFixture,
90+
) -> None:
91+
caplog.set_level(logging.DEBUG, logger="pulp_glue.openapi")
92+
mock_openapi.call("post_test_id", body={"text": "Trace"})
93+
assert caplog.record_tuples == [
94+
("pulp_glue.openapi", logging.DEBUG + 3, "post_test_id : post test/"),
95+
("pulp_glue.openapi", logging.DEBUG + 2, ""),
96+
("pulp_glue.openapi", logging.DEBUG + 2, " User-Agent: test agent"),
97+
("pulp_glue.openapi", logging.DEBUG + 2, " Accept: application/json"),
98+
("pulp_glue.openapi", logging.DEBUG + 2, " Content-Type: application/json"),
99+
("pulp_glue.openapi", logging.DEBUG + 1, '\'{"text": "Trace"}\''),
100+
("pulp_glue.openapi", logging.DEBUG + 3, "Response: 200"),
101+
("pulp_glue.openapi", logging.DEBUG + 1, "b'{}'"),
102+
]

0 commit comments

Comments
 (0)