mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-27 12:40:35 +00:00
backport OptionsChainsData serialize schema fix from v5 branch (#7511)
This commit is contained in:
@@ -4,6 +4,16 @@ from datetime import (
|
||||
date as dateType,
|
||||
datetime,
|
||||
)
|
||||
from typing import Any, get_args, get_origin
|
||||
|
||||
from pydantic import (
|
||||
Field,
|
||||
GetJsonSchemaHandler,
|
||||
create_model,
|
||||
field_validator,
|
||||
model_serializer,
|
||||
)
|
||||
from pydantic.json_schema import JsonSchemaValue
|
||||
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from openbb_core.provider.utils.descriptions import (
|
||||
@@ -11,7 +21,6 @@ from openbb_core.provider.utils.descriptions import (
|
||||
QUERY_DESCRIPTIONS,
|
||||
)
|
||||
from openbb_core.provider.utils.options_chains_properties import OptionsChainsProperties
|
||||
from pydantic import Field, field_validator, model_serializer
|
||||
|
||||
|
||||
class OptionsChainsQueryParams(QueryParams):
|
||||
@@ -369,3 +378,54 @@ class OptionsChainsData(OptionsChainsProperties):
|
||||
records = [dict(zip(data.keys(), values)) for values in zip(*data.values())]
|
||||
|
||||
return records
|
||||
|
||||
@classmethod
|
||||
def __get_pydantic_json_schema__(
|
||||
cls,
|
||||
core_schema: Any,
|
||||
handler: GetJsonSchemaHandler,
|
||||
) -> JsonSchemaValue:
|
||||
"""Return a JSON schema that matches serialized row records."""
|
||||
_ = (core_schema, handler)
|
||||
|
||||
row_fields: dict[str, Any] = {}
|
||||
for field_name, field_info in cls.model_fields.items():
|
||||
annotation = field_info.annotation
|
||||
inner = (
|
||||
get_args(annotation)[0]
|
||||
if get_origin(annotation) is list
|
||||
else annotation
|
||||
)
|
||||
|
||||
if inner is datetime or (get_origin(inner) is None and inner is dateType):
|
||||
inner = str | None
|
||||
|
||||
row_fields[field_name] = (
|
||||
(
|
||||
inner | None
|
||||
if get_origin(inner) is None and inner is not type(None)
|
||||
else inner
|
||||
),
|
||||
Field(
|
||||
default=None,
|
||||
description=field_info.description,
|
||||
json_schema_extra=field_info.json_schema_extra,
|
||||
),
|
||||
)
|
||||
|
||||
row_model = create_model(f"{cls.__name__}Item", **row_fields) # type: ignore[call-overload]
|
||||
items_schema = row_model.model_json_schema(
|
||||
ref_template="#/components/schemas/{model}",
|
||||
)
|
||||
defs = items_schema.pop("$defs", None)
|
||||
|
||||
schema: JsonSchemaValue = {
|
||||
"type": "array",
|
||||
"description": (cls.__doc__ or "").split("\n\n", maxsplit=1)[0].strip(),
|
||||
"items": items_schema,
|
||||
"title": cls.__name__,
|
||||
}
|
||||
if defs:
|
||||
schema["$defs"] = defs
|
||||
|
||||
return schema
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Tests for options chains serialization and schema shape."""
|
||||
|
||||
from datetime import date, datetime
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
from openbb_core.app.model.obbject import OBBject
|
||||
from openbb_core.provider.standard_models.options_chains import OptionsChainsData
|
||||
|
||||
|
||||
def _sample_payload() -> dict:
|
||||
return {
|
||||
"underlying_symbol": ["FAKE", "FAKE"],
|
||||
"underlying_price": [100.0, 100.0],
|
||||
"contract_symbol": ["FAKE240117C00100000", "FAKE240117P00100000"],
|
||||
"expiration": [date(2030, 1, 17), date(2030, 1, 17)],
|
||||
"strike": [100.0, 100.0],
|
||||
"option_type": ["call", "put"],
|
||||
"open_interest": [10, 5],
|
||||
"volume": [100, 50],
|
||||
"bid": [1.5, 1.4],
|
||||
"ask": [1.6, 1.5],
|
||||
"last_trade_time": [datetime(2026, 1, 1, 15, 30), datetime(2026, 1, 1, 15, 31)],
|
||||
}
|
||||
|
||||
|
||||
def test_options_chains_model_dump_is_row_records() -> None:
|
||||
data = OptionsChainsData(**_sample_payload())
|
||||
dumped = data.model_dump()
|
||||
|
||||
assert isinstance(dumped, list)
|
||||
assert len(dumped) == 2
|
||||
assert dumped[0]["option_type"] == "call"
|
||||
assert dumped[1]["option_type"] == "put"
|
||||
assert isinstance(dumped[0]["last_trade_time"], str)
|
||||
|
||||
|
||||
def test_options_chains_json_schema_is_array_of_rows() -> None:
|
||||
schema = OptionsChainsData.model_json_schema()
|
||||
|
||||
assert schema["type"] == "array"
|
||||
assert schema["items"]["type"] == "object"
|
||||
|
||||
strike_types = {
|
||||
entry.get("type") for entry in schema["items"]["properties"]["strike"]["anyOf"]
|
||||
}
|
||||
assert "number" in strike_types
|
||||
assert "null" in strike_types
|
||||
|
||||
|
||||
def test_fastapi_openapi_results_references_row_schema() -> None:
|
||||
app = FastAPI()
|
||||
|
||||
@app.get("/chains")
|
||||
def chains() -> OBBject[OptionsChainsData]:
|
||||
return OBBject(results=[])
|
||||
|
||||
openapi = app.openapi()
|
||||
components = openapi["components"]["schemas"]
|
||||
|
||||
assert components["OptionsChainsData"]["type"] == "array"
|
||||
|
||||
route_schema = openapi["paths"]["/chains"]["get"]["responses"]["200"]["content"][
|
||||
"application/json"
|
||||
]["schema"]
|
||||
wrapper_name = route_schema["$ref"].rsplit("/", 1)[-1]
|
||||
results_schema = components[wrapper_name]["properties"]["results"]
|
||||
refs = [
|
||||
entry.get("$ref")
|
||||
for entry in results_schema.get("anyOf", [results_schema])
|
||||
if entry.get("$ref")
|
||||
]
|
||||
assert any(ref.endswith("OptionsChainsData") for ref in refs)
|
||||
Reference in New Issue
Block a user