Skip to content

@W-24132874@ Add Python port of b2c-tooling-sdk (SDK, samples, docs, agent-skills plugin) - #667

Open
priandsf wants to merge 42 commits into
SalesforceCommerceCloud:mainfrom
priandsf:python
Open

priandsf wants to merge 42 commits into
SalesforceCommerceCloud:mainfrom
priandsf:python

Conversation

@priandsf

@priandsf priandsf commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

@W-24132874@

What

Adds a Python port of the B2C tooling SDK under python/b2c-tooling-sdk/, plus supporting samples, documentation, and an agent-skills plugin for consuming it. This is additive — it lives entirely under python/ and does not touch the existing TypeScript packages.

Highlights

  • salesforce-b2c-tooling-sdk (import b2c_tooling_sdk) — a faithful port of @salesforce/b2c-tooling-sdk: async-first with a complete synchronous facade (b2c_tooling_sdk.sync), Python 3.10+.
    • Authentication (OAuth client-credentials, JWT Bearer, PKCE, implicit, Basic, API-key) and a persistent session store.
    • Config resolution (dw.json, ~/.mobify, settings.json, multi-env aliases).
    • Typed OCAPI / SCAPI / WebDAV clients (ClientResult, never raise on 4xx/5xx) and higher-level success-or-raise operations (code deploy, jobs, sites, catalogs, BM users/roles, sandboxes, metrics, logs).
    • SLAS shopper token helpers.
  • CLI interoperability — shares the same on-disk auth-sessions.json and config files as @salesforce/b2c-cli, so a session created by b2c auth login works from Python and vice versa.
  • Samples (python/samples/) — runnable scripts and Jupyter notebooks for each scenario; offline, mocked, CI-safe notebooks under the SDK's docs/notebooks/.
  • Documentation — an MkDocs site with guides and a generated API reference.
  • b2c-python-sdk agent-skills plugin — a consumer skill (using-b2c-tooling-sdk) with a full symbol catalog, wired into the marketplace and the b2c setup skills installer.

Notes for reviewers

  • Install instructions in the SDK docs/skill/samples point at SalesforceCommerceCloud/…@main and only resolve once this merges to main (chicken-and-egg by design; PyPI publishing is future work).
  • RELEASE.md / release.sh intentionally still describe the interim fork-based tag release process.
  • SDK quality gate (ruff check + ruff format + mypy strict + pytest, 1195 tests) is green.

Adds the standalone Python SDK (import: b2c_tooling_sdk; dist:
salesforce-b2c-tooling-sdk) that mirrors the TypeScript b2c-tooling-sdk public
surface and interoperates with the B2C CLI (shared auth-session store and config
files). Includes auth (all methods), config resolution, OCAPI/SCAPI typed
clients, WebDAV, operations, SLAS shopper tokens, a sync facade, docs, and
runnable example notebooks.

Also adds python/LICENSE (Apache-2.0), python/.gitignore, and a
tag-triggered GitHub Actions release workflow (python-v*) that builds the
wheel/sdist and attaches them to a GitHub Release for pip installation.
Document installation via git+https from the python branch (and pinned tags) as
a temporary arrangement during development, until the package is published to
PyPI. Remove the now-unneeded GitHub Release workflow — the git+https install
builds straight from the ref and needs no release assets.
Interactive script proposes the next minor version, runs the quality gate,
bumps pyproject.toml, commits, tags python-v<version>, and pushes to the fork
after confirmation. RELEASE.md now leads with the script; manual steps kept as
reference.
get_default_data_dir() now mirrors @oclif/core's Config.dataDir (and the
sibling get_b2c_config_directory): $B2C_DATA_DIR | $XDG_DATA_HOME |
(win32 %LOCALAPPDATA%) | ~/.local/share, then /b2c. Previously it returned
~/Library/Application Support/@salesforce/b2c-cli on macOS, so find_auth_session
could never locate sessions written by the b2c CLI. The identical bug still
exists in the TS SDK (session-store.ts getDefaultDataDir).
Five auth/API scenarios (oauth_ocapi, oauth_scapi, basic_webdav, cli_session,
slas_shopper) in async + sync, plus matching notebooks, all reading one
git-ignored dw.json (dw.example.json is the pseudo-value template).
…irectly

The sync facade proxies the whole object graph, so instance.webdav from
b2c_tooling_sdk.sync.resolve_config is already blocking. The sample was
await-ing webdav.put/get/delete, which raised TypeError on a None result.
Call the WebDAV methods directly (no asyncio) and correct the docstring +
README note that repeated the wrong 'wrap in asyncio.run' premise.
Adds browser_login_{async,sync}.py + notebook/06-browser-login.ipynb showing
the SDK equivalent of 'b2c auth login <clientId>': create_user_auth_strategy +
get_token_response runs the Authorization Code + PKCE browser flow and persists
the session to the shared auth-sessions store (reusable by the CLI).
Adds multi_env_{async,sync}.py + notebook/07-multi-env.ipynb + a bundled
multi-env.example.json demonstrating named-environment selection from a
multi-config dw.json via ResolveConfigOptions(instance=...). Offline; no
credentials or network required.
… deterministic

