Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .fern/metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@
"originGitCommitIsDirty": false,
"invokedBy": "ci",
"ciProvider": "github",
"sdkVersion": "2.1.20"
"sdkVersion": "2.2.0"
}
2 changes: 2 additions & 0 deletions .fernignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ MIGRATE.md
# Custom Pipedream client files
src/pipedream/__init__.py
src/pipedream/pipedream.py
src/pipedream/private_key_jwt.py
tests/custom/test_connect_token_project_id.py
tests/custom/test_private_key_jwt.py

# Custom Proxy files
src/pipedream/proxy/client.py
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
run: |
curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1
- name: Install dependencies
run: poetry install
run: poetry install --extras private-key
- name: Compile
run: poetry run mypy .
test:
Expand All @@ -35,13 +35,13 @@ jobs:
run: |
curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1
- name: Install dependencies
run: poetry install
run: poetry install --extras private-key

- name: Test
run: poetry run pytest -rP -n auto .

- name: Install aiohttp extra
run: poetry install --extras aiohttp
run: poetry install --extras "aiohttp private-key"

- name: Test (aiohttp)
run: poetry run pytest -rP -n auto -m aiohttp .
Expand Down
21 changes: 20 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ Two layers of clients exist:
pipedream import Pipedream`).

The wrapper adds: env-var resolution (`PIPEDREAM_ACCESS_TOKEN`,
`PIPEDREAM_CLIENT_ID`, `PIPEDREAM_CLIENT_SECRET`, `PIPEDREAM_PROJECT_ID`,
`PIPEDREAM_CLIENT_ID`, `PIPEDREAM_CLIENT_SECRET`, `PIPEDREAM_PRIVATE_KEY`,
`PIPEDREAM_KEY_ID`, `PIPEDREAM_PROJECT_ID`,
`PIPEDREAM_PROJECT_ENVIRONMENT`, `PIPEDREAM_BASE_URL`,
`PIPEDREAM_WORKFLOW_DOMAIN`), the `access_token` shorthand (turns into a
`token=lambda: …` callable when calling `super().__init__`), `project_id`
Expand Down Expand Up @@ -87,6 +88,24 @@ generated base — `Client` constructs an `OAuthTokenProvider` and threads its
`get_token` into a `SyncClientWrapper`. The wrapper does **not** override this;
it just decides which branch to take.

**Private key (`private_key`, optional `key_id`)**: the client authenticates
with RFC 7523 client assertions instead of a secret. This path is custom
(`src/pipedream/private_key_jwt.py`, in `.fernignore`): `ClientAssertionSigner`
infers ES256/RS256 from the key, sets `aud` to the base URL's origin, and signs
a new single-use `jti` per token request. `PrivateKeyJwtTokenProvider` /
`AsyncPrivateKeyJwtTokenProvider` cache the token like the generated provider,
but send token requests with `max_retries: 0` and retry them themselves with a
**newly signed** assertion, because the API rejects a reused `jti`. They use
their own token-less client wrapper over the same `httpx` client. The sync
wrapper passes `token=provider.get_token`. The async wrapper sets
`_async_token`, mirroring the generated async OAuth wiring, so
`AsyncPipedream.raw_access_token` is `None` there, as it is for async client
secrets. Signing needs the optional `private-key` extra (`pyjwt[crypto]`),
imported lazily. Install it in CI, and declare it in Fern's `generators.yml`
`extras` so a regen doesn't drop it from `pyproject.toml`. `client_secret` and
`private_key` are mutually exclusive (`_resolve_credentials` in
`pipedream.py`).

`raw_access_token` reaches into `self._client_wrapper._get_token()` to expose
the current bearer token regardless of which branch was taken. Don't make
`_get_token` private at the wrapper level.
Expand Down
218 changes: 217 additions & 1 deletion poetry.lock

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ dynamic = ["version"]

[tool.poetry]
name = "pipedream"
version = "2.1.20"
version = "2.2.0"
description = ""
readme = "README.md"
authors = []
Expand Down Expand Up @@ -38,10 +38,12 @@ Repository = 'https://github.com/PipedreamHQ/pipedream-sdk-python'
[tool.poetry.dependencies]
python = "^3.10"
aiohttp = { version = ">=3.13.4,<4", optional = true, python = ">=3.9"}
cryptography = { version = ">=3.4.0", optional = true}
httpx = ">=0.21.2"
httpx-aiohttp = { version = "0.1.8", optional = true, python = ">=3.9"}
pydantic = ">= 1.9.2"
pydantic-core = ">=2.18.2,<3.0.0"
pyjwt = { version = ">=2.8.0,<3", optional = true}
typing_extensions = ">= 4.0.0"

[tool.poetry.group.dev.dependencies]
Expand Down Expand Up @@ -93,4 +95,5 @@ requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

[tool.poetry.extras]
private-key=["pyjwt", "cryptography"]
aiohttp=["aiohttp", "httpx-aiohttp"]
4 changes: 2 additions & 2 deletions src/pipedream/core/client_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ def get_headers(self) -> typing.Dict[str, str]:
import platform

headers: typing.Dict[str, str] = {
"User-Agent": "pipedream/2.1.20",
"User-Agent": "pipedream/2.2.0",
"X-Fern-Language": "Python",
"X-Fern-Runtime": f"python/{platform.python_version()}",
"X-Fern-Platform": f"{platform.system().lower()}/{platform.release()}",
"X-Fern-SDK-Name": "pipedream",
"X-Fern-SDK-Version": "2.1.20",
"X-Fern-SDK-Version": "2.2.0",
**(self.get_custom_headers() or {}),
}
if self._project_environment is not None:
Expand Down
46 changes: 38 additions & 8 deletions src/pipedream/oauth_tokens/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ def create(
self,
*,
client_id: str,
client_secret: str,
client_secret: typing.Optional[str] = OMIT,
client_assertion_type: typing.Optional[
typing.Literal["urn:ietf:params:oauth:client-assertion-type:jwt-bearer"]
] = OMIT,
client_assertion: typing.Optional[str] = OMIT,
scope: typing.Optional[str] = OMIT,
request_options: typing.Optional[RequestOptions] = None,
) -> CreateOAuthTokenResponse:
Expand All @@ -41,7 +45,14 @@ def create(
----------
client_id : str

client_secret : str
client_secret : typing.Optional[str]
The client secret, for clients that authenticate with a client secret.

client_assertion_type : typing.Optional[typing.Literal["urn:ietf:params:oauth:client-assertion-type:jwt-bearer"]]
Required with `client_assertion`.

client_assertion : typing.Optional[str]
For clients that authenticate with a public key: a JWT signed with the client's private key (ES256 or RS256). Claims: `iss` and `sub` set to the client ID, `aud` set to `https://api.pipedream.com`, `exp` at most 1 hour ahead, and a unique `jti`. Sign a new assertion for every request, including retries.

