Skip to content

Commit d073245

Browse files
author
Anders Brams
committed
fix: freeform objects
1 parent 99b4cf8 commit d073245

5 files changed

Lines changed: 117 additions & 29 deletions

File tree

openapi_python/generator/normalize.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,8 @@ def _schema_to_type(
469469
return _schema_freeform_object_to_type(schema), state
470470
if isinstance(additional_properties, dict):
471471
return _schema_map_to_type(state, schema, hint)
472+
if additional_properties is None:
473+
return _schema_freeform_object_to_type(schema), state
472474

473475
if schema_type == "object" or "properties" in schema:
474476
return _schema_object_to_type(
Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
from datetime import datetime
34
from typing import Any
45

56
from fastapi import FastAPI
@@ -10,14 +11,53 @@
1011

1112
class TaskExecutionDto(BaseModel):
1213
id: int
14+
task_id: int
15+
finished_at: datetime | None
1316
log: list[dict[str, Any]]
14-
metadata: dict[str, object]
17+
result: int | None
18+
output: dict | None
19+
should_retry: bool
20+
stacktrace: dict | None
1521

1622

17-
@app.get("/task-executions/{task_id}", response_model=TaskExecutionDto)
18-
def get_task_execution(task_id: int) -> TaskExecutionDto:
19-
return TaskExecutionDto(
20-
id=task_id,
23+
class TaskDto(BaseModel):
24+
id: int
25+
state: int
26+
queue_id: int
27+
n_attempts: int
28+
meta: dict | None
29+
payload: dict
30+
enqueued_at: datetime
31+
started_at: datetime | None
32+
finished_at: datetime | None
33+
pipeline_execution_id: int | None = None
34+
pipeline_node_id: int | None = None
35+
executions: list[TaskExecutionDto] | None = None
36+
37+
38+
@app.get("/tasks", response_model=list[TaskDto])
39+
def list_tasks() -> list[TaskDto]:
40+
execution = TaskExecutionDto(
41+
id=1,
42+
task_id=1,
43+
finished_at=None,
2144
log=[{"message": "started", "attempt": 1}],
22-
metadata={"trigger": "manual", "dry_run": False},
45+
result=None,
46+
output={"status": "ok"},
47+
should_retry=False,
48+
stacktrace=None,
2349
)
50+
return [
51+
TaskDto(
52+
id=1,
53+
state=1,
54+
queue_id=1,
55+
n_attempts=1,
56+
meta=None,
57+
payload={"operation": "deploy"},
58+
enqueued_at=datetime(2026, 1, 1),
59+
started_at=None,
60+
finished_at=None,
61+
executions=[execution],
62+
)
63+
]

tests/contract/freeform_objects/generate.py

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,39 +10,69 @@
1010
from openapi_python.generator import GenerationRequest, generate_client
1111

1212

13+
def _strip_additional_properties(schema: dict) -> None:
14+
match schema:
15+
case dict():
16+
if schema.get("additionalProperties") is True:
17+
schema.pop("additionalProperties")
18+
for value in schema.values():
19+
_strip_additional_properties(value)
20+
case list():
21+
for item in schema:
22+
_strip_additional_properties(item)
23+
24+
1325
def main() -> None:
1426
output_dir = Path(__file__).parent / "generated"
27+
spec = app.openapi()
28+
_strip_additional_properties(spec)
1529
generate_client(
1630
GenerationRequest(
1731
output_dir=output_dir,
18-
spec_json=json.dumps(app.openapi()),
32+
spec_json=json.dumps(spec),
1933
overwrite=True,
2034
)
2135
)
2236

2337
source = (output_dir / "my_client" / "types.py").read_text()
2438
assert "class TaskExecutionDtoLogItem(TypedDict):" not in source
39+
assert "class TaskExecutionDtoOutputVariant(TypedDict):" not in source
40+
assert "class TaskExecutionDtoStacktraceVariant(TypedDict):" not in source
41+
assert "class TaskDtoMetaVariant(TypedDict):" not in source
42+
assert "class TaskDtoPayload(TypedDict):" not in source
2543
assert "log: list[dict[str, Any]]" in source
26-
assert "metadata: dict[str, Any]" in source
44+
assert "output: dict[str, Any] | None" in source
45+
assert "stacktrace: dict[str, Any] | None" in source
46+
assert "meta: dict[str, Any] | None" in source
47+
assert "payload: dict[str, Any]" in source
48+
assert "executions: NotRequired[list[TaskExecutionDto] | None]" in source
2749

2850
generated_types = importlib.import_module("generated.my_client.types")
2951
api_b = FastAPI()
3052

31-
def get_task_execution() -> object:
32-
return {}
53+
def list_tasks() -> object:
54+
return []
3355

34-
api_b.get(
35-
"/task-executions/{task_id}",
36-
response_model=generated_types.TaskExecutionDto,
37-
)(get_task_execution)
56+
api_b.get("/tasks", response_model=list[generated_types.TaskDto])(list_tasks)
3857

39-
schema = api_b.openapi()["components"]["schemas"]["TaskExecutionDto"]
40-
log_items = schema["properties"]["log"]["items"]
41-
metadata = schema["properties"]["metadata"]
58+
schemas = api_b.openapi()["components"]["schemas"]
59+
task_execution = schemas["TaskExecutionDto"]
60+
task = schemas["TaskDto"]
61+
log_items = task_execution["properties"]["log"]["items"]
62+
output = task_execution["properties"]["output"]["anyOf"][0]
63+
stacktrace = task_execution["properties"]["stacktrace"]["anyOf"][0]
64+
meta = task["properties"]["meta"]["anyOf"][0]
65+
payload = task["properties"]["payload"]
4266
assert log_items["type"] == "object"
4367
assert log_items["additionalProperties"] is True
44-
assert metadata["type"] == "object"
45-
assert metadata["additionalProperties"] is True
68+
assert output["type"] == "object"
69+
assert output["additionalProperties"] is True
70+
assert stacktrace["type"] == "object"
71+
assert stacktrace["additionalProperties"] is True
72+
assert meta["type"] == "object"
73+
assert meta["additionalProperties"] is True
74+
assert payload["type"] == "object"
75+
assert payload["additionalProperties"] is True
4676

4777

4878
if __name__ == "__main__":

tests/contract/freeform_objects/usage_async.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,21 @@
33
from typing import Any, assert_type
44

55
from generated.my_client import AsyncClient
6-
from generated.my_client.types import TaskExecutionDto
6+
from generated.my_client.types import TaskDto, TaskExecutionDto
77

88
client = AsyncClient(base_url="http://testserver")
99

1010

1111
async def main() -> None:
12-
result = await client.get("/task-executions/{task_id}")(params={"task_id": 1})
13-
assert_type(result, TaskExecutionDto)
14-
assert_type(result["log"], list[dict[str, Any]])
15-
assert_type(result["metadata"], dict[str, Any])
12+
result = await client.get("/tasks")()
13+
assert_type(result, list[TaskDto])
14+
task = result[0]
15+
assert_type(task["meta"], dict[str, Any] | None)
16+
assert_type(task["payload"], dict[str, Any])
17+
assert_type(task["executions"], list[TaskExecutionDto] | None)
18+
execution = task["executions"][0] if task["executions"] is not None else None
19+
assert_type(execution, TaskExecutionDto | None)
20+
if execution is not None:
21+
assert_type(execution["log"], list[dict[str, Any]])
22+
assert_type(execution["output"], dict[str, Any] | None)
23+
assert_type(execution["stacktrace"], dict[str, Any] | None)

tests/contract/freeform_objects/usage_sync.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,19 @@
33
from typing import Any, assert_type
44

55
from generated.my_client import Client
6-
from generated.my_client.types import TaskExecutionDto
6+
from generated.my_client.types import TaskDto, TaskExecutionDto
77

88
client = Client(base_url="http://testserver")
99

10-
result = client.get("/task-executions/{task_id}")(params={"task_id": 1})
11-
assert_type(result, TaskExecutionDto)
12-
assert_type(result["log"], list[dict[str, Any]])
13-
assert_type(result["metadata"], dict[str, Any])
10+
result = client.get("/tasks")()
11+
assert_type(result, list[TaskDto])
12+
task = result[0]
13+
assert_type(task["meta"], dict[str, Any] | None)
14+
assert_type(task["payload"], dict[str, Any])
15+
assert_type(task["executions"], list[TaskExecutionDto] | None)
16+
execution = task["executions"][0] if task["executions"] is not None else None
17+
assert_type(execution, TaskExecutionDto | None)
18+
if execution is not None:
19+
assert_type(execution["log"], list[dict[str, Any]])
20+
assert_type(execution["output"], dict[str, Any] | None)
21+
assert_type(execution["stacktrace"], dict[str, Any] | None)

0 commit comments

Comments
 (0)