Read specs from the sibling JS package (packages/b2c-tooling-sdk/specs)
instead of vendoring a second copy under python/specs, and stop bundling
specs in the wheel/sdist. Generate models with datamodel-code-generator's
builtin formatter, then format each file with the project's pinned ruff --
datamodel's black/isort integration silently no-ops on recent toolchains,
which made regeneration non-deterministic. Pin datamodel-code-generator
so output stays stable. Regenerated models are AST-identical (pure
reformat).
…s, and content

Add live Jupyter notebooks demonstrating the admin Data APIs with OAuth
client-credentials, parsing responses into the SDK's generated Pydantic
types:
- 10-scapi-catalogs: list catalogs via create_catalogs_backend and a typed
  product/catalogs/v1 call (SCAPI Data)
- 11-ocapi-products: product search + product by id (OCAPI Data)
- 12-ocapi-content-assets: list a folder's content + content asset by id
  (OCAPI Data)

SCAPI Admin covers only catalog listing here, so products/content go
through the OCAPI Data API; notebooks handle the OCAPI Data allow-list 403
gracefully. Also add the previously-uncommitted SLAS Shopper notebooks
(08/09) and document all of them in the samples README.
Add python/CLAUDE.md documenting the Python SDK subproject (commands,
architecture, async/sync facade, generated models, conventions), and a
using-b2c-tooling-sdk skill guiding consumption of the SDK from scripts and
notebooks (install, auth by use case, available clients and operations).
…ing SDK

Move the using-b2c-tooling-sdk consumer skill out of the Python package's local .claude/skills into a first-class, installable skills plugin so it ships to consumers. Register it in plugins.json (release packaging), both marketplace manifests, and the CLI skill installer (sources.ts + SkillSet type) so it installs via the plugin marketplace or 'b2c setup skills b2c-python-sdk'. Add the missing references/api-catalog.md the skill points to.
Move the Python package (src, tests, docs, scripts, build/config) from python/ into python/b2c-tooling-sdk/, alongside the sibling python/samples/. Pure relocation.
The Python SDK is headed for SalesforceCommerceCloud/b2c-developer-tooling@main, so update the git-install URL, release badge, and 'python' branch prose in the SDK README, docs/index, and the b2c-python-sdk skill to reference the upstream repo and main branch. Also fix the skill's SDK-development pointer to python/b2c-tooling-sdk/CLAUDE.md after the reorg. RELEASE.md is left untouched — it documents the current fork-based release process. Note: the @main install command only works once the branch is merged upstream.
Remove unused imports, sort imports, and apply ruff format to the offline docs/notebooks so the SDK lint/format gate is green. Autofix only; no behavioral changes. Samples notebooks are left untouched.
…tory

The SDK moved into python/b2c-tooling-sdk/; update the samples requirements pin to the new subdirectory path.
Point the samples requirements at SalesforceCommerceCloud/...@main instead of the priandsf fork, matching the consumer install docs. Release machinery (RELEASE.md, release.sh) still targets the fork by design.
@priandsf
priandsf requested a review from clavery as a code owner September 9, 2026 21:17
priandsf and others added 2 commits September 10, 2026 09:55
The b2c-python-sdk skill set was added to SKILL_SOURCES but the ALL_SKILL_SETS test still expected 5 entries. Include it and bump the expected length to 6.
@clavery

clavery commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator

I did a review and there were some good findings but not critical. However we're declaring python 3.10 support here but that's failing the tests. 3.11+ works. I'd suggest just dropping 3.10 instead of resolving it.

