Skip to content

Commit 2a4547e

Browse files
committed
NEW: markers GetItem, Drop, ReasonPhraseMixin, TextMixin, and StatusCode
1 parent 9cf3992 commit 2a4547e

File tree

8 files changed

+140
-20
lines changed

8 files changed

+140
-20
lines changed

combadge/core/markers/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .method import * # noqa: F403
2+
from .parameter import * # noqa: F403
3+
from .response import * # noqa: F403

combadge/core/markers/response.py

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from abc import ABC, abstractmethod
44
from dataclasses import dataclass
5-
from typing import Any, Dict, TypeVar
5+
from typing import Any, Dict, Mapping, TypeVar
66

77
from combadge.core.markers.base import AnnotatedMarker
88

@@ -20,15 +20,49 @@ def transform(self, response: Any, payload: Any) -> Any:
2020

2121
@dataclass
2222
class Map(ResponseMarker):
23+
"""Map a payload to a dictionary under the specified key."""
24+
25+
key: Any
26+
"""Key under which the response will be mapped."""
27+
28+
def transform(self, response: Any, payload: Any) -> Dict[Any, Any]: # noqa: D102
29+
return {self.key: payload}
30+
31+
32+
@dataclass
33+
class GetItem(ResponseMarker):
2334
"""
24-
Map a payload to a dictionary under the specified key.
35+
Extract a value from the specified key.
2536
26-
Other Args:
27-
_InputPayloadT (type): input payload type
37+
Examples:
38+
>>> def call() -> Annotated[
39+
>>> int, # Status code is an integer
40+
>>> Drop(), # Drop the payload
41+
>>> StatusCodeMixin(), # Mix in the status code from the HTTP response
42+
>>> GetItem("status_code"), # Extract the status code
43+
>>> ]:
44+
>>> ...
2845
"""
2946

3047
key: Any
31-
"""Key under which the response will be mapped."""
48+
"""Key which will be extracted from the payload."""
49+
50+
def transform(self, response: Any, payload: Mapping[Any, Any]) -> Any: # noqa: D102
51+
return payload[self.key]
52+
53+
54+
class Drop(ResponseMarker): # pragma: no cover
55+
"""
56+
Drop the payload.
57+
58+
It might be useful if one is only interested, for example, in an HTTP status code:
59+
60+
Examples:
61+
>>> def call() -> Annotated[..., Drop(), StatusCodeMixin()]
62+
>>> ...
63+
"""
64+
65+
__slots__ = ()
3266

3367
def transform(self, response: Any, payload: Any) -> Dict[Any, Any]: # noqa: D102
34-
return {self.key: payload}
68+
return {}

combadge/support/http/abc.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,24 @@ def get_payload(self) -> dict:
9090

9191

9292
class SupportsStatusCode(Protocol):
93-
"""Supports a status code attribute or property."""
93+
"""Supports a read-only status code attribute or property."""
9494

95-
status_code: int
95+
@property
96+
def status_code(self) -> int: # noqa: D102
97+
raise NotImplementedError
98+
99+
100+
class SupportsReasonPhrase(Protocol):
101+
"""Supports a read-only reason phrase attribute or property."""
102+
103+
@property
104+
def reason_phrase(self) -> str: # noqa: D102
105+
raise NotImplementedError
106+
107+
108+
class SupportsText(Protocol):
109+
"""Supports a read-only text attribute or property."""
110+
111+
@property
112+
def text(self) -> str: # noqa: D102
113+
raise NotImplementedError

combadge/support/http/markers/request.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,12 +125,13 @@ class Payload(ParameterMarker[ContainsPayload]):
125125
Mark parameter as a request payload. An argument gets converted to a dictionary and passed over to a backend.
126126
127127
Examples:
128-
>>> class BodyModel(BaseModel):
129-
>>> ...
128+
Simple usage:
130129
131130
>>> def call(body: Payload[BodyModel]) -> ...:
132131
>>> ...
133132
133+
Equivalent expanded usage:
134+
134135
>>> def call(body: Annotated[BodyModel, Payload()]) -> ...:
135136
>>> ...
136137
"""
@@ -184,9 +185,6 @@ class FormData(ParameterMarker[ContainsFormData]):
184185
An argument gets converted to a dictionary and passed over to a backend.
185186
186187
Examples:
187-
>>> class FormModel(BaseModel):
188-
>>> ...
189-
190188
>>> def call(body: FormData[FormModel]) -> ...:
191189
>>> ...
192190

combadge/support/http/markers/response.py

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
from __future__ import annotations
22

33
from dataclasses import dataclass
4+
from http import HTTPStatus
45
from typing import Any, MutableMapping, TypeVar
56

6-
from combadge.core.markers.response import ResponseMarker
7-
from combadge.support.http.abc import SupportsStatusCode
7+
from typing_extensions import Annotated, TypeAlias
8+
9+
from combadge.core.markers.response import Drop, GetItem, ResponseMarker
10+
from combadge.support.http.abc import SupportsReasonPhrase, SupportsStatusCode, SupportsText
811

912
_MutableMappingT = TypeVar("_MutableMappingT", bound=MutableMapping[Any, Any])
1013

@@ -23,5 +26,54 @@ class StatusCodeMixin(ResponseMarker):
2326
"""Key under which the status code should assigned in the payload."""
2427

2528
def transform(self, response: SupportsStatusCode, input_: _MutableMappingT) -> _MutableMappingT: # noqa: D102
26-
input_[self.key] = response.status_code
29+
input_[self.key] = HTTPStatus(response.status_code)
2730
return input_
31+
32+
33+
@dataclass
34+
class ReasonPhraseMixin(ResponseMarker):
35+
"""Update payload with HTTP reason message."""
36+
37+
key: Any = "reason"
38+
"""Key under which the reason message should assigned in the payload."""
39+
40+
def transform(self, response: SupportsReasonPhrase, input_: _MutableMappingT) -> _MutableMappingT: # noqa: D102
41+
input_[self.key] = response.reason_phrase
42+
return input_
43+
44+
45+
@dataclass
46+
class TextMixin(ResponseMarker):
47+
"""
48+
Update payload with HTTP response text.
49+
50+
Examples:
51+
>>> class MyResponse(BaseModel):
52+
>>> my_text: str
53+
>>>
54+
>>> class MyService(Protocol):
55+
>>> @http_method("GET")
56+
>>> @path(...)
57+
>>> def get_text(self) -> Annotated[MyResponse, TextMixin("my_text")]:
58+
>>> ...
59+
"""
60+
61+
key: Any = "text"
62+
"""Key under which the text contents should assigned in the payload."""
63+
64+
def transform(self, response: SupportsText, input_: _MutableMappingT) -> _MutableMappingT: # noqa: D102
65+
input_[self.key] = response.text
66+
return input_
67+
68+
69+
StatusCode: TypeAlias = Annotated[HTTPStatus, Drop(), StatusCodeMixin(), GetItem("status_code")]
70+
"""
71+
Shortcut to retrieve just a response status code.
72+
73+
Examples:
74+
>>> def call(...) -> StatusCode:
75+
>>> ...
76+
"""
77+
78+
79+
__all__ = ("StatusCodeMixin", "ReasonPhraseMixin", "TextMixin", "StatusCode")

docs/support/core.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,20 @@ simplify the binding process:
99
options:
1010
heading_level: 3
1111

12+
<hr>
13+
1214
## Method markers
1315

1416
::: combadge.core.markers.method
1517
options:
1618
heading_level: 3
1719
members: ["wrap_with"]
1820

21+
<hr>
22+
1923
## Response markers
2024

2125
::: combadge.core.markers.response
2226
options:
2327
heading_level: 3
24-
members: ["Map"]
28+
members: ["Drop", "Map", "GetItem"]

docs/support/http.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
options:
77
heading_level: 3
88

9+
<hr>
10+
911
## Response markers
1012

1113
::: combadge.support.http.markers.response

tests/support/http/test_markers.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import inspect
2-
from types import SimpleNamespace
2+
from http import HTTPStatus
33
from typing import Any, Dict, Tuple
44

55
import pytest
6+
from httpx import Response
67

78
from combadge.support.http.abc import ContainsUrlPath
8-
from combadge.support.http.markers import Path, StatusCodeMixin
9+
from combadge.support.http.markers import Path, ReasonPhraseMixin, StatusCodeMixin, TextMixin
910

1011

1112
@pytest.mark.parametrize(
@@ -32,7 +33,15 @@ def test_path_factory() -> None:
3233

3334

3435
def test_status_code_mixin() -> None:
35-
assert StatusCodeMixin("key").transform(SimpleNamespace(status_code=200), {}) == {"key": 200}
36+
assert StatusCodeMixin("key").transform(Response(status_code=200), {}) == {"key": HTTPStatus.OK}
37+
38+
39+
def test_reason_phrase_mixin() -> None:
40+
assert ReasonPhraseMixin("key").transform(Response(status_code=200), {}) == {"key": "OK"}
41+
42+
43+
def test_text_mixin() -> None:
44+
assert TextMixin("key").transform(Response(status_code=200, text="my text"), {}) == {"key": "my text"}
3645

3746

3847
def _example(positional: str, *, keyword: str) -> None:

0 commit comments

Comments
 (0)