scope : typing.Optional[str]
Optional space-separated scopes for the access token. Defaults to `*`.
Expand All @@ -65,11 +76,15 @@ def create(
)
client.oauth_tokens.create(
client_id="client_id",
client_secret="client_secret",
)
"""
_response = self._raw_client.create(
client_id=client_id, client_secret=client_secret, scope=scope, request_options=request_options
client_id=client_id,
client_secret=client_secret,
client_assertion_type=client_assertion_type,
client_assertion=client_assertion,
scope=scope,
request_options=request_options,
)
return _response.data

Expand All @@ -93,7 +108,11 @@ async def create(
self,
*,
client_id: str,
client_secret: str,
client_secret: typing.Optional[str] = OMIT,
client_assertion_type: typing.Optional[
typing.Literal["urn:ietf:params:oauth:client-assertion-type:jwt-bearer"]
] = OMIT,
client_assertion: typing.Optional[str] = OMIT,
scope: typing.Optional[str] = OMIT,
request_options: typing.Optional[RequestOptions] = None,
) -> CreateOAuthTokenResponse:
Expand All @@ -104,7 +123,14 @@ async def create(
----------
client_id : str

client_secret : str
client_secret : typing.Optional[str]
The client secret, for clients that authenticate with a client secret.

client_assertion_type : typing.Optional[typing.Literal["urn:ietf:params:oauth:client-assertion-type:jwt-bearer"]]
Required with `client_assertion`.

client_assertion : typing.Optional[str]
For clients that authenticate with a public key: a JWT signed with the client's private key (ES256 or RS256). Claims: `iss` and `sub` set to the client ID, `aud` set to `https://api.pipedream.com`, `exp` at most 1 hour ahead, and a unique `jti`. Sign a new assertion for every request, including retries.

scope : typing.Optional[str]
Optional space-separated scopes for the access token. Defaults to `*`.
Expand Down Expand Up @@ -133,13 +159,17 @@ async def create(
async def main() -> None:
await client.oauth_tokens.create(
client_id="client_id",
client_secret="client_secret",
)


asyncio.run(main())
"""
_response = await self._raw_client.create(
client_id=client_id, client_secret=client_secret, scope=scope, request_options=request_options
client_id=client_id,
client_secret=client_secret,
client_assertion_type=client_assertion_type,
client_assertion=client_assertion,
scope=scope,
request_options=request_options,
)
return _response.data
34 changes: 30 additions & 4 deletions src/pipedream/oauth_tokens/raw_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ def create(
self,
*,
client_id: str,
client_secret: str,
client_secret: typing.Optional[str] = OMIT,
client_assertion_type: typing.Optional[
typing.Literal["urn:ietf:params:oauth:client-assertion-type:jwt-bearer"]
] = OMIT,
client_assertion: typing.Optional[str] = OMIT,
scope: typing.Optional[str] = OMIT,
request_options: typing.Optional[RequestOptions] = None,
) -> HttpResponse[CreateOAuthTokenResponse]:
Expand All @@ -36,7 +40,14 @@ def create(
----------
client_id : str

client_secret : str
client_secret : typing.Optional[str]
The client secret, for clients that authenticate with a client secret.

client_assertion_type : typing.Optional[typing.Literal["urn:ietf:params:oauth:client-assertion-type:jwt-bearer"]]
Required with `client_assertion`.

client_assertion : typing.Optional[str]
For clients that authenticate with a public key: a JWT signed with the client's private key (ES256 or RS256). Claims: `iss` and `sub` set to the client ID, `aud` set to `https://api.pipedream.com`, `exp` at most 1 hour ahead, and a unique `jti`. Sign a new assertion for every request, including retries.

scope : typing.Optional[str]
Optional space-separated scopes for the access token. Defaults to `*`.
Expand All @@ -55,6 +66,8 @@ def create(
json={
"client_id": client_id,
"client_secret": client_secret,
"client_assertion_type": client_assertion_type,
"client_assertion": client_assertion,
"scope": scope,
"grant_type": "client_credentials",
},
Expand Down Expand Up @@ -103,7 +116,11 @@ async def create(
self,
*,
client_id: str,
client_secret: str,
client_secret: typing.Optional[str] = OMIT,
client_assertion_type: typing.Optional[
typing.Literal["urn:ietf:params:oauth:client-assertion-type:jwt-bearer"]
] = OMIT,
client_assertion: typing.Optional[str] = OMIT,
scope: typing.Optional[str] = OMIT,
request_options: typing.Optional[RequestOptions] = None,
) -> AsyncHttpResponse[CreateOAuthTokenResponse]:
Expand All @@ -114,7 +131,14 @@ async def create(
----------
client_id : str

client_secret : str
client_secret : typing.Optional[str]
The client secret, for clients that authenticate with a client secret.

client_assertion_type : typing.Optional[typing.Literal["urn:ietf:params:oauth:client-assertion-type:jwt-bearer"]]
Required with `client_assertion`.

client_assertion : typing.Optional[str]
For clients that authenticate with a public key: a JWT signed with the client's private key (ES256 or RS256). Claims: `iss` and `sub` set to the client ID, `aud` set to `https://api.pipedream.com`, `exp` at most 1 hour ahead, and a unique `jti`. Sign a new assertion for every request, including retries.

scope : typing.Optional[str]
Optional space-separated scopes for the access token. Defaults to `*`.
Expand All @@ -133,6 +157,8 @@ async def create(
json={
"client_id": client_id,
"client_secret": client_secret,
"client_assertion_type": client_assertion_type,
"client_assertion": client_assertion,
"scope": scope,
"grant_type": "client_credentials",
},
Expand Down
Loading
Loading