Skip to content

Commit b358c02

Browse files
author
Oleksandr Bazarnov
committed
add deprecation warnings
1 parent a73d162 commit b358c02

File tree

4 files changed

+147
-2
lines changed

4 files changed

+147
-2
lines changed

airbyte_cdk/sources/declarative/declarative_component_schema.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1883,6 +1883,7 @@ definitions:
18831883
enum: [HttpRequester]
18841884
url_base:
18851885
deprecated: true
1886+
deprecation_message: "Use `url` field instead."
18861887
sharable: true
18871888
title: API Base URL
18881889
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.
@@ -1922,6 +1923,7 @@ definitions:
19221923
- "https://example.com/api/v1/resource/{{ next_page_token['id'] }}"
19231924
path:
19241925
deprecated: true
1926+
deprecation_message: "Use `url` field instead."
19251927
title: URL Path
19261928
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.
19271929
type: string
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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
8+
9+
from pydantic.v1 import BaseModel
10+
11+
# format the warning message
12+
warnings.formatwarning = (
13+
lambda message, category, *args, **kwargs: f"{category.__name__}: {message}"
14+
)
15+
16+
FIELDS_TAG = "__fields__"
17+
DEPRECATED = "deprecated"
18+
DEPRECATION_MESSAGE = "deprecation_message"
19+
20+
21+
class BaseModelWithDeprecations(BaseModel):
22+
"""
23+
Pydantic BaseModel that warns when deprecated fields are accessed.
24+
"""
25+
26+
def _deprecated_warning(self, field_name: str, message: str) -> None:
27+
"""
28+
Show a warning message for deprecated fields (to stdout).
29+
Args:
30+
field_name (str): Name of the deprecated field.
31+
message (str): Warning message to be displayed.
32+
"""
33+
34+
warnings.warn(
35+
f"Component type: `{self.__class__.__name__}`. Field '{field_name}' is deprecated. {message}",
36+
DeprecationWarning,
37+
)
38+
39+
def __init__(self, **data: Any) -> None:
40+
"""
41+
Show warnings for deprecated fields during component initialization.
42+
"""
43+
44+
model_fields = self.__fields__
45+
46+
for field_name in data:
47+
if field_name in model_fields:
48+
if model_fields[field_name].field_info.extra.get(DEPRECATED, False):
49+
message = model_fields[field_name].field_info.extra.get(DEPRECATION_MESSAGE, "")
50+
self._deprecated_warning(field_name, message)
51+
52+
# Call the parent constructor
53+
super().__init__(**data)
54+
55+
def __getattribute__(self, name: str) -> Any:
56+
"""
57+
Show warnings for deprecated fields during field usage.
58+
"""
59+
60+
value = super().__getattribute__(name)
61+
62+
if name == FIELDS_TAG:
63+
try:
64+
model_fields = super().__getattribute__(FIELDS_TAG)
65+
field_info = model_fields.get(name)
66+
if field_info and field_info.field_info.extra.get(DEPRECATED):
67+
self._deprecated_warning(name, field_info)
68+
except (AttributeError, KeyError):
69+
pass
70+
71+
return value

airbyte_cdk/sources/declarative/models/declarative_component_schema.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88

99
from pydantic.v1 import BaseModel, Extra, Field
1010

11+
from airbyte_cdk.sources.declarative.models.base_model_with_deprecations import (
12+
BaseModelWithDeprecations,
13+
)
14+
1115

1216
class AuthFlowType(Enum):
1317
oauth2_0 = "oauth2.0"
@@ -2145,11 +2149,12 @@ class SessionTokenAuthenticator(BaseModel):
21452149
parameters: Optional[Dict[str, Any]] = Field(None, alias="$parameters")
21462150

21472151

