Skip to content

Commit b9f4736

Browse files
author
Baz
authored
fix: (CDK) (Manifest) - Add deprecations support and handle deprecation warnings; deprecate url_base, path, request_body_json and request_body_data for HttpRequester (#486)
1 parent 8ef954c commit b9f4736

File tree

16 files changed

+652
-48
lines changed

16 files changed

+652
-48
lines changed

airbyte_cdk/connector_builder/test_reader/reader.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55

66
import logging
7+
from math import log
78
from typing import Any, ClassVar, Dict, Iterator, List, Mapping, Optional, Union
89

910
from airbyte_cdk.connector_builder.models import (
@@ -112,11 +113,16 @@ def run_test_read(
112113
record_limit = self._check_record_limit(record_limit)
113114
# The connector builder currently only supports reading from a single stream at a time
114115
stream = source.streams(config)[0]
116+
117+
# get any deprecation warnings during the component creation
118+
deprecation_warnings: List[LogMessage] = source.deprecation_warnings()
119+
115120
schema_inferrer = SchemaInferrer(
116121
self._pk_to_nested_and_composite_field(stream.primary_key),
117122
self._cursor_field_to_nested_and_composite_field(stream.cursor_field),
118123
)
119124
datetime_format_inferrer = DatetimeFormatInferrer()
125+
120126
message_group = get_message_groups(
121127
self._read_stream(source, config, configured_catalog, state),
122128
schema_inferrer,
@@ -127,6 +133,10 @@ def run_test_read(
127133
slices, log_messages, auxiliary_requests, latest_config_update = self._categorise_groups(
128134
message_group
129135
)
136+
137+
# extend log messages with deprecation warnings
138+
log_messages.extend(deprecation_warnings)
139+
130140
schema, log_messages = self._get_infered_schema(
131141
configured_catalog, schema_inferrer, log_messages
132142
)
@@ -269,6 +279,7 @@ def _categorise_groups(self, message_groups: MESSAGE_GROUPS) -> GROUPED_MESSAGES
269279
auxiliary_requests = []
270280
latest_config_update: Optional[AirbyteControlMessage] = None
271281

282+
# process the message groups first
272283
for message_group in message_groups:
273284
match message_group:
274285
case AirbyteLogMessage():

airbyte_cdk/manifest_migrations/migrations/registry.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
#
44

55
manifest_migrations:
6-
- version: 6.47.1
6+
- version: 6.48.0
77
migrations:
88
- name: http_requester_url_base_to_url
99
order: 1

airbyte_cdk/sources/declarative/declarative_component_schema.yaml

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1911,15 +1911,16 @@ definitions:
19111911
type: object
19121912
required:
19131913
- type
1914-
- url_base
19151914
properties:
19161915
type:
19171916
type: string
19181917
enum: [HttpRequester]
19191918
url_base:
1920-
linkable: true
1919+
deprecated: true
1920+
deprecation_message: "Use `url` field instead."
19211921
title: API Base URL
1922-
description: The Base URL of the API source. Do not put sensitive information (e.g. API tokens) into this field - Use the Authentication component for this.
1922+
description: Deprecated, use the `url` instead. Base URL of the API source. Do not put sensitive information (e.g. API tokens) into this field - Use the Authentication component for this.
1923+
linkable: true
19231924
type: string
19241925
interpolation_context:
19251926
- config
@@ -1935,9 +1936,29 @@ definitions:
19351936
- "{{ config['base_url'] or 'https://app.posthog.com'}}/api"
19361937
- "https://connect.squareup.com/v2/quotes/{{ stream_partition['id'] }}/quote_line_groups"
19371938
- "https://example.com/api/v1/resource/{{ next_page_token['id'] }}"
1939+
url:
1940+
title: The URL of an API endpoint
1941+
description: The URL of the API source. Do not put sensitive information (e.g. API tokens) into this field - Use the Authentication component for this.
1942+
type: string
1943+
interpolation_context:
1944+
- config
1945+
- next_page_token
1946+
- stream_interval
1947+
- stream_partition
1948+
- stream_slice
1949+
- creation_response
1950+
- polling_response
1951+
- download_target
1952+
examples:
1953+
- "https://connect.squareup.com/v2"
1954+
- "{{ config['url'] or 'https://app.posthog.com'}}/api"
1955+
- "https://connect.squareup.com/v2/quotes/{{ stream_partition['id'] }}/quote_line_groups"
1956+
- "https://example.com/api/v1/resource/{{ next_page_token['id'] }}"
19381957
path:
1958+
deprecated: true
1959+
deprecation_message: "Use `url` field instead."
19391960
title: URL Path
1940-
description: The Path the specific API endpoint that this stream represents. Do not put sensitive information (e.g. API tokens) into this field - Use the Authentication component for this.
1961+
description: Deprecated, use the `url` instead. Path the specific API endpoint that this stream represents. Do not put sensitive information (e.g. API tokens) into this field - Use the Authentication component for this.
19411962
type: string
19421963
interpolation_context:
19431964
- config
@@ -1983,6 +2004,8 @@ definitions:
19832004
description: Allows for retrieving a dynamic set of properties from an API endpoint which can be injected into outbound request using the stream_partition.extra_fields.
19842005
"$ref": "#/definitions/PropertiesFromEndpoint"
19852006
request_body_data:
2007+
deprecated: true
2008+
deprecation_message: "Use `request_body` field instead."
19862009
title: Request Body Payload (Non-JSON)
19872010
description: Specifies how to populate the body of the request with a non-JSON payload. Plain text will be sent as is, whereas objects will be converted to a urlencoded form.
19882011
anyOf:
@@ -2001,6 +2024,8 @@ definitions:
20012024
[{"value": {{ stream_interval['start_time'] | int * 1000 }} }]
20022025
}, "orderBy": 1, "columnName": "Timestamp"}]/
20032026
request_body_json:
2027+
deprecated: true
2028+
deprecation_message: "Use `request_body` field instead."
20042029
title: Request Body JSON Payload
20052030
description: Specifies how to populate the body of the request with a JSON payload. Can contain nested objects.
20062031
anyOf:
@@ -2019,6 +2044,35 @@ definitions:
20192044
- sort:
20202045
field: "updated_at"
20212046
order: "ascending"
2047+
request_body:
2048+
title: Request Body Payload to be send as a part of the API request.
2049+
description: Specifies how to populate the body of the request with a payload. Can contain nested objects.
2050+
anyOf:
2051+
- "$ref": "#/definitions/RequestBody"
2052+
interpolation_context:
2053+
- next_page_token
2054+
- stream_interval
2055+
- stream_partition
2056+
- stream_slice
2057+
examples:
2058+
- type: RequestBodyJson
2059+
value:
2060+
sort_order: "ASC"
2061+
sort_field: "CREATED_AT"
2062+
- type: RequestBodyJson
2063+
value:
2064+
key: "{{ config['value'] }}"
2065+
- type: RequestBodyJson
2066+
value:
2067+
sort:
2068+
field: "updated_at"
2069+
order: "ascending"
2070+
- type: RequestBodyData
2071+
value: "plain_text_body"
2072+
- type: RequestBodyData
2073+
value:
2074+
param1: "value1"
2075+
param2: "{{ config['param2_value'] }}"
20222076
request_headers:
20232077
title: Request Headers
20242078
description: Return any non-auth headers. Authentication headers will overwrite any overlapping headers returned from this method.
@@ -4019,6 +4073,27 @@ definitions:
40194073
- type
40204074
- stream_template
40214075
- components_resolver
4076+
RequestBody:
4077+
type: object
4078+
description: The request body payload. Can be either URL encoded data or JSON.
4079+
properties:
4080+
type:
4081+
anyOf:
4082+
- type: string
4083+
enum: [RequestBodyData]
4084+
- type: string
4085+
enum: [RequestBodyJson]
4086+
value:
4087+
anyOf:
4088+
- type: string
4089+
description: The request body payload as a string.
4090+
- type: object
4091+
description: The request body payload as a Non-JSON object (url-encoded data).
4092+
additionalProperties:
4093+
type: string
4094+
- type: object
4095+
description: The request body payload as a JSON object (json-encoded data).
4096+
additionalProperties: true
40224097
interpolation:
40234098
variables:
40244099
- title: config
@@ -4227,4 +4302,4 @@ interpolation:
42274302
regex: The regular expression to search for. It must include a capture group.
42284303
return_type: str
42294304
examples:
4230-
- '{{ "goodbye, cruel world" | regex_search("goodbye,\s(.*)$") }} -> "cruel world"'
4305+
- '{{ "goodbye, cruel world" | regex_search("goodbye,\s(.*)$") }} -> "cruel world"'

airbyte_cdk/sources/declarative/declarative_source.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44

55
import logging
66
from abc import abstractmethod
7-
from typing import Any, Mapping, Tuple
7+
from typing import Any, List, Mapping, Tuple
88

9+
from airbyte_cdk.connector_builder.models import (
10+
LogMessage as ConnectorBuilderLogMessage,
11+
)
912
from airbyte_cdk.sources.abstract_source import AbstractSource
1013
from airbyte_cdk.sources.declarative.checks.connection_checker import ConnectionChecker
1114

@@ -34,3 +37,9 @@ def check_connection(
3437
The error object will be cast to string to display the problem to the user.
3538
"""
3639
return self.connection_checker.check_connection(self, logger, config)
40+
41+
def deprecation_warnings(self) -> List[ConnectorBuilderLogMessage]:
42+
"""
43+
Returns a list of deprecation warnings for the source.
44+
"""
45+
return []

airbyte_cdk/sources/declarative/interpolation/interpolated_nested_mapping.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
NestedMappingEntry = Union[
1313
dict[str, "NestedMapping"], list["NestedMapping"], str, int, float, bool, None
1414
]
15-
NestedMapping = Union[dict[str, NestedMappingEntry], str]
15+
NestedMapping = Union[dict[str, NestedMappingEntry], str, dict[str, Any]]
1616

1717

1818
@dataclass

airbyte_cdk/sources/declarative/manifest_declarative_source.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
from jsonschema.validators import validate
1616
from packaging.version import InvalidVersion, Version
1717

18+
from airbyte_cdk.connector_builder.models import (
19+
LogMessage as ConnectorBuilderLogMessage,
20+
)
1821
from airbyte_cdk.manifest_migrations.migration_handler import (
1922
ManifestMigrationHandler,
2023
)
@@ -230,6 +233,9 @@ def dynamic_streams(self) -> List[Dict[str, Any]]:
230233
with_dynamic_stream_name=True,
231234
)
232235

236+
def deprecation_warnings(self) -> List[ConnectorBuilderLogMessage]:
237+
return self._constructor.get_model_deprecations()
238+
233239
@property
234240
def connection_checker(self) -> ConnectionChecker:
235241
check = self._source_config["check"]
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
2+
3+
# THIS IS A STATIC CLASS MODEL USED TO DISPLAY DEPRECATION WARNINGS
4+
# WHEN DEPRECATED FIELDS ARE ACCESSED
5+
6+
import warnings
7+
from typing import Any, List
8+
9+
from pydantic.v1 import BaseModel
10+
11+
from airbyte_cdk.connector_builder.models import LogMessage as ConnectorBuilderLogMessage
12+
13+
# format the warning message
14+
warnings.formatwarning = (
15+
lambda message, category, *args, **kwargs: f"{category.__name__}: {message}"
16+
)
17+
18+
FIELDS_TAG = "__fields__"
19+
DEPRECATED = "deprecated"
20+
DEPRECATION_MESSAGE = "deprecation_message"
21+
DEPRECATION_LOGS_TAG = "_deprecation_logs"
22+
23+
24+
class BaseModelWithDeprecations(BaseModel):
25+
"""
26+
Pydantic BaseModel that warns when deprecated fields are accessed.
27+
The deprecation message is stored in the field's extra attributes.
28+
This class is used to create models that can have deprecated fields
29+
and show warnings when those fields are accessed or initialized.
30+
31+
The `_deprecation_logs` attribute is stored in the model itself.
32+
The collected deprecation warnings are further propagated to the Airbyte log messages,
33+
during the component creation process, in `model_to_component._collect_model_deprecations()`.
34+
35+
The component implementation is not responsible for handling the deprecation warnings,
36+
since the deprecation warnings are already handled in the model itself.
37+
"""
38+
39+
class Config:
40+
"""
41+
Allow extra fields in the model. In case the model restricts extra fields.
42+
"""
43+
44+
extra = "allow"
45+
46+
def __init__(self, **model_fields: Any) -> None:
47+
"""
48+
Show warnings for deprecated fields during component initialization.
49+
"""
50+
# call the parent constructor first to initialize Pydantic internals
51+
super().__init__(**model_fields)
52+
# set the placeholder for the default deprecation messages
53+
self._default_deprecation_messages: List[str] = []
54+
# set the placeholder for the deprecation logs
55+
self._deprecation_logs: List[ConnectorBuilderLogMessage] = []
56+
# process deprecated fields, if present
57+
self._process_fields(model_fields)
58+
# emit default deprecation messages
59+
self._emit_default_deprecation_messages()
60+
# set the deprecation logs attribute to the model
61+
self._set_deprecation_logs_attr_to_model()
62+
63+
def _is_deprecated_field(self, field_name: str) -> bool:
64+
return (
65+
self.__fields__[field_name].field_info.extra.get(DEPRECATED, False)
66+
if field_name in self.__fields__.keys()
67+
else False
68+
)
69+
70+
def _get_deprecation_message(self, field_name: str) -> str:
71+
return (
72+
self.__fields__[field_name].field_info.extra.get(
73+
DEPRECATION_MESSAGE, "<missing_deprecation_message>"
74+
)
75+
if field_name in self.__fields__.keys()
76+
else "<missing_deprecation_message>"
77+
)
78+
79+
def _process_fields(self, model_fields: Any) -> None:
80+
"""
81+
Processes the fields in the provided model data, checking for deprecated fields.
82+
83+
For each field in the input `model_fields`, this method checks if the field exists in the model's defined fields.
84+
If the field is marked as deprecated (using the `DEPRECATED` flag in its metadata), it triggers a deprecation warning
85+
by calling the `_create_warning` method with the field name and an optional deprecation message.
86+
87+
Args:
88+
model_fields (Any): The data containing fields to be processed.
89+
90+
Returns:
91+
None
92+
"""
93+
94+
if hasattr(self, FIELDS_TAG):
95+
for field_name in model_fields.keys():
96+
if self._is_deprecated_field(field_name):
97+
self._create_warning(
98+
field_name,
99+
self._get_deprecation_message(field_name),
100+
)
101+
102+
def _set_deprecation_logs_attr_to_model(self) -> None:
103+
"""
104+
Sets the deprecation logs attribute on the model instance.
105+
106+
This method attaches the current instance's deprecation logs to the model by setting
107+
an attribute named by `DEPRECATION_LOGS_TAG` to the value of `self._deprecation_logs`.
108+
This is typically used to track or log deprecated features or configurations within the model.
109+
110+
Returns:
111+
None
112+
"""
113+
setattr(self, DEPRECATION_LOGS_TAG, self._deprecation_logs)
114+
115+
def _create_warning(self, field_name: str, message: str) -> None:
116+
"""
117+
Show a warning message for deprecated fields (to stdout).
118+
Args:
119+
field_name (str): Name of the deprecated field.
120+
message (str): Warning message to be displayed.
121+
"""
122+
123+
deprecated_message = f"Component type: `{self.__class__.__name__}`. Field '{field_name}' is deprecated. {message}"
124+
125+
if deprecated_message not in self._default_deprecation_messages:
126+
# Avoid duplicates in the default deprecation messages
127+
self._default_deprecation_messages.append(deprecated_message)
128+
129+
# Create an Airbyte deprecation log message
130+
deprecation_log_message = ConnectorBuilderLogMessage(
131+
level="WARN", message=deprecated_message
132+
)
133+
# Add the deprecation message to the Airbyte log messages,
134+
# this logs are displayed in the Connector Builder.
135+
if deprecation_log_message not in self._deprecation_logs:
136+
# Avoid duplicates in the deprecation logs
137+
self._deprecation_logs.append(deprecation_log_message)
138+
139+
def _emit_default_deprecation_messages(self) -> None:
140+
"""
141+
Emit default deprecation messages for deprecated fields to STDOUT.
142+
"""
143+
for message in self._default_deprecation_messages:
144+
warnings.warn(message, DeprecationWarning)

0 commit comments

Comments
 (0)