Skip to content

Commit 4b61054

Browse files
committed
api-servier
1 parent bd36009 commit 4b61054

File tree

8 files changed

+198
-18
lines changed

8 files changed

+198
-18
lines changed

services/api-server/openapi.json

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5544,7 +5544,7 @@
55445544
"urls": {
55455545
"items": {
55465546
"type": "string",
5547-
"maxLength": 65536,
5547+
"maxLength": 2083,
55485548
"minLength": 1,
55495549
"format": "uri"
55505550
},
@@ -6159,7 +6159,7 @@
61596159
},
61606160
"download_link": {
61616161
"type": "string",
6162-
"maxLength": 65536,
6162+
"maxLength": 2083,
61636163
"minLength": 1,
61646164
"format": "uri",
61656165
"title": "Download Link"
@@ -6201,14 +6201,14 @@
62016201
},
62026202
"docs_url": {
62036203
"type": "string",
6204-
"maxLength": 65536,
6204+
"maxLength": 2083,
62056205
"minLength": 1,
62066206
"format": "uri",
62076207
"title": "Docs Url"
62086208
},
62096209
"docs_dev_url": {
62106210
"type": "string",
6211-
"maxLength": 65536,
6211+
"maxLength": 2083,
62126212
"minLength": 1,
62136213
"format": "uri",
62146214
"title": "Docs Dev Url"
@@ -6471,7 +6471,6 @@
64716471
"enum": [
64726472
"TIER"
64736473
],
6474-
"const": "TIER",
64756474
"title": "PricingPlanClassification"
64766475
},
64776476
"PricingUnitGet": {
@@ -6891,7 +6890,7 @@
68916890
"type": "integer",
68926891
"x_unit": "second"
68936892
},
6894-
"key": "input_2",
6893+
"key": "f763658f-a89a-4a90-ace4-c44631290f12",
68956894
"kind": "input"
68966895
}
68976896
},
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,41 @@
1+
from copy import deepcopy
2+
13
from common_library.json_serialization import json_dumps, json_loads
4+
from pydantic import GetJsonSchemaHandler
5+
from pydantic.json_schema import JsonSchemaValue
6+
from pydantic_core.core_schema import CoreSchema
27

38

49
class BaseConfig:
510
json_loads = json_loads
611
json_dumps = json_dumps
12+
13+
14+
class UriSchema:
15+
"""
16+
Use with HttpUrl to produce schema with uri format such as
17+
{
18+
"format": "uri",
19+
"maxLength": 2083,
20+
"minLength": 1,
21+
"type": "string",
22+
}
23+
24+
SEE # https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-00#section-7.3.5
25+
"""
26+
27+
@classmethod
28+
def __get_pydantic_json_schema__(
29+
cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler
30+
) -> JsonSchemaValue:
31+
json_schema = deepcopy(handler(core_schema))
32+
33+
if (schema := core_schema.get("schema", {})) and schema.get("type") == "url":
34+
json_schema.update(
35+
type="string",
36+
format="uri",
37+
)
38+
if max_length := schema.get("max_length"):
39+
json_schema.update(maxLength=max_length, minLength=1)
40+
41+
return json_schema

services/api-server/src/simcore_service_api_server/models/schemas/files.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
)
2424
from servicelib.file_utils import create_sha256_checksum
2525

26+
from .._utils_pydantic import UriSchema
27+
2628
_NAMESPACE_FILEID_KEY = UUID("aa154444-d22d-4290-bb15-df37dba87865")
2729

2830

@@ -167,7 +169,7 @@ class UploadLinks(BaseModel):
167169

168170
class FileUploadData(BaseModel):
169171
chunk_size: NonNegativeInt
170-
urls: list[HttpUrl]
172+
urls: list[Annotated[HttpUrl, UriSchema()]]
171173
links: UploadLinks
172174

173175