2148-
class HttpRequester(BaseModel):
2152+
class HttpRequester(BaseModelWithDeprecations):
21492153
type: Literal["HttpRequester"]
21502154
url_base: Optional[str] = Field(
21512155
None,
21522156
deprecated=True,
2157+
deprecation_message="Use `url` field instead.",
21532158
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.",
21542159
examples=[
21552160
"https://connect.squareup.com/v2",
@@ -2173,6 +2178,7 @@ class HttpRequester(BaseModel):
21732178
path: Optional[str] = Field(
21742179
None,
21752180
deprecated=True,
2181+
deprecation_message="Use `url` field instead.",
21762182
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.",
21772183
examples=[
21782184
"/products",

bin/generate_component_manifest_files.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Copyright (c) 2024 Airbyte, Inc., all rights reserved.
22

3+
import re
34
import sys
45
from glob import glob
56
from pathlib import Path
@@ -28,6 +29,63 @@ def generate_init_module_content() -> str:
2829
return header
2930

3031

32+
def replace_base_model_for_classes_with_deprecated_fields(post_processed_content: str) -> str:
33+
"""
34+
Replace the base model for classes with deprecated fields.
35+
This function looks for classes that inherit from `BaseModel` and have fields marked as deprecated.
36+
It replaces the base model with `BaseModelWithDeprecations` for those classes.
37+
"""
38+
39+
# Find classes with deprecated fields
40+
classes_with_deprecated_fields = set()
41+
class_matches = re.finditer(r"class (\w+)\(BaseModel\):", post_processed_content)
42+
43+
for class_match in class_matches:
44+
class_name = class_match.group(1)
45+
class_start = class_match.start()
46+
# Find the next class definition or end of file
47+
next_class_match = re.search(
48+
r"class \w+\(",
49+
post_processed_content[class_start + len(class_match.group(0)) :],
50+
)
51+
class_end = (
52+
len(post_processed_content)
53+
if next_class_match is None
54+
else class_start + len(class_match.group(0)) + next_class_match.start()
55+
)
56+
class_content = post_processed_content[class_start:class_end]
57+
58+
# Check if any field has deprecated=True
59+
if re.search(r"deprecated\s*=\s*True", class_content):
60+
classes_with_deprecated_fields.add(class_name)
61+
62+
# update the imports to include the new base model with deprecation warinings
63+
# only if there are classes with the fields marked as deprecated.
64+
if len(classes_with_deprecated_fields) > 0:
65+
# Find where to insert the base model - after imports but before class definitions
66+
imports_end = post_processed_content.find(
67+
"\n\n",
68+
post_processed_content.find("from pydantic.v1 import"),
69+
)
70+
if imports_end > 0:
71+
post_processed_content = (
72+
post_processed_content[:imports_end]
73+
+ "\n\n"
74+
+ "from airbyte_cdk.sources.declarative.models.base_model_with_deprecations import (\n"
75+
+ " BaseModelWithDeprecations,\n"
76+
+ ")"
77+
+ post_processed_content[imports_end:]
78+
)
79+
80+
# Use the `BaseModelWithDeprecations` base model for the classes with deprecated fields
81+
for class_name in classes_with_deprecated_fields:
82+
pattern = rf"class {class_name}\(BaseModel\):"
83+
replacement = f"class {class_name}(BaseModelWithDeprecations):"
84+
post_processed_content = re.sub(pattern, replacement, post_processed_content)
85+
86+
return post_processed_content
87+
88+
3189
async def post_process_codegen(codegen_container: dagger.Container):
3290
codegen_container = codegen_container.with_exec(
3391
["mkdir", "/generated_post_processed"], use_entrypoint=True
@@ -41,6 +99,11 @@ async def post_process_codegen(codegen_container: dagger.Container):
4199
post_processed_content = original_content.replace(
42100
" _parameters:", " parameters:"
43101
).replace("from pydantic", "from pydantic.v1")
102+
103+
post_processed_content = replace_base_model_for_classes_with_deprecated_fields(
104+
post_processed_content
105+
)
106+
44107
codegen_container = codegen_container.with_new_file(
45108
f"/generated_post_processed/{generated_file}", contents=post_processed_content
46109
)
@@ -75,9 +138,12 @@ async def main():
75138
"--set-default-enum-member",
76139
"--use-double-quotes",
77140
"--remove-special-field-name-prefix",
78-
# account the `deprecated` flag provided for the field.
141+
# allow usage of the extra key such as `deprecated`, etc.
79142
"--field-extra-keys",
143+
# account the `deprecated` flag provided for the field.
80144
"deprecated",
145+
# account the `deprecation_message` provided for the field.
146+
"deprecation_message",
81147
],
82148
use_entrypoint=True,
83149
)

0 commit comments

Comments
 (0)