Skip to content

Commit 1b541c4

Browse files
authored
Merge pull request #2 from flusflas/feature/form-urlencoded-support
Adds support for request payloads with `application/x-www-form-urlencoded` content type, and improves handling of structured content types.
2 parents f0e2e6e + 35da5a8 commit 1b541c4

File tree

24 files changed

+487
-188
lines changed

24 files changed

+487
-188
lines changed

CHANGELOG.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,21 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.3.0](https://github.com/flusflas/apier/tree/v0.3.0) (2025-07-31)
9+
10+
This release adds support for request payloads with
11+
`application/x-www-form-urlencoded` content type, and improves handling of
12+
structured content types such as `application/json-patch+json`.
13+
14+
### Added
15+
16+
- Handle request content types with structured syntax suffixes.
17+
- Support for `application/x-www-form-urlencoded` request payloads.
18+
819
## [0.2.0](https://github.com/flusflas/apier/tree/v0.2.0) (2025-07-24)
920

10-
This release adds support for multipart requests, binary responses, improved request handling, and includes various fixes and minor enhancements.
21+
This release adds support for multipart requests, binary responses, improved
22+
request handling, and includes various fixes and minor enhancements.
1123

1224
### Added
1325

apier/templates/python_tree/base/internal/content_type.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import mimetypes
33
from dataclasses import dataclass, field
44
from io import IOBase
5-
from typing import Any, Optional
5+
from typing import Any, Optional, Union
66
from urllib.parse import parse_qsl
77

88
import xmltodict
@@ -109,7 +109,7 @@ class ContentTypeValidationResult:
109109
type: str = "" # The request's Content-Type, indicating the data format
110110
data: Any = None
111111
files: Optional[dict] = None
112-
json: Optional[dict] = None
112+
json: Optional[Union[dict, list]] = None
113113
headers: CaseInsensitiveDict = field(default_factory=dict)
114114

115115

@@ -124,6 +124,33 @@ def to_plain_text(obj) -> ContentTypeValidationResult:
124124
)
125125

126126

127+
def to_form_urlencoded(obj) -> ContentTypeValidationResult:
128+
"""
129+
Returns the form-urlencoded representation of the given object.
130+
Raises an exception if the object cannot be serialized to a valid
131+
application/x-www-form-urlencoded format.
132+
"""
133+
result = ContentTypeValidationResult(
134+
type="application/x-www-form-urlencoded",
135+
headers=CaseInsensitiveDict(
136+
{"Content-Type": "application/x-www-form-urlencoded"}
137+
),
138+
)
139+
140+
if isinstance(obj, (str, bytes)):
141+
result.data = str(obj)
142+
elif isinstance(obj, dict):
143+
result.data = obj
144+
elif isinstance(obj, APIBaseModel):
145+
result.data = json.loads(obj.json(by_alias=True))
146+
else:
147+
raise ValueError(
148+
f'Value type "{type(obj).__name__}" cannot be converted to form-urlencoded'
149+
)
150+
151+
return result
152+
153+
127154
def to_json(obj) -> ContentTypeValidationResult:
128155
"""
129156
Returns the JSON representation of the given object.
@@ -225,6 +252,7 @@ def to_multipart(obj) -> ContentTypeValidationResult:
225252

226253

227254
SUPPORTED_REQUEST_CONTENT_TYPES = {
255+
"application/x-www-form-urlencoded": to_form_urlencoded,
228256
"application/json": to_json,
229257
"application/xml": to_xml,
230258
"text/plain": to_plain_text,

apier/templates/python_tree/base/internal/resource.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,12 @@ def _validate_request_payload(
399399
try:
400400
result = conv_func(body)
401401

402+
# If the content types are not an exact match, update the info
403+
# (e.g. application/json to application/json-patch+json)
404+
if not content_types_match(content_type, ct):
405+
result.type = content_type
406+
result.headers["Content-Type"] = content_type
407+
402408
# Keep the headers from the request
403409
if headers:
404410
result.headers.update(headers)

apier/templates/python_tree/security.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,7 @@ def parse_security_schemes(definition: Definition) -> list[str]:
3131
else:
3232
warnings.warn(
3333
f"Security scheme '{name}' has an unsupported OAuth2 "
34-
f"flow type ({flow_name}) schema type '{scheme_type}' "
35-
f"and will be ignored"
34+
f"flow type '{flow_name}' and will be ignored"
3635
)
3736
continue
3837

apier/templates/python_tree/templates/api.jinja

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import json
21
from typing import Union
2+
from urllib.parse import urljoin
33

44
import requests
55
from pydantic import BaseModel
@@ -39,7 +39,7 @@ class API(
3939
if not host.startswith("http://") and not host.startswith("https://"):
4040
host = "https://" + host
4141

42-
self.host = host
42+
self.host = host.rstrip("/")
4343
self._verify = verify
4444
self.headers = {}
4545
self._raise_errors = {{ raise_errors }}
@@ -110,15 +110,16 @@ class API(
110110

111111
headers.update(self.headers)
112112

113+
# If the payload is a pydantic model, handle it as JSON
113114
if isinstance(data, BaseModel):
114-
data = data.json(by_alias=True)
115+
json = data.json(by_alias=True)
115116
if isinstance(json, BaseModel):
116117
json = json.json(by_alias=True)
117118

118119
if url.lower().startswith("http://") or url.lower().startswith("https://"):
119120
url = url
120121
else:
121-
url = self.host + url
122+
url = urljoin(self.host, url)
122123

123124
req = requests.Request(
124125
method, url, params=params, headers=headers, data=data, json=json

docs/templates/python_tree.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,5 +277,6 @@ In the example above, the `stream=True` parameter is used to enable streaming of
277277
## ⚠️ Limitations
278278

279279
- 🧬 The `python-tree` template is best suited for APIs that have a clear hierarchical structure. It may not be the best choice for APIs with flat or complex endpoint structures, where a different template design might be more appropriate.
280-
- 💾 The `python-tree` template supports text, JSON, XML, and multipart payloads for requests. Responses can handle text, JSON, XML, and binary data payloads. Other payload types have not been tested and therefore may not work as expected (or may not work at all).
280+
- 💾 **Request payloads**: This template supports request payloads with content types such as `text/plain`, `application/json`, `application/xml`, `multipart/form-data`, `application/x-www-form-urlencoded`, and types using structured syntax suffixes like `application/json-patch+json`. Other content types are untested and may not be supported.
281+
- 📦 **Response payloads**: Supports response content types like `text/plain`, `application/json`, `application/xml`, and binary data.
281282
- 🧭 Strategies requiring dynamic evaluation of runtime expressions, such as page or offset pagination, are not yet supported.

0 commit comments

Comments
 (0)