Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f68924a
refactor(client): issue requests through a generated transport
chandrasekharan-zipstack Aug 11, 2026
c1ab0af
test(client): pin behaviour against the last released client
chandrasekharan-zipstack Aug 11, 2026
04d997d
fix(client): map a connect timeout to ConnectTimeout, not Timeout
chandrasekharan-zipstack Aug 11, 2026
d0130d0
fix(client): raise ReadTimeout, not a bare Timeout, on a read timeout
chandrasekharan-zipstack Aug 12, 2026
e84a935
chore(sdk): generate from the backend's own committed spec
chandrasekharan-zipstack Aug 12, 2026
c291e36
feat(client): accept the deployment's request parameters on structure…
chandrasekharan-zipstack Aug 12, 2026
ed89066
feat(client): accept the status endpoint's query parameters
chandrasekharan-zipstack Aug 12, 2026
d704489
chore(sdk): regenerate from the remediated spec
chandrasekharan-zipstack Aug 12, 2026
68e83c0
chore(tools): fail generation when the generator warns
chandrasekharan-zipstack Aug 12, 2026
20b686b
fix(client): spell query booleans the way the released client did
chandrasekharan-zipstack Aug 12, 2026
981e659
fix(client): post to the deployment URL the caller gave
chandrasekharan-zipstack Aug 12, 2026
29d4ac3
fix(client): translate the transport failures that were escaping
chandrasekharan-zipstack Aug 12, 2026
e210cdf
test: compare the headers that actually go on the wire
chandrasekharan-zipstack Aug 12, 2026
f8f13f3
ci: fail when the committed SDK is not what the spec generates
chandrasekharan-zipstack Aug 12, 2026
49acebd
feat(client): allow a socket timeout to be configured
chandrasekharan-zipstack Aug 12, 2026
a24afd2
test: pin how the status endpoint is read
chandrasekharan-zipstack Aug 12, 2026
f1bdd97
test: pin the parity baseline by digest
chandrasekharan-zipstack Aug 12, 2026
22ed443
build: bound httpx to the series the transport is generated against
chandrasekharan-zipstack Aug 12, 2026
0882b45
fix(client): treat an explicit None as unset on the optional execute …
chandrasekharan-zipstack Aug 12, 2026
4d3c4a9
docs: trim comments that narrate rather than explain
chandrasekharan-zipstack Aug 12, 2026
19a5644
fix: poll status under the deployment URL's own path prefix
chandrasekharan-zipstack Aug 12, 2026
54f09f4
feat!: drop the `unstract` console script
chandrasekharan-zipstack Aug 12, 2026
27dd806
fix: follow redirects, refuse a blank execution id, translate InvalidURL
chandrasekharan-zipstack Aug 12, 2026
114aef8
fix: poll the endpoint the service returned, query and all
chandrasekharan-zipstack Aug 12, 2026
a77ef6a
docs: record the deliberate differences, and pin the parity row exactly
chandrasekharan-zipstack Aug 13, 2026
5ae45fd
fix: give the generator its own venv on PATH
chandrasekharan-zipstack Aug 13, 2026
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
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
src/unstract/api_deployments/sdk_docstudio/** linguist-generated=true
28 changes: 28 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,31 @@ jobs:

- name: Tests (pytest)
run: uv run pytest tests/ -v

sdk-drift:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install uv
uses: astral-sh/setup-uv@v6
with:
version: "0.6.14"
enable-cache: true

# The generated tree is committed, so an edit to it reviews like any
# other change and then disappears on the next regeneration. Same for a
# spec change that never had the generator run over it.
- name: Regenerate from the committed spec
run: ./tools/gen_sdk.sh

# `git add -N` first: a diff alone cannot see a file the generator has
# newly created, which is exactly what a spec growing an endpoint does.
- name: Fail if the committed SDK is not what the spec generates
run: |
git add -N -- src/unstract/api_deployments/sdk_docstudio
git diff --exit-code -- src/unstract/api_deployments/sdk_docstudio
34 changes: 27 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,16 +100,16 @@ client = APIDeploymentsClient(
The retry logic uses exponential backoff with full jitter and respects the `Retry-After` header on 429 responses.


## Unstract CLI
## Cloning an organization

Installing `unstract-client` also provides the `unstract` command:
Installing `unstract-client` also provides a clone command:

```bash
pip install unstract-client
unstract --help
python -m unstract.clone --help
```

### `unstract clone`
### `python -m unstract.clone`

Clones an organization's resources to another org, on the same or a different
deployment (e.g. promote **dev** → **QA** → **prod**). Covers adapters,
Expand All @@ -124,7 +124,7 @@ so keys never land in shell history:
export UNSTRACT_SRC_PLATFORM_KEY="<source platform key>"
export UNSTRACT_TGT_PLATFORM_KEY="<target platform key>"

unstract clone \
python -m unstract.clone \
--source-url https://dev.example.com --source-org org_dev123 \
--target-url https://qa.example.com --target-org org_qa456 \
--dry-run
Expand Down Expand Up @@ -152,14 +152,34 @@ failed run can be resumed by re-running the same command.

#### Compatibility

`unstract clone` is capability-probed: each phase checks for its endpoint on the
Cloning is capability-probed: each phase checks for its endpoint on the
source and target, and clones only what both orgs support. A capability missing on
either side is reported and skipped — the run never fails because of a version
difference. Cloning a newer source into an older target therefore drops the entity
types the target lacks (listed in the end-of-run report).

- Run the source and target on the same (or a newer-target) Unstract build.
- Use `unstract-client >= 1.4.0`, the first release that ships `unstract clone`.
- Use `unstract-client >= 1.4.0`, the first release that ships the clone command.

## Behaviour that differs from earlier releases

Deliberate, and listed here so a difference is not rediscovered as a bug:

- **The status poll is resolved under the deployment URL's own path prefix.**
Earlier releases concatenated the base URL and the endpoint the service
returned, which never reached a deployment served under an ingress or reverse
proxy path. Where no prefix can be derived from the deployment URL, the
endpoint the service returned is used as it came.
- **An absolute `status_check_api_endpoint` now resolves.** Concatenation
produced `https://hosthttps://host/...`, which reached nothing at all; it is
joined instead.
- **Query parameters this client sets win a collision** with the ones on the
returned endpoint. Everything else on that endpoint is forwarded unchanged.
- **A malformed `api_url` raises this client's own exception classes** —
`APIDeploymentsClientException` for a URL with no host, `MissingSchema` for one
with no scheme — where earlier releases raised `InvalidSchema` for both. Code
catching `requests.exceptions.RequestException` around client construction no
longer catches the first of those.

## Questions and Feedback

Expand Down
41 changes: 41 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Release notes — draft

Content for the next release. Not published yet.

## Breaking

**The `unstract` console script is gone.** Installing this package no longer
puts an `unstract` command on your PATH.

- The clone command is still here: run it as `python -m unstract.clone`, with
the same options it has always taken.
- The name now belongs to the `unstract-cli` package, whose `unstract clone`
wraps this same code.

An environment that holds both an older release of this package and the new CLI
gives the name to whichever was installed last, so check what answers before
reporting a missing command:

```bash
command -v unstract && unstract --version
```

## Behaviour that differs from earlier releases

Deliberate; each is described in the README under *Behaviour that differs from
earlier releases*:

- The status poll resolves under the deployment URL's own path prefix, and falls
back to the endpoint the service returned where no prefix can be derived.
- An absolute `status_check_api_endpoint` resolves instead of being concatenated
into an unreachable URL.
- Query parameters this client sets win a collision with the returned
endpoint's; everything else on that endpoint is forwarded.
- A malformed `api_url` raises this client's own exception classes rather than
`InvalidSchema`.

## Under the hood

The HTTP layer is generated from the service's OpenAPI spec and runs on `httpx`.
Transport failures are still raised as their `requests` equivalents, so code
catching `ConnectionError`, `Timeout` and the rest keeps working.
12 changes: 9 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ authors = [
{name = "Zipstack Inc", email = "devsupport@zipstack.com"},
]
dependencies = [
# The transport layer is generated against httpx; attrs backs its models.
# Upper-bounded because the generated code is written against one minor
# series: a bump has to be regenerated and re-tested, not resolved into.
"httpx>=0.27,<0.29",
"attrs>=23.2",
# Kept for its exception classes, which callers catch by name.
"requests>=2.32.3",
"tenacity>=8.2.0",
"click>=8.1",
Expand All @@ -27,9 +33,6 @@ classifiers = [
"Topic :: Software Development :: Libraries :: Python Modules",
]

[project.scripts]
unstract = "unstract.cli:main"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
Expand Down Expand Up @@ -64,6 +67,9 @@ lint = [

[tool.ruff]
line-length = 88
# Generated and vendored code is overwritten wholesale by its refresh script, so
# a lint finding there can never be fixed in place.
extend-exclude = ["src/unstract/api_deployments/sdk_docstudio", "tests/baseline"]

[tool.ruff.lint]
select = ["E", "F", "W", "I"]
Expand Down
Loading
Loading