But we should probably add a CI step to run pytest, ruff, etc. Basic coverage. We can even scope that to run only on PRs with python/* file changes (skipping otherwise). That might be good to add now so future PRs have a baseline regression suite for py

For reference these are some of the issues identified but all this looks like eventual followups or nit-picking rather than necessary here (except for the CI stuff):

  • ZIP extraction does not block ../ paths, allowing files to be written outside the destination.
  • Python 3.10 is declared as supported, but one deploy-timeout test fails there.
  • 401 retry state is stored permanently by URL, so later token expirations on the same endpoint won’t retry.
  • GitHub CI does not run any Python tests, linting, typing, or supported-version matrix.
  • Auth-session temp files are chmodded only after writing, briefly exposing refresh tokens under permissive umasks.
  • make build-sync calls a script that does not exist.

@clavery

clavery commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator

Also I highly recommend we get this onboarded into changesets here. all packages use it even if they aren't nodejs (for instance the agent-skills and docs site here are in changesets).

https://github.com/changesets/changesets/blob/main/docs/versioning-apps.md

3.10 was declared as supported but failed the deploy-timeout test suite;
3.11+ works reliably. Bumping the floor also raises ruff/mypy's
target-version, which auto-converts datetime.timezone.utc to the 3.11+
datetime.UTC alias across the codebase.
The Python SDK had no CI coverage at all - no tests, linting, type
checking, or supported-version matrix ran on PRs. Adds a workflow scoped
to python/b2c-tooling-sdk/** and python/samples/** changes that runs
ruff/mypy/pytest across Python 3.11-3.13, plus a cheap import-smoke job
for the (otherwise untested, credential-requiring) sample scripts.
Extracting a code-version or site-archive ZIP joined entry names onto
the output directory with plain os.path.join, so a malicious archive
entry named e.g. ../../../etc/evil could write outside the destination
(zip-slip). Adds resolve_zip_entry_path(), which resolves the joined
path and raises if it would escape the base directory, and uses it at
both extraction sites (code download, site-archive export).
_AuthMiddleware and _ScapiAuthMiddleware tracked already-retried requests
in a set/dict keyed by "method + URL", which persists for the middleware
instance's lifetime. Once any call to an endpoint retried once, every
later call to that same endpoint was permanently blocked from retrying
again - even on a genuine, later token expiry. Switches both to a
weakref.WeakSet/WeakKeyDictionary keyed by the httpx.Request object
itself, matching the TS SDK's WeakSet<Request> design: a fresh Request
is built per outer call, so tracking naturally resets per call while
still preventing a double-retry within one call.
…ally

The session store wrote the temp file with the process's default mode
via Path.write_text, then chmodded it to 0o600 afterwards - leaving a
window where the file (containing refresh tokens) could be world/group
readable under a permissive umask. Opens the temp file directly via
os.open(..., 0o600) instead, so the restrictive mode applies from the
first open() syscall, matching the TS backend's writeFileSync mode option.
make build-sync invoked scripts/build_sync.py, which was never written -
an early plan considered generating the sync layer via unasync, but a
runtime facade (sync/_runner.py, sync/_proxy.py) was built instead. The
target survived the pivot as dead, confusing tooling.
Calling b2c_tooling_sdk.sync from inside an already-running event loop
(every Jupyter cell, regardless of await usage) blocked that loop with
no explanation - warn instead so notebooks keep working but the
condition is visible. Mixing direct `await` use and `sync` use of the
same object raised a bare cross-loop RuntimeError far from the cause -
append an actionable hint. syncify() built a fresh SyncProxy per call
with no __eq__/__hash__, breaking identity/equality between the async
object and its synced wrapper - cache proxies by target identity and
add __eq__/__hash__ that delegate to the wrapped object.

Also fixes a PKCE code example in the b2c-python-sdk skill that
referenced a nonexistent AuthCredentials constructor instead of
PkceOAuthConfig.
Registers python/b2c-tooling-sdk as a private, unpublished workspace
package (@salesforce/b2c-tooling-sdk-python), following the same
"versioning app" pattern already used for docs/ and skills/: it exists
purely so Changesets can version and changelog the Python SDK.

A new sync script (scripts/sync-python-sdk-version.mjs), run as part of
`pnpm run version`, propagates the changeset-bumped version into
pyproject.toml and version.py's fallback - fixing a pre-existing drift
where the fallback was stuck at 0.3.0 while pyproject.toml was at 0.4.0.

release.sh no longer prompts for or bumps a version itself; it reads
whatever Changesets already committed to pyproject.toml and just tags +
pushes. Actual release automation (CI tag/publish) is intentionally not
wired up here, since Python releases ship from a separate `python`
branch/fork rather than `main` - tagging stays a manual release.sh step.

Updates RELEASE.md/CLAUDE.md/AGENTS.md to describe the new flow.
…llback server dual-stack

webdav.py was sending the pre-middleware request body to auth.fetch instead
of the (possibly rewritten) body on the httpx.Request middleware returned,
silently dropping any body changes middleware made. The OAuth redirect
callback server bound only 127.0.0.1 while the default redirect URI is
http://localhost:{port}; bind localhost (both loopback families) to match
Node's dual-stack listen() default and avoid connection failures where
localhost resolves to ::1 first. Also drops a redundant duplicate
_ZIP_HEADERS definition in download.py.
…er-tooling into python

# Conflicts:
#	.gitignore
#	AGENTS.md
#	packages/b2c-tooling-sdk/src/skills/sources.ts
#	packages/b2c-tooling-sdk/src/skills/types.ts
#	packages/b2c-tooling-sdk/test/skills/sources.test.ts
#	pnpm-lock.yaml
#	pnpm-workspace.yaml
@priandsf

Copy link
Copy Markdown
Collaborator Author

@clavery Thanks for the review. I should have addressed all the findings.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants