Skip to content

Commit c4ff128

Browse files
fix: update jsonschema constraint to allow 4.20.0+ for fastmcp compatibility (#737)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 9ef1a3d commit c4ff128

File tree

8 files changed

+289
-89
lines changed

8 files changed

+289
-89
lines changed

airbyte_cdk/manifest_server/Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ FROM python:3.12-slim-bookworm
1111
RUN apt-get update && \
1212
apt-get install -y git && \
1313
rm -rf /var/lib/apt/lists/* && \
14-
pip install poetry==1.8.3
14+
pip install poetry==2.0.1
1515

1616
# Configure poetry to not create virtual environments and disable interactive mode
1717
ENV POETRY_NO_INTERACTION=1 \
@@ -42,4 +42,4 @@ USER airbyte:airbyte
4242

4343
EXPOSE 8080
4444

45-
CMD ["uvicorn", "airbyte_cdk.manifest_server.app:app", "--host", "0.0.0.0", "--port", "8080"]
45+
CMD ["uvicorn", "airbyte_cdk.manifest_server.app:app", "--host", "0.0.0.0", "--port", "8080"]

airbyte_cdk/sources/utils/schema_helpers.py

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,16 @@
77
import json
88
import os
99
import pkgutil
10-
from typing import Any, ClassVar, Dict, List, Mapping, MutableMapping, Optional, Tuple
10+
from copy import deepcopy
11+
from typing import TYPE_CHECKING, Any, ClassVar, Dict, List, Mapping, MutableMapping, Tuple, cast
1112

1213
import jsonref
13-
from jsonschema import RefResolver, validate
14+
from jsonschema import validate
1415
from jsonschema.exceptions import ValidationError
1516
from pydantic.v1 import BaseModel, Field
17+
from referencing import Registry, Resource
18+
from referencing._core import Resolver # used for type hints
19+
from referencing.jsonschema import DRAFT7
1620

1721
from airbyte_cdk.models import ConnectorSpecification, FailureType
1822
from airbyte_cdk.utils.traced_exception import AirbyteTracedException
@@ -63,18 +67,30 @@ def resolve_ref_links(obj: Any) -> Any:
6367
return obj
6468

6569

66-
def _expand_refs(schema: Any, ref_resolver: Optional[RefResolver] = None) -> None:
70+
def get_ref_resolver_registry(schema: dict[str, Any]) -> Registry:
71+
"""Get a reference resolver registry for the given schema."""
72+
resource: Resource = Resource.from_contents(
73+
contents=schema,
74+
default_specification=DRAFT7,
75+
)
76+
return cast( # Mypy has a hard time detecting this return type.
77+
"Registry",
78+
Registry().with_resource(
79+
uri="",
80+
resource=resource,
81+
),
82+
)
83+
84+
85+
def _expand_refs(schema: Any, ref_resolver: Resolver) -> None:
6786
"""Internal function to iterate over schema and replace all occurrences of $ref with their definitions. Recursive.
6887
6988
:param schema: schema that will be patched
70-
:param ref_resolver: resolver to get definition from $ref, if None pass it will be instantiated
7189
"""
72-
ref_resolver = ref_resolver or RefResolver.from_schema(schema)
73-
7490
if isinstance(schema, MutableMapping):
7591
if "$ref" in schema:
7692
ref_url = schema.pop("$ref")
77-
_, definition = ref_resolver.resolve(ref_url)
93+
definition = ref_resolver.lookup(ref_url).contents
7894
_expand_refs(
7995
definition, ref_resolver=ref_resolver
8096
) # expand refs in definitions as well
@@ -90,10 +106,14 @@ def _expand_refs(schema: Any, ref_resolver: Optional[RefResolver] = None) -> Non
90106
def expand_refs(schema: Any) -> None:
91107
"""Iterate over schema and replace all occurrences of $ref with their definitions.
92108
109+
If a "definitions" section is present at the root of the schema, it will be removed
110+
after $ref resolution is complete.
111+
93112
:param schema: schema that will be patched
94113
"""
95-
_expand_refs(schema)
96-
schema.pop("definitions", None) # remove definitions created by $ref
114+
ref_resolver = get_ref_resolver_registry(schema).resolver()
115+
_expand_refs(schema, ref_resolver)
116+
schema.pop("definitions", None)
97117

98118

99119
def rename_key(schema: Any, old_key: str, new_key: str) -> None:

airbyte_cdk/sources/utils/transform.py

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,25 @@
33
#
44

55
import logging
6+
from copy import deepcopy
67
from enum import Flag, auto
7-
from typing import Any, Callable, Dict, Generator, Mapping, Optional, cast
8+
from typing import TYPE_CHECKING, Any, Callable, Dict, Generator, Mapping, Optional, cast
9+
10+
from jsonschema import Draft7Validator, ValidationError, validators
11+
from referencing import Registry, Resource
12+
from referencing._core import Resolver
13+
from referencing.exceptions import Unresolvable
14+
from referencing.jsonschema import DRAFT7
15+
16+
from airbyte_cdk.sources.utils.schema_helpers import expand_refs
17+
18+
from .schema_helpers import get_ref_resolver_registry
19+
20+
try:
21+
from jsonschema.validators import Validator
22+
except:
23+
from jsonschema import Validator
824

9-
from jsonschema import Draft7Validator, RefResolver, ValidationError, Validator, validators
1025

1126
MAX_NESTING_DEPTH = 3
1227
json_to_python_simple = {
@@ -191,30 +206,27 @@ def normalizator(
191206
validators parameter for detailed description.
192207
:
193208
"""
209+
# Very first step is to expand $refs in the schema itself:
210+
expand_refs(schema)
211+
212+
# Now we can expand $refs in the property value:
213+
if isinstance(property_value, dict):
214+
expand_refs(property_value)
194215

195-
def resolve(subschema: dict[str, Any]) -> dict[str, Any]:
196-
if "$ref" in subschema:
197-
_, resolved = cast(
198-
RefResolver,
199-
validator_instance.resolver,
200-
).resolve(subschema["$ref"])
201-
return cast(dict[str, Any], resolved)
202-
return subschema
216+
# Now we can validate and normalize the values:
203217

204218
# Transform object and array values before running json schema type checking for each element.
205219
# Recursively normalize every value of the "instance" sub-object,
206220
# if "instance" is an incorrect type - skip recursive normalization of "instance"
207221
if schema_key == "properties" and isinstance(instance, dict):
208222
for k, subschema in property_value.items():
209223
if k in instance:
210-
subschema = resolve(subschema)
211224
instance[k] = self.__normalize(instance[k], subschema)
212225
# Recursively normalize every item of the "instance" sub-array,
213226
# if "instance" is an incorrect type - skip recursive normalization of "instance"
214227
elif schema_key == "items" and isinstance(instance, list):
215-
subschema = resolve(property_value)
216228
for index, item in enumerate(instance):
217-
instance[index] = self.__normalize(item, subschema)
229+
instance[index] = self.__normalize(item, property_value)
218230

219231
# Running native jsonschema traverse algorithm after field normalization is done.
220232
yield from original_validator(

airbyte_cdk/utils/spec_schema_transformations.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,23 @@
66
import re
77
from typing import Any
88

9-
from jsonschema import RefResolver
9+
from referencing import Registry, Resource
10+
from referencing.jsonschema import DRAFT7
1011

1112

1213
def resolve_refs(schema: dict[str, Any]) -> dict[str, Any]:
1314
"""
1415
For spec schemas generated using Pydantic models, the resulting JSON schema can contain refs between object
1516
relationships.
1617
"""
17-
json_schema_ref_resolver = RefResolver.from_schema(schema)
18+
resource = Resource.from_contents(schema, default_specification=DRAFT7)
19+
registry = Registry().with_resource("", resource)
20+
resolver = registry.resolver()
1821
str_schema = json.dumps(schema)
1922
for ref_block in re.findall(r'{"\$ref": "#\/definitions\/.+?(?="})"}', str_schema):
2023
ref = json.loads(ref_block)["$ref"]
21-
str_schema = str_schema.replace(
22-
ref_block, json.dumps(json_schema_ref_resolver.resolve(ref)[1])
23-
)
24+
resolved = resolver.lookup(ref).contents
25+
str_schema = str_schema.replace(ref_block, json.dumps(resolved))
2426
pyschema: dict[str, Any] = json.loads(str_schema)
2527
del pyschema["definitions"]
2628
return pyschema

0 commit comments

Comments
 (0)