services/api-server/src/simcore_service_api_server/models/schemas/jobs.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
from ...models.schemas.files import File
2828
from ...models.schemas.solvers import Solver
29+
from .._utils_pydantic import UriSchema
2930
from ..api_resources import (
3031
RelativeResourceName,
3132
compose_resource_name,
@@ -152,7 +153,9 @@ class JobMetadata(BaseModel):
152153
metadata: dict[str, MetaValueType] = Field(..., description="Custom key-value map")
153154

154155
# Links
155-
url: HttpUrl | None = Field(..., description="Link to get this resource (self)")
156+
url: Annotated[HttpUrl, UriSchema()] | None = Field(
157+
..., description="Link to get this resource (self)"
158+
)
156159

157160
model_config = ConfigDict(
158161
json_schema_extra={
@@ -198,11 +201,13 @@ class Job(BaseModel):
198201
)
199202

200203
# Get links to other resources
201-
url: HttpUrl | None = Field(..., description="Link to get this resource (self)")
202-
runner_url: HttpUrl | None = Field(
204+
url: Annotated[HttpUrl, UriSchema()] | None = Field(
205+
..., description="Link to get this resource (self)"
206+
)
207+
runner_url: Annotated[HttpUrl, UriSchema()] | None = Field(
203208
..., description="Link to the solver's job (parent collection)"
204209
)
205-
outputs_url: HttpUrl | None = Field(
210+
outputs_url: Annotated[HttpUrl, UriSchema()] | None = Field(
206211
..., description="Link to the job outputs (sub-collection)"
207212
)
208213

@@ -223,7 +228,7 @@ class Job(BaseModel):
223228

224229
@field_validator("name", mode="before")
225230
@classmethod
226-
def check_name(cls, v, info: ValidationInfo):
231+
def _check_name(cls, v, info: ValidationInfo):
227232
_id = str(info.data["id"])
228233
if not v.endswith(f"/{_id}"):
229234
msg = f"Resource name [{v}] and id [{_id}] do not match"

services/api-server/src/simcore_service_api_server/models/schemas/meta.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
from typing import Annotated
2+
13
from models_library.api_schemas__common.meta import BaseMeta
2-
from pydantic import AnyHttpUrl, ConfigDict
4+
from pydantic import ConfigDict, HttpUrl
5+
from simcore_service_api_server.models._utils_pydantic import UriSchema
36

47

58
class Meta(BaseMeta):
6-
docs_url: AnyHttpUrl
7-
docs_dev_url: AnyHttpUrl
9+
docs_url: Annotated[HttpUrl, UriSchema()]
10+
docs_dev_url: Annotated[HttpUrl, UriSchema()]
811
model_config = ConfigDict(
912
json_schema_extra={
1013
"example": {

services/api-server/src/simcore_service_api_server/models/schemas/solvers.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from models_library.services_regex import COMPUTATIONAL_SERVICE_KEY_RE
88
from packaging.version import Version
99
from pydantic import BaseModel, ConfigDict, Field, HttpUrl, StringConstraints
10+
from simcore_service_api_server.models._utils_pydantic import UriSchema
1011

1112
from ..api_resources import compose_resource_name
1213
from ..basic_types import VersionStr
@@ -52,7 +53,11 @@ class Solver(BaseModel):
5253
# TODO: consider version_aliases: list[str] = [] # remaining tags
5354

5455
# Get links to other resources
55-
url: Annotated[HttpUrl | None, Field(..., description="Link to get this resource")]
56+
url: Annotated[
57+
Annotated[HttpUrl, UriSchema()] | None,
58+
Field(..., description="Link to get this resource"),
59+
]
60+
5661
model_config = ConfigDict(
5762
extra="ignore",
5863
json_schema_extra={

services/api-server/src/simcore_service_api_server/models/schemas/studies.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
from typing import TypeAlias
1+
from typing import Annotated, TypeAlias
22

33
from models_library import projects, projects_nodes_io
44
from pydantic import BaseModel, ConfigDict, Field, HttpUrl
5+
from simcore_service_api_server.models._utils_pydantic import UriSchema
56

67
from .. import api_resources
78
from . import solvers
89

910
StudyID: TypeAlias = projects.ProjectID
1011
NodeName: TypeAlias = str
11-
DownloadLink: TypeAlias = HttpUrl
12+
DownloadLink: TypeAlias = Annotated[HttpUrl, UriSchema()]
1213

1314

1415
class Study(BaseModel):
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# pylint: disable=redefined-outer-name
2+
# pylint: disable=unused-argument
3+
# pylint: disable=unused-variable
4+
# pylint: disable=too-many-arguments
5+
from typing import Annotated
6+
7+
from fastapi import FastAPI
8+
from pydantic import BaseModel, HttpUrl
9+
from simcore_service_api_server.models._utils_pydantic import UriSchema
10+
from simcore_service_api_server.models.schemas.files import FileUploadData
11+
12+
13+
def test_it():
14+
15+
schema = FileUploadData.model_json_schema()
16+
assert schema["properties"]["urls"]
17+
18+
19+
def test_annotated_url_in_pydantic_changes_in_fastapi():
20+
class TestModel(BaseModel):
21+
urls0: list[HttpUrl]
22+
urls1: list[Annotated[HttpUrl, UriSchema()]]
23+
24+
# with and w/o
25+
url0: HttpUrl
26+
url1: Annotated[HttpUrl, UriSchema()]
27+
28+
# # including None inside/outside annotated
29+
url2: Annotated[HttpUrl, UriSchema()] | None
30+
url3: Annotated[HttpUrl | None, UriSchema()]
31+
32+
# # mistake
33+
int0: Annotated[int, UriSchema()]
34+
35+
pydantic_schema = TestModel.model_json_schema()
36+
37+
assert pydantic_schema["properties"] == {
38+
"int0": {"title": "Int0", "type": "integer"},
39+
"url0": {
40+
"format": "uri",
41+
"maxLength": 2083,
42+
"minLength": 1,
43+
"title": "Url0",
44+
"type": "string",
45+
},
46+
"url1": {
47+
"format": "uri",
48+
"maxLength": 2083,
49+
"minLength": 1,
50+
"title": "Url1",
51+
"type": "string",
52+
},
53+
"url2": {
54+
"anyOf": [
55+
{"format": "uri", "maxLength": 2083, "minLength": 1, "type": "string"},
56+
{"type": "null"},
57+
],
58+
"title": "Url2",
59+
},
60+
"url3": {
61+
"anyOf": [
62+
{"format": "uri", "maxLength": 2083, "minLength": 1, "type": "string"},
63+
{"type": "null"},
64+
],
65+
"title": "Url3",
66+
},
67+
"urls0": {
68+
"items": {
69+
"format": "uri",
70+
"maxLength": 2083,
71+
"minLength": 1,
72+
"type": "string",
73+
},
74+
"title": "Urls0",
75+
"type": "array",
76+
},
77+
"urls1": {
78+
"items": {
79+
"format": "uri",
80+
"maxLength": 2083,
81+
"minLength": 1,
82+
"type": "string",
83+
},
84+
"title": "Urls1",
85+
"type": "array",
86+
},
87+
}
88+
89+
app = FastAPI()
90+
91+
@app.get("/", response_model=TestModel)
92+
def _h():
93+
...
94+
95+
openapi = app.openapi()
96+
fastapi_schema = openapi["components"]["schemas"]["TestModel"]
97+
98+
assert fastapi_schema["properties"] == {
99+
"int0": {"title": "Int0", "type": "integer"},
100+
"url0": {"title": "Url0", "type": "string"},
101+
"url1": {
102+
"format": "uri",
103+
"maxLength": 2083,
104+
"minLength": 1,
105+
"title": "Url1",
106+
"type": "string",
107+
},
108+
"url2": {
109+
"anyOf": [
110+
{"format": "uri", "maxLength": 2083, "minLength": 1, "type": "string"},
111+
{"type": "null"},
112+
],
113+
"title": "Url2",
114+
},
115+
"url3": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Url3"},
116+
"urls0": {"items": {"type": "string"}, "title": "Urls0", "type": "array"},
117+
"urls1": {
118+
"items": {
119+
"format": "uri",
120+
"maxLength": 2083,
121+
"minLength": 1,
122+
"type": "string",
123+
},
124+
"title": "Urls1",
125+
"type": "array",
126+
},
127+
}
128+
129+
# NOTE @all: I cannot understand this?!
130+
assert fastapi_schema["properties"] != pydantic_schema["properties"]

0 commit comments

Comments
 (0)