This is a Fern-generated Python SDK with substantial team customizations. Most
files in src/pipedream/ are auto-regenerated by the Fern CLI from the
Pipedream API spec; a small set of files in .fernignore are hand-maintained
and must not be replaced by regen output. Generated and custom code coexist
in the same trees.
.fernignore is the only thing standing between custom code and a regen wiping
it out. Treat it as a load-bearing file.
- Never comment out entries before running a regen "to see what happens."
That has happened, and it deleted the custom
Pipedreamwrapper, the entireworkflowsmodule, the proxy customizations, the license, and the comprehensive.gitignore. Recovery took a full restoration pass — seegit logaround0d5faa8("Bump to 2.0.0"), with the original disabling commit ataaa8f29and the destructive regen at1eb2b4e. - The source-of-truth SHA for restoring custom files is the commit before
.fernignorewas last disabled. Once it has been re-armed, that's HEAD. - Before adding new custom files, add their paths to
.fernignorefirst. - When the Fern API spec adds a resource that needs custom client behavior (like
proxy or workflows), the custom files go on
.fernignoreimmediately.
Two layers of clients exist:
src/pipedream/client.py— generator-produced base classesClientandAsyncClient. These hold the actual sub-client wiring and the auth bootstrap, and are regenerated on every Fern run. Not exported publicly.src/pipedream/pipedream.py— thePipedreamandAsyncPipedreamclasses (in.fernignore). They subclassClient/AsyncClientand add the public-facing convenience layer. This is what consumers import (from pipedream import Pipedream).
The wrapper adds: env-var resolution (PIPEDREAM_ACCESS_TOKEN,
PIPEDREAM_CLIENT_ID, PIPEDREAM_CLIENT_SECRET, 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
validation (ValueError("Project ID is required")), the raw_access_token
property, and the self.workflows = WorkflowsClient(...) attachment.
The wrapper also forwards every new Fern-introduced constructor param
(headers, max_retries, follow_redirects, httpx_client, logging)
through to super().__init__. When a regen adds another such param, mirror it
here too — otherwise consumers can't reach it without instantiating Client
directly.
src/pipedream/__init__.py is in .fernignore but is otherwise a normal
Fern-generated module index (the lazy _dynamic_imports dict, __all__ list,
and if typing.TYPE_CHECKING: block). After every regen we accept the new
content as the base and re-apply three additions for the workflows module:
from .workflows.client import AsyncWorkflowsClient, WorkflowsClientplusworkflowsin thefrom . import (…)block, both inside theTYPE_CHECKINGguard."AsyncWorkflowsClient": ".workflows.client","WorkflowsClient": ".workflows.client", and"workflows": ".workflows"in_dynamic_imports."AsyncWorkflowsClient","WorkflowsClient", and"workflows"in__all__.
Do not restore old ConfigurableProp_* or Emitter_* discriminator aliases
— those types no longer exist in the regenerated types/ and importing them
raises.
The wrapper accepts an access_token: Optional[str] for the common "I already
have a bearer token" case and converts it to the generator's token: Callable[[], str] shape:
if access_token:
super().__init__(token=(lambda: access_token), **common_kwargs)
else:
super().__init__(client_id=client_id, client_secret=client_secret,
**common_kwargs)The OAuth flow (client_id + client_secret) is owned entirely by the
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.
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.
Workflows (src/pipedream/workflows/) — entirely custom. Calls a different
domain (m.pipedream.net by default, overridable via workflow_domain), not
api.pipedream.com. Surfaces invoke() and invoke_for_external_user(). The
Fern API spec doesn't model this resource; if a regen ever adds a generated
workflows/ directory, delete it and keep the custom one. Public surface:
HTTPAuthType (NONE/STATIC_BEARER/OAUTH), WorkflowsClient /
AsyncWorkflowsClient with the two invoke* methods, all returning
httpx.Response. There is no with_raw_response accessor here — the
response is already raw.
Proxy (src/pipedream/proxy/) — caller-friendly request shape, not the
generator's (url_64, …, request) shape. Callers pass (url, *, external_user_id, account_id, headers=None, params=None[, body]); the client
base64-encodes the URL internally, prefixes all caller headers with
x-pd-proxy- via RequestOptions(additional_headers=…), and switches between
parsed ProxyResponse and binary Iterator[bytes] based on the response's
Content-Type. Empty bodies return None. The generator periodically tries to
revert the public surface to (url_64, …) returning Iterator[bytes] only —
check reference.md Proxy section after every regen.
The proxy raw client returns HttpResponse[Optional[Union[ProxyResponse, Iterator[bytes]]]] and is wrapped by the high-level client through a shared
_consume_sync / _consume_async helper that handles the dual return shape
(eager-read for JSON, generator with deferred context-manager exit for streams).
src/pipedream/client.py— baseClient/AsyncClient. Auth, sub-client wiring,_make_default_async_client(withhttpx_aiohttpautodetect). Regenerates fully on each Fern run.src/pipedream/core/*.py—client_wrapper.py,http_client.py,pagination.py,parse_error.py,logging.py,pydantic_utilities.py, etc. These are the runtime substrate; custom code uses them but should not modify them.src/pipedream/version.py— reads__version__from package metadata viaimportlib.metadata. There is no hardcoded version in this file.src/pipedream/types/,src/pipedream/errors/, and every resource client except the ones listed in.fernignore.tests/utils/*andtests/test_aiohttp_autodetect.py— generator produces and updates these. They must keep passing after any change toclient.py,client_wrapper.py, orhttp_client.py.
__version__ is auto-read from package metadata, so the source of truth is
pyproject.toml. Three places hold the version string:
pyproject.toml—[tool.poetry] version = "X.Y.Z".src/pipedream/core/client_wrapper.py—User-Agent: pipedream/X.Y.Zheader (line ~37).src/pipedream/core/client_wrapper.py—X-Fern-SDK-Version: "X.Y.Z"header (line ~42).
After bumping, run poetry install --all-extras to refresh the installed
package metadata; otherwise pipedream.__version__ will lag. Use semver: new
generator types/runtime defaults that change observable behavior are breaking
and require a major bump (see 0d5faa8 for the v2.0.0 example).
When a Fern regen runs (fern-bot PR or local fern generate):
- Confirm
.fernignorehas every custom file listed and is not commented out. Compare against0d5faa8's.fernignoreif unsure. - After regen, run
poetry install --all-extrasthenpoetry run python -c "from pipedream import Pipedream, AsyncPipedream; from pipedream.workflows.client import HTTPAuthType; print('ok')". If it fails, errors usually fall into three buckets:- Removed types referenced by
src/pipedream/__init__.py's_dynamic_imports(e.g., aConfigurableProp*variant the spec dropped) — drop the dead entry. - Sub-client option shape changes in
client_wrapper.py(e.g., the generator adds a new constructor param). Mirror it onPipedream/AsyncPipedreamso consumers can reach it. - Custom code referencing a renamed/removed generator symbol — see what the
regen produced and adjust the bridge. The current proxy and workflows
custom code uses
encode_path_param,parse_obj_as,ValidationError → ParsingErrormapping, and theOMITsentinel; if the regen changes those names, follow.
- Removed types referenced by
- Run
poetry run pytest -rP -n auto .. Expect 69 passed, 1 skipped (the skip is the aiohttp autodetect guard). - Spot-check the public API:
Pipedream(project_id="x", access_token="y")constructs and exposes.workflows,.proxy,.proxy.with_raw_response, and.raw_access_token;Pipedream()with no env vars raisesValueError("Project ID is required"). - If the generator left the SDK version unchanged but you made customization changes, bump it manually per the checklist above.
- Diff
.github/workflows/ci.yml'spublishjob. The Fern generator emits apoetry publish --username/--passwordstep againstsecrets.PYPI_USERNAME/PYPI_PASSWORD; this project actually publishes via PyPI Trusted Publishing (OIDC) —permissions: id-token: writepluspypa/gh-action-pypi-publish@release/v1. NoPYPI_*secrets exist on the repo or org, so if a regen reverts the auth shape (which already happened once on v2.0.0 — see CI run 25577372729) the next tag push silently fails with403 Invalid or non-existent authentication information..fernignoreguards this file, but a temporary.fernignoredisable + regen will re-open the trapdoor.
- The workflows client passes both
data=bodyandjson=bodytohttpx_client.request(...). This was inherited from the pre-2.0 code and is preserved for backcompat. Fern's wrapper currently accepts both kwargs, but the dispatch behavior is sensitive to body type — if a workflow expects strict JSON and starts receiving form-encoded payloads (or vice versa), this is the place to look. ConfigurablePropis a plaintyping.Union, not a Pydantic discriminated annotation. Payloads with a missing or wrongtypemay silently parse into a best-fit variant instead of raising. Don't rely on validation alone to catch malformed configurable-prop payloads.max_retries=2is the default. For mutating proxy / workflow calls, passmax_retries=0(per-client or per-call viarequest_options) if duplicate side effects on transient 5xx are unacceptable.- The version string lives in three places (see version bump checklist).
pipedream.__version__reads from installed package metadata, so a stalepoetry installwill mask a fresh bump.
- Install:
poetry install --all-extras - Tests:
poetry run pytest -rP -n auto .(≈2s; expect 69 passed, 1 skipped). - Smoke imports: see step 2 of the regen playbook.
- Diff sanity after a regen:
git diff main -- .fernignoreshould match the un-commented form;git statusshould not showsrc/pipedream/workflows/as deleted.
- Build / package manager: Poetry 2.x (
pyproject.toml,poetry.lock). CI usespoetry install --no-rootthenpoetry install. - Test runner: pytest 9.x with
pytest-xdist(-n auto),pytest-asyncio(mode=auto). Config inpyproject.toml's[tool.pytest.ini_options]. - Pydantic: v2.x; the SDK supports the v1 compat shim via
core/pydantic_utilities.py. Don't import directly frompydantic.v1. - HTTP: httpx (sync + async), with optional
httpx-aiohttpfor the aiohttp transport. Detection inclient.py's_make_default_async_client. - Python floor: 3.10 (per
pyproject.tomlpython = "^3.10").
.github/workflows/ci.yml runs the test matrix (default httpx + an aiohttp
variant), with cancel-in-progress: false concurrency, and the publish job runs
on tag pushes using PYPI_USERNAME / PYPI_PASSWORD secrets. The Python
version is pinned to 3.10 in CI even though the floor in pyproject.toml allows
higher; bump deliberately if you raise the floor.
.github/workflows/auto-close-empty-prs.yml is custom; the regen does not
produce it. Don't let a regen drop it.