Skip to content

Commit fd6fbf6

Browse files
committed
feat: odata constants
1 parent 50d6eff commit fd6fbf6

8 files changed

Lines changed: 133 additions & 57 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Open Points
2+
3+
**OData & HTTP**:
4+
- Padronizar HTTP transport também
5+
- Perguntas:
6+
- Como nós podemos acelarar criacão de classes para business a partir de API Spec + Client utilizando Odata ou HTTP Transport
7+
- Opcões são: stainless x Skill x static converter (CLI, code, plugin)
8+
9+
**Agent Gateway**:
10+
- Filtrar MCP Tools
11+
- Cache MCP Tools: Precisa ser configurável e resetável
12+
- Conversor para Pydantic também (task criada)
13+
14+
**Destination**:
15+
- Destination ou HTTP / Odata client usando ztis

src/sap_cloud_sdk/core/odata/_async_transport.py

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@
88

99
import httpx
1010

11+
from sap_cloud_sdk.core.odata._constants import (
12+
CSRF_FETCH_TIMEOUT,
13+
CSRF_FETCH_VALUE,
14+
CSRF_HEADER,
15+
DEFAULT_HEADERS,
16+
MUTATING_METHODS,
17+
REQUEST_TIMEOUT,
18+
)
1119
from sap_cloud_sdk.core.odata.exceptions import (
1220
ODataAuthError,
1321
ODataCsrfError,
@@ -17,12 +25,6 @@
1725

1826
logger = logging.getLogger(__name__)
1927

20-
_CSRF_HEADER = "X-CSRF-Token"
21-
_CSRF_FETCH_VALUE = "Fetch"
22-
_CSRF_FETCH_TIMEOUT = 10
23-
_MUTATING_METHODS = {"POST", "PUT", "PATCH", "DELETE"}
24-
_REQUEST_TIMEOUT = 30
25-
2628

2729
class AsyncODataHttpTransport:
2830
"""Asynchronous HTTP transport for OData v4 services.
@@ -86,14 +88,14 @@ async def request(
8688
"""
8789
extra = dict(headers or {})
8890

89-
if method.upper() in _MUTATING_METHODS and self._csrf_enabled:
90-
extra[_CSRF_HEADER] = await self._get_csrf_token()
91+
if method.upper() in MUTATING_METHODS and self._csrf_enabled:
92+
extra[CSRF_HEADER] = await self._get_csrf_token()
9193
try:
9294
return await self._execute(method, path, params=params, json=json, extra_headers=extra)
9395
except ODataAuthError as exc:
9496
if exc.status_code == 403:
9597
await self._invalidate_csrf_token()
96-
extra[_CSRF_HEADER] = await self._get_csrf_token()
98+
extra[CSRF_HEADER] = await self._get_csrf_token()
9799
return await self._execute(method, path, params=params, json=json, extra_headers=extra)
98100
raise
99101

@@ -126,13 +128,13 @@ async def _fetch_csrf_token(self) -> str:
126128
try:
127129
resp = await self._client.get(
128130
url,
129-
headers={_CSRF_HEADER: _CSRF_FETCH_VALUE},
130-
timeout=_CSRF_FETCH_TIMEOUT,
131+
headers={CSRF_HEADER: CSRF_FETCH_VALUE},
132+
timeout=CSRF_FETCH_TIMEOUT,
131133
)
132134
except httpx.RequestError as exc:
133135
raise ODataCsrfError(f"Async CSRF fetch failed: {exc}") from exc
134136

135-
token = resp.headers.get(_CSRF_HEADER, "")
137+
token = resp.headers.get(CSRF_HEADER, "")
136138
if not token:
137139
raise ODataCsrfError(
138140
f"Service did not return a CSRF token (HTTP {resp.status_code})"
@@ -149,12 +151,7 @@ async def _execute(
149151
extra_headers: dict[str, str] | None = None,
150152
) -> dict[str, Any]:
151153
url = self.absolute_url(path)
152-
req_headers: dict[str, str] = {
153-
"Accept": "application/json",
154-
"Content-Type": "application/json",
155-
}
156-
if extra_headers:
157-
req_headers.update(extra_headers)
154+
req_headers = {**DEFAULT_HEADERS, **(extra_headers or {})}
158155

159156
logger.debug("%s %s params=%s", method, url, params)
160157
try:
@@ -164,7 +161,7 @@ async def _execute(
164161
headers=req_headers,
165162
params=params,
166163
json=json,
167-
timeout=_REQUEST_TIMEOUT,
164+
timeout=REQUEST_TIMEOUT,
168165
)
169166
except httpx.RequestError as exc:
170167
raise ODataCsrfError(f"Request failed: {exc}") from exc
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""Shared constants for the OData v4 HTTP layer."""
2+
3+
from __future__ import annotations
4+
5+
# ---------------------------------------------------------------------------
6+
# CSRF
7+
# ---------------------------------------------------------------------------
8+
9+
CSRF_HEADER = "X-CSRF-Token"
10+
CSRF_FETCH_VALUE = "Fetch"
11+
CSRF_FETCH_TIMEOUT = 10
12+
13+
# ---------------------------------------------------------------------------
14+
# HTTP
15+
# ---------------------------------------------------------------------------
16+
17+
REQUEST_TIMEOUT = 30
18+
19+
MUTATING_METHODS = frozenset({"POST", "PUT", "PATCH", "DELETE"})
20+
21+
DEFAULT_HEADERS: dict[str, str] = {
22+
"Accept": "application/json",
23+
"Content-Type": "application/json",
24+
}
25+
26+
# HTTP method literals
27+
GET = "GET"
28+
POST = "POST"
29+
PUT = "PUT"
30+
PATCH = "PATCH"
31+
DELETE = "DELETE"
32+
33+
# Standard conditional-request header
34+
IF_MATCH_HEADER = "If-Match"
35+
36+
# ---------------------------------------------------------------------------
37+
# OData system query options
38+
# ---------------------------------------------------------------------------
39+
40+
QUERY_SELECT = "$select"
41+
QUERY_FILTER = "$filter"
42+
QUERY_ORDERBY = "$orderby"
43+
QUERY_TOP = "$top"
44+
QUERY_SKIP = "$skip"
45+
QUERY_EXPAND = "$expand"
46+
47+
# ---------------------------------------------------------------------------
48+
# OData response envelope keys
49+
# ---------------------------------------------------------------------------
50+
51+
RESPONSE_VALUE = "value"
52+
RESPONSE_NEXT_LINK = "@odata.nextLink"

src/sap_cloud_sdk/core/odata/_csrf.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,12 @@
77

88
import requests as _requests
99

10+
from sap_cloud_sdk.core.odata._constants import CSRF_FETCH_TIMEOUT, CSRF_FETCH_VALUE, CSRF_HEADER
1011
from sap_cloud_sdk.core.odata.exceptions import ODataCsrfError
1112

1213
if TYPE_CHECKING:
1314
from ._transport import ODataHttpTransport
1415

15-
_CSRF_HEADER = "X-CSRF-Token"
16-
_CSRF_FETCH_VALUE = "Fetch"
17-
_FETCH_TIMEOUT = 10
18-
1916

2017
class CsrfTokenProvider:
2118
"""Fetch and cache a CSRF token for one OData service root.
@@ -57,13 +54,13 @@ def _fetch(self) -> str:
5754
try:
5855
resp = self._transport._session.get(
5956
url,
60-
headers={_CSRF_HEADER: _CSRF_FETCH_VALUE},
61-
timeout=_FETCH_TIMEOUT,
57+
headers={CSRF_HEADER: CSRF_FETCH_VALUE},
58+
timeout=CSRF_FETCH_TIMEOUT,
6259
)
6360
except _requests.RequestException as exc:
6461
raise ODataCsrfError(f"CSRF fetch failed: {exc}") from exc
6562

66-
token = resp.headers.get(_CSRF_HEADER, "")
63+
token = resp.headers.get(CSRF_HEADER, "")
6764
if not token:
6865
raise ODataCsrfError(
6966
f"Service did not return a CSRF token (HTTP {resp.status_code})"

src/sap_cloud_sdk/core/odata/_query.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@
66
from enum import Enum
77
from typing import TYPE_CHECKING
88

9+
from sap_cloud_sdk.core.odata._constants import (
10+
QUERY_EXPAND,
11+
QUERY_FILTER,
12+
QUERY_ORDERBY,
13+
QUERY_SELECT,
14+
QUERY_SKIP,
15+
QUERY_TOP,
16+
)
17+
918
if TYPE_CHECKING:
1019
from ._filter import FilterExpression
1120

@@ -120,19 +129,19 @@ def custom(self, key: str, value: str) -> "StructuredQuery":
120129
def to_params(self) -> dict[str, str]:
121130
params: dict[str, str] = {}
122131
if self._select:
123-
params["$select"] = ",".join(self._select)
132+
params[QUERY_SELECT] = ",".join(self._select)
124133
if self._filter is not None:
125-
params["$filter"] = str(self._filter)
134+
params[QUERY_FILTER] = str(self._filter)
126135
if self._orderby:
127-
params["$orderby"] = ",".join(
136+
params[QUERY_ORDERBY] = ",".join(
128137
f"{f} {d.value}" for f, d in self._orderby
129138
)
130139
if self._top is not None:
131-
params["$top"] = str(self._top)
140+
params[QUERY_TOP] = str(self._top)
132141
if self._skip is not None:
133-
params["$skip"] = str(self._skip)
142+
params[QUERY_SKIP] = str(self._skip)
134143
if self._expand:
135-
params["$expand"] = ",".join(self._expand)
144+
params[QUERY_EXPAND] = ",".join(self._expand)
136145
for k, v in self._custom:
137146
params[k] = v
138147
return params

src/sap_cloud_sdk/core/odata/_request_builders.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@
55
import dataclasses
66
from typing import Any, Generic, Iterator, TypeVar, TYPE_CHECKING
77

8+
from sap_cloud_sdk.core.odata._constants import (
9+
DELETE,
10+
GET,
11+
IF_MATCH_HEADER,
12+
PATCH,
13+
POST,
14+
PUT,
15+
)
816
from sap_cloud_sdk.core.odata._query import OrderDirection, StructuredQuery
917
from sap_cloud_sdk.core.odata._response import deserialize_collection, deserialize_single
1018
from sap_cloud_sdk.core.odata._pagination import ODataPageIterator
@@ -87,7 +95,7 @@ def expand(self, *nav_properties: str) -> "GetAllRequestBuilder[T]":
8795
def execute(self) -> list[T]:
8896
"""Execute the request and return all matching entities."""
8997
path = _entity_set_path(self._entity_type)
90-
data = self._transport.request("GET", path, params=self._query.to_params())
98+
data = self._transport.request(GET, path, params=self._query.to_params())
9199
return deserialize_collection(data, self._entity_type)
92100

93101
def iterate_pages(self) -> Iterator[list[T]]:
@@ -100,7 +108,7 @@ def iterate_pages(self) -> Iterator[list[T]]:
100108
first_url += "?" + urlencode(params)
101109

102110
iterator = ODataPageIterator(
103-
fetch_page=lambda url: self._transport.request("GET", _strip_base(url, self._transport._base_url)),
111+
fetch_page=lambda url: self._transport.request(GET, _strip_base(url, self._transport._base_url)),
104112
entity_type=self._entity_type,
105113
first_url=first_url,
106114
)
@@ -145,7 +153,7 @@ def expand(self, *nav_properties: str) -> "GetByKeyRequestBuilder[T]":
145153
def execute(self) -> T:
146154
"""Fetch the entity, raising :exc:`ODataNotFoundError` if absent."""
147155
path = _entity_set_path(self._entity_type) + _build_key_segment(self._key)
148-
data = self._transport.request("GET", path, params=self._query.to_params())
156+
data = self._transport.request(GET, path, params=self._query.to_params())
149157
return deserialize_single(data, self._entity_type)
150158

151159

@@ -163,7 +171,7 @@ def execute(self) -> T:
163171
entity_type = type(self._entity)
164172
path = _entity_set_path(entity_type)
165173
body = self._entity.to_dict() if hasattr(self._entity, "to_dict") else dataclasses.asdict(self._entity) # type: ignore[arg-type]
166-
data = self._transport.request("POST", path, json=body)
174+
data = self._transport.request(POST, path, json=body)
167175
return deserialize_single(data, entity_type)
168176

169177

@@ -198,10 +206,10 @@ def execute(self) -> T:
198206
key = {k: getattr(self._entity, k) for k in key_fields}
199207
path = _entity_set_path(entity_type) + _build_key_segment(key)
200208
body = self._entity.to_dict() if hasattr(self._entity, "to_dict") else dataclasses.asdict(self._entity) # type: ignore[arg-type]
201-
method = "PUT" if self._use_put else "PATCH"
209+
method = PUT if self._use_put else PATCH
202210
extra: dict[str, str] = {}
203211
if self._etag is not None:
204-
extra["If-Match"] = self._etag
212+
extra[IF_MATCH_HEADER] = self._etag
205213
data = self._transport.request(method, path, json=body, headers=extra or None)
206214
if not data:
207215
return self._entity
@@ -228,5 +236,5 @@ def execute(self) -> None:
228236
path = _entity_set_path(self._entity_type) + _build_key_segment(self._key)
229237
extra: dict[str, str] = {}
230238
if self._etag is not None:
231-
extra["If-Match"] = self._etag
232-
self._transport.request("DELETE", path, headers=extra or None)
239+
extra[IF_MATCH_HEADER] = self._etag
240+
self._transport.request(DELETE, path, headers=extra or None)

src/sap_cloud_sdk/core/odata/_response.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import dataclasses
66
from typing import Any, TypeVar
77

8+
from sap_cloud_sdk.core.odata._constants import RESPONSE_NEXT_LINK, RESPONSE_VALUE
89
from sap_cloud_sdk.core.odata.exceptions import ODataDeserializationError
910

1011
T = TypeVar("T")
@@ -22,7 +23,7 @@ def deserialize_single(data: dict[str, Any], entity_type: type[T]) -> T:
2223
f"{entity_type!r} is not a dataclass — cannot deserialize"
2324
)
2425
try:
25-
payload = data.get("value", data) if isinstance(data.get("value"), dict) else data
26+
payload = data.get(RESPONSE_VALUE, data) if isinstance(data.get(RESPONSE_VALUE), dict) else data
2627
known = {f.name for f in dataclasses.fields(entity_type)} # type: ignore[arg-type]
2728
kwargs = {k: v for k, v in payload.items() if k in known}
2829
return entity_type(**kwargs) # type: ignore[call-arg]
@@ -43,7 +44,7 @@ def deserialize_collection(data: dict[str, Any], entity_type: type[T]) -> list[T
4344
f"{entity_type!r} is not a dataclass — cannot deserialize"
4445
)
4546
try:
46-
items: list[dict[str, Any]] = data.get("value", [])
47+
items: list[dict[str, Any]] = data.get(RESPONSE_VALUE, [])
4748
return [deserialize_single(item, entity_type) for item in items]
4849
except ODataDeserializationError:
4950
raise
@@ -55,4 +56,4 @@ def deserialize_collection(data: dict[str, Any], entity_type: type[T]) -> list[T
5556

5657
def next_link(data: dict[str, Any]) -> str | None:
5758
"""Extract ``@odata.nextLink`` from a collection response, or ``None``."""
58-
return data.get("@odata.nextLink")
59+
return data.get(RESPONSE_NEXT_LINK)

src/sap_cloud_sdk/core/odata/_transport.py

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@
88
import requests
99
from requests.exceptions import RequestException
1010

11+
from sap_cloud_sdk.core.odata._constants import (
12+
CSRF_HEADER,
13+
DEFAULT_HEADERS,
14+
MUTATING_METHODS,
15+
REQUEST_TIMEOUT,
16+
)
1117
from sap_cloud_sdk.core.odata._csrf import CsrfTokenProvider
1218
from sap_cloud_sdk.core.odata.exceptions import (
1319
ODataAuthError,
@@ -17,10 +23,6 @@
1723

1824
logger = logging.getLogger(__name__)
1925

20-
_CSRF_HEADER = "X-CSRF-Token"
21-
_MUTATING_METHODS = {"POST", "PUT", "PATCH", "DELETE"}
22-
_REQUEST_TIMEOUT = 30
23-
2426

2527
class ODataHttpTransport:
2628
"""Reusable synchronous HTTP transport for OData v4 services.
@@ -87,14 +89,14 @@ def request(
8789
"""
8890
extra = dict(headers or {})
8991

90-
if method.upper() in _MUTATING_METHODS and self._csrf is not None:
91-
extra[_CSRF_HEADER] = self._csrf.get()
92+
if method.upper() in MUTATING_METHODS and self._csrf is not None:
93+
extra[CSRF_HEADER] = self._csrf.get()
9294
try:
9395
return self._execute(method, path, params=params, json=json, extra_headers=extra)
9496
except ODataAuthError as exc:
9597
if exc.status_code == 403:
9698
self._csrf.invalidate()
97-
extra[_CSRF_HEADER] = self._csrf.get()
99+
extra[CSRF_HEADER] = self._csrf.get()
98100
return self._execute(method, path, params=params, json=json, extra_headers=extra)
99101
raise
100102

@@ -118,12 +120,7 @@ def _execute(
118120
extra_headers: dict[str, str] | None = None,
119121
) -> dict[str, Any]:
120122
url = self.absolute_url(path)
121-
req_headers: dict[str, str] = {
122-
"Accept": "application/json",
123-
"Content-Type": "application/json",
124-
}
125-
if extra_headers:
126-
req_headers.update(extra_headers)
123+
req_headers = {**DEFAULT_HEADERS, **(extra_headers or {})}
127124

128125
logger.debug("%s %s params=%s", method, url, params)
129126
try:
@@ -133,7 +130,7 @@ def _execute(
133130
headers=req_headers,
134131
params=params,
135132
json=json,
136-
timeout=_REQUEST_TIMEOUT,
133+
timeout=REQUEST_TIMEOUT,
137134
)
138135
except RequestException as exc:
139136
raise ODataRequestError.__new__(ODataRequestError) from exc

0 commit comments

Comments
 (0)