Skip to content

Commit c986bdc

Browse files
author
Rusi Popov
committed
Merge remote-tracking branch 'remotes/airbyte/main' into 50395-2-2
* remotes/airbyte/main: fix(airbyte-cdk): Fix RequestOptionsProvider for PerPartitionWithGlobalCursor (airbytehq#254) feat(low-code): add profile assertion flow to oauth authenticator component (airbytehq#236) feat(Low-Code Concurrent CDK): Add ConcurrentPerPartitionCursor (airbytehq#111) fix: don't mypy unit_tests (airbytehq#241) fix: handle backoff_strategies in CompositeErrorHandler (airbytehq#225) feat(concurrent cursor): attempt at clamping datetime (airbytehq#234) ci: use `ubuntu-24.04` explicitly (resolves CI warnings) (airbytehq#244) Fix(sdm): module ref issue in python components import (airbytehq#243) feat(source-declarative-manifest): add support for custom Python components from dynamic text input (airbytehq#174) chore(deps): bump avro from 1.11.3 to 1.12.0 (airbytehq#133) docs: comments on what the `Dockerfile` is for (airbytehq#240) chore: move ruff configuration to dedicated ruff.toml file (airbytehq#237)
2 parents 2381916 + ec7e961 commit c986bdc

File tree

60 files changed

+2005836
-223
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+2005836
-223
lines changed

.github/workflows/connector-tests.yml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ concurrency:
2525
jobs:
2626
cdk_changes:
2727
name: Get Changes
28-
runs-on: ubuntu-latest
28+
runs-on: ubuntu-24.04
2929
permissions:
3030
statuses: write
3131
pull-requests: read
@@ -62,7 +62,7 @@ jobs:
6262
# Forked PRs are handled by the community_ci.yml workflow
6363
# If the condition is not met the job will be skipped (it will not fail)
6464
# runs-on: connector-test-large
65-
runs-on: ubuntu-latest
65+
runs-on: ubuntu-24.04
6666
timeout-minutes: 360 # 6 hours
6767
strategy:
6868
fail-fast: false
@@ -123,6 +123,10 @@ jobs:
123123
repository: airbytehq/airbyte
124124
ref: master
125125
path: airbyte
126+
- name: Set up Python
127+
uses: actions/setup-python@v5
128+
with:
129+
python-version: "3.10"
126130
- name: Test Connector
127131
if: steps.no_changes.outputs.status != 'cancelled'
128132
timeout-minutes: 90
@@ -131,7 +135,7 @@ jobs:
131135
POETRY_DYNAMIC_VERSIONING_BYPASS: "0.0.0"
132136
run: |
133137
cd airbyte
134-
make tools.airbyte-ci-binary.install
138+
make tools.airbyte-ci-dev.install
135139
airbyte-ci \
136140
--ci-report-bucket-name=airbyte-ci-reports-multi \
137141
connectors \

.github/workflows/pdoc_preview.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ on:
88

99
jobs:
1010
preview_docs:
11-
runs-on: ubuntu-latest
11+
runs-on: ubuntu-24.04
1212

1313
steps:
1414
- name: Checkout code

.github/workflows/pdoc_publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ concurrency:
2222

2323
jobs:
2424
publish_docs:
25-
runs-on: ubuntu-latest
25+
runs-on: ubuntu-24.04
2626
environment:
2727
name: "github-pages"
2828
url: ${{ steps.deployment.outputs.page_url }}

.github/workflows/pypi_publish.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ on:
3535
jobs:
3636
build:
3737
name: Build Python Package
38-
runs-on: ubuntu-latest
38+
runs-on: ubuntu-24.04
3939
steps:
4040
- name: Detect Release Tag Version
4141
if: startsWith(github.ref, 'refs/tags/v')
@@ -107,7 +107,7 @@ jobs:
107107

108108
publish_cdk:
109109
name: Publish CDK version to PyPI
110-
runs-on: ubuntu-latest
110+
runs-on: ubuntu-24.04
111111
needs: [build]
112112
permissions:
113113
id-token: write
@@ -156,7 +156,7 @@ jobs:
156156
(github.event_name == 'workflow_dispatch' &&
157157
github.event.inputs.publish_to_dockerhub == 'true'
158158
)
159-
runs-on: ubuntu-latest
159+
runs-on: ubuntu-24.04
160160
needs: [build]
161161
environment:
162162
name: DockerHub
@@ -257,7 +257,7 @@ jobs:
257257
env:
258258
VERSION: ${{ needs.build.outputs.VERSION }}
259259
IS_PRERELEASE: ${{ needs.build.outputs.IS_PRERELEASE }}
260-
runs-on: ubuntu-latest
260+
runs-on: ubuntu-24.04
261261
steps:
262262
- uses: actions/setup-python@v5
263263
with:

.github/workflows/pytest_fast.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ on:
99
jobs:
1010
test-build:
1111
name: Build and Inspect Python Package
12-
runs-on: ubuntu-latest
12+
runs-on: ubuntu-24.04
1313
steps:
1414
- name: Checkout code
1515
uses: actions/checkout@v4
@@ -36,7 +36,7 @@ jobs:
3636
3737
pytest-fast:
3838
name: Pytest (Fast)
39-
runs-on: ubuntu-latest
39+
runs-on: ubuntu-24.04
4040
steps:
4141
# Common steps:
4242
- name: Checkout code

.github/workflows/python_lint.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ on:
99
jobs:
1010
ruff-lint-check:
1111
name: Ruff Lint Check
12-
runs-on: ubuntu-latest
12+
runs-on: ubuntu-24.04
1313
steps:
1414
# Common steps:
1515
- name: Checkout code
@@ -32,7 +32,7 @@ jobs:
3232

3333
ruff-format-check:
3434
name: Ruff Format Check
35-
runs-on: ubuntu-latest
35+
runs-on: ubuntu-24.04
3636
steps:
3737
# Common steps:
3838
- name: Checkout code
@@ -55,7 +55,7 @@ jobs:
5555

5656
mypy-check:
5757
name: MyPy Check
58-
runs-on: ubuntu-latest
58+
runs-on: ubuntu-24.04
5959
steps:
6060
# Common steps:
6161
- name: Checkout code

.github/workflows/release_drafter.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
permissions:
1717
contents: write
1818
pull-requests: write
19-
runs-on: ubuntu-latest
19+
runs-on: ubuntu-24.04
2020
steps:
2121
# Drafts the next Release notes as Pull Requests are merged into "main"
2222
- uses: release-drafter/release-drafter@v6

.github/workflows/semantic_pr_check.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ permissions:
1414
jobs:
1515
validate_pr_title:
1616
name: Validate PR title
17-
runs-on: ubuntu-latest
17+
runs-on: ubuntu-24.04
1818
steps:
1919
- uses: amannn/action-semantic-pull-request@v5
2020
if: ${{ github.event.pull_request.draft == false }}

.github/workflows/slash_command_dispatch.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jobs:
88
slashCommandDispatch:
99
# Only allow slash commands on pull request (not on issues)
1010
if: ${{ github.event.issue.pull_request }}
11-
runs-on: ubuntu-latest
11+
runs-on: ubuntu-24.04
1212
steps:
1313
- name: Slash Command Dispatch
1414
id: dispatch

.github/workflows/test-command.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ on:
1515
jobs:
1616
start-workflow:
1717
name: Append 'Starting' Comment
18-
runs-on: ubuntu-latest
18+
runs-on: ubuntu-24.04
1919
steps:
2020
- name: Get PR JSON
2121
id: pr-info
@@ -127,7 +127,7 @@ jobs:
127127
log-success-comment:
128128
name: Append 'Success' Comment
129129
needs: [pytest-on-demand]
130-
runs-on: ubuntu-latest
130+
runs-on: ubuntu-24.04
131131
steps:
132132
- name: Append success comment
133133
uses: peter-evans/create-or-update-comment@v4
@@ -143,7 +143,7 @@ jobs:
143143
# This job will only run if the workflow fails
144144
needs: [pytest-on-demand, start-workflow]
145145
if: always() && needs.pytest-on-demand.result == 'failure'
146-
runs-on: ubuntu-latest
146+
runs-on: ubuntu-24.04
147147
steps:
148148
- name: Append failure comment
149149
uses: peter-evans/create-or-update-comment@v4

Dockerfile

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
# This Dockerfile is used to build `airbyte/source-declarative-manifest` image that in turn is used
2+
# 1. to build Manifest-only connectors themselves
3+
# 2. to run manifest (Builder) connectors published into a particular user's workspace in Airbyte
4+
#
5+
# A new version of source-declarative-manifest is built for every new Airbyte CDK release, and their versions are kept in sync.
6+
#
7+
18
FROM docker.io/airbyte/python-connector-base:3.0.0@sha256:1a0845ff2b30eafa793c6eee4e8f4283c2e52e1bbd44eed6cb9e9abd5d34d844
29

310
WORKDIR /airbyte/integration_code

airbyte_cdk/cli/source_declarative_manifest/_run.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,12 @@ def create_declarative_source(
171171
"Invalid config: `__injected_declarative_manifest` should be provided at the root "
172172
f"of the config but config only has keys: {list(config.keys() if config else [])}"
173173
)
174+
if not isinstance(config["__injected_declarative_manifest"], dict):
175+
raise ValueError(
176+
"Invalid config: `__injected_declarative_manifest` should be a dictionary, "
177+
f"but got type: {type(config['__injected_declarative_manifest'])}"
178+
)
179+
174180
return ConcurrentDeclarativeSource(
175181
config=config,
176182
catalog=catalog,

airbyte_cdk/connector_builder/connector_builder_handler.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ def get_limits(config: Mapping[str, Any]) -> TestReadLimits:
5252
def create_source(config: Mapping[str, Any], limits: TestReadLimits) -> ManifestDeclarativeSource:
5353
manifest = config["__injected_declarative_manifest"]
5454
return ManifestDeclarativeSource(
55+
config=config,
5556
emit_connector_builder_messages=True,
5657
source_config=manifest,
5758
component_factory=ModelToComponentFactory(

airbyte_cdk/sources/declarative/auth/oauth.py

Lines changed: 68 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
#
44

55
from dataclasses import InitVar, dataclass, field
6-
from typing import Any, List, Mapping, Optional, Union
6+
from typing import Any, List, Mapping, MutableMapping, Optional, Union
77

88
import pendulum
99

1010
from airbyte_cdk.sources.declarative.auth.declarative_authenticator import DeclarativeAuthenticator
11+
from airbyte_cdk.sources.declarative.interpolation.interpolated_boolean import InterpolatedBoolean
1112
from airbyte_cdk.sources.declarative.interpolation.interpolated_mapping import InterpolatedMapping
1213
from airbyte_cdk.sources.declarative.interpolation.interpolated_string import InterpolatedString
1314
from airbyte_cdk.sources.message import MessageRepository, NoopMessageRepository
@@ -44,10 +45,10 @@ class DeclarativeOauth2Authenticator(AbstractOauth2Authenticator, DeclarativeAut
4445
message_repository (MessageRepository): the message repository used to emit logs on HTTP requests
4546
"""
4647

47-
client_id: Union[InterpolatedString, str]
48-
client_secret: Union[InterpolatedString, str]
4948
config: Mapping[str, Any]
5049
parameters: InitVar[Mapping[str, Any]]
50+
client_id: Optional[Union[InterpolatedString, str]] = None
51+
client_secret: Optional[Union[InterpolatedString, str]] = None
5152
token_refresh_endpoint: Optional[Union[InterpolatedString, str]] = None
5253
refresh_token: Optional[Union[InterpolatedString, str]] = None
5354
scopes: Optional[List[str]] = None
@@ -66,6 +67,8 @@ class DeclarativeOauth2Authenticator(AbstractOauth2Authenticator, DeclarativeAut
6667
grant_type_name: Union[InterpolatedString, str] = "grant_type"
6768
grant_type: Union[InterpolatedString, str] = "refresh_token"
6869
message_repository: MessageRepository = NoopMessageRepository()
70+
profile_assertion: Optional[DeclarativeAuthenticator] = None
71+
use_profile_assertion: Optional[Union[InterpolatedBoolean, str, bool]] = False
6972

7073
def __post_init__(self, parameters: Mapping[str, Any]) -> None:
7174
super().__init__()
@@ -76,11 +79,19 @@ def __post_init__(self, parameters: Mapping[str, Any]) -> None:
7679
else:
7780
self._token_refresh_endpoint = None
7881
self._client_id_name = InterpolatedString.create(self.client_id_name, parameters=parameters)
79-
self._client_id = InterpolatedString.create(self.client_id, parameters=parameters)
82+
self._client_id = (
83+
InterpolatedString.create(self.client_id, parameters=parameters)
84+
if self.client_id
85+
else self.client_id
86+
)
8087
self._client_secret_name = InterpolatedString.create(
8188
self.client_secret_name, parameters=parameters
8289
)
83-
self._client_secret = InterpolatedString.create(self.client_secret, parameters=parameters)
90+
self._client_secret = (
91+
InterpolatedString.create(self.client_secret, parameters=parameters)
92+
if self.client_secret
93+
else self.client_secret
94+
)
8495
self._refresh_token_name = InterpolatedString.create(
8596
self.refresh_token_name, parameters=parameters
8697
)
@@ -99,7 +110,12 @@ def __post_init__(self, parameters: Mapping[str, Any]) -> None:
99110
self.grant_type_name = InterpolatedString.create(
100111
self.grant_type_name, parameters=parameters
101112
)
102-
self.grant_type = InterpolatedString.create(self.grant_type, parameters=parameters)
113+
self.grant_type = InterpolatedString.create(
114+
"urn:ietf:params:oauth:grant-type:jwt-bearer"
115+
if self.use_profile_assertion
116+
else self.grant_type,
117+
parameters=parameters,
118+
)
103119
self._refresh_request_body = InterpolatedMapping(
104120
self.refresh_request_body or {}, parameters=parameters
105121
)
@@ -115,6 +131,13 @@ def __post_init__(self, parameters: Mapping[str, Any]) -> None:
115131
if self.token_expiry_date
116132
else pendulum.now().subtract(days=1) # type: ignore # substract does not have type hints
117133
)
134+
self.use_profile_assertion = (
135+
InterpolatedBoolean(self.use_profile_assertion, parameters=parameters)
136+
if isinstance(self.use_profile_assertion, str)
137+
else self.use_profile_assertion
138+
)
139+
self.assertion_name = "assertion"
140+
118141
if self.access_token_value is not None:
119142
self._access_token_value = InterpolatedString.create(
120143
self.access_token_value, parameters=parameters
@@ -126,9 +149,20 @@ def __post_init__(self, parameters: Mapping[str, Any]) -> None:
126149
self._access_token_value if self.access_token_value else None
127150
)
128151

152+
if not self.use_profile_assertion and any(
153+
client_creds is None for client_creds in [self.client_id, self.client_secret]
154+
):
155+
raise ValueError(
156+
"OAuthAuthenticator configuration error: Both 'client_id' and 'client_secret' are required for the "
157+
"basic OAuth flow."
158+
)
159+
if self.profile_assertion is None and self.use_profile_assertion:
160+
raise ValueError(
161+
"OAuthAuthenticator configuration error: 'profile_assertion' is required when using the profile assertion flow."
162+
)
129163
if self.get_grant_type() == "refresh_token" and self._refresh_token is None:
130164
raise ValueError(
131-
"OAuthAuthenticator needs a refresh_token parameter if grant_type is set to `refresh_token`"
165+
"OAuthAuthenticator configuration error: A 'refresh_token' is required when the 'grant_type' is set to 'refresh_token'."
132166
)
133167

134168
def get_token_refresh_endpoint(self) -> Optional[str]:
@@ -145,19 +179,21 @@ def get_client_id_name(self) -> str:
145179
return self._client_id_name.eval(self.config) # type: ignore # eval returns a string in this context
146180

147181
def get_client_id(self) -> str:
148-
client_id: str = self._client_id.eval(self.config)
182+
client_id = self._client_id.eval(self.config) if self._client_id else self._client_id
149183
if not client_id:
150184
raise ValueError("OAuthAuthenticator was unable to evaluate client_id parameter")
151-
return client_id
185+
return client_id # type: ignore # value will be returned as a string, or an error will be raised
152186

153187
def get_client_secret_name(self) -> str:
154188
return self._client_secret_name.eval(self.config) # type: ignore # eval returns a string in this context
155189

156190
def get_client_secret(self) -> str:
157-
client_secret: str = self._client_secret.eval(self.config)
191+
client_secret = (
192+
self._client_secret.eval(self.config) if self._client_secret else self._client_secret
193+
)
158194
if not client_secret:
159195
raise ValueError("OAuthAuthenticator was unable to evaluate client_secret parameter")
160-
return client_secret
196+
return client_secret # type: ignore # value will be returned as a string, or an error will be raised
161197

162198
def get_refresh_token_name(self) -> str:
163199
return self._refresh_token_name.eval(self.config) # type: ignore # eval returns a string in this context
@@ -192,6 +228,27 @@ def get_token_expiry_date(self) -> pendulum.DateTime:
192228
def set_token_expiry_date(self, value: Union[str, int]) -> None:
193229
self._token_expiry_date = self._parse_token_expiration_date(value)
194230

231+
def get_assertion_name(self) -> str:
232+
return self.assertion_name
233+
234+
def get_assertion(self) -> str:
235+
if self.profile_assertion is None:
236+
raise ValueError("profile_assertion is not set")
237+
return self.profile_assertion.token
238+
239+
def build_refresh_request_body(self) -> Mapping[str, Any]:
240+
"""
241+
Returns the request body to set on the refresh request
242+
243+
Override to define additional parameters
244+
"""
245+
if self.use_profile_assertion:
246+
return {
247+
self.get_grant_type_name(): self.get_grant_type(),
248+
self.get_assertion_name(): self.get_assertion(),
249+
}
250+
return super().build_refresh_request_body()
251+
195252
@property
196253
def access_token(self) -> str:
197254
if self._access_token is None:

0 commit comments

Comments
 (0)