refactor: sandbox Printables behind an adapter that cannot raise - #103
Merged
Conversation
Printables has no public, documented, or versioned API — platecli talks to
their GraphQL endpoint against an observed schema. It is the most likely part
of the tool to break through no fault of ours, and it was not fenced off:
- four call sites imported the *private* _is_printables_model_url, so there
was no public surface to hold stable
- failures collapsed to a (None, None) sentinel, so a caller could not tell
"not a Printables URL" from "they changed the schema" from "network down"
from "this model has no STL" — every one of them printed the same
"Failed to resolve Printables model URL."
- get_download_link read result.get("data", {}).get("getDownloadLink", {}).
For the standard {"errors": [...], "data": null} envelope that returns
None, not {}, so it raised AttributeError. The broad except caught it and
showed the user: "'NoneType' object has no attribute 'get'". The sibling
function had a comment warning about this exact trap.
printables.py becomes a package with a hard inside/outside line:
errors.py typed failures: unavailable / contract_changed / model_unavailable,
each carrying a reason token and a remedy
client.py the only module that knows the wire format. Raises typed errors
instead of sentinels. SEALED: check_layers.py now fails CI if
anything outside the package imports it
adapter.py the containment boundary
__init__.py the entire public surface
The adapter's guarantee: no Printables failure escapes as an exception.
resolve() always returns a PrintablesResolution. A renamed field, a redesigned
error envelope, an HTML error page, a 4MB junk body — all become a typed result
with an actionable message, never a traceback inside plate job. KeyboardInterrupt
and SystemExit are the only things still allowed to propagate, so Ctrl-C works.
Also fixed while in here: the null-data AttributeError above, and an unbounded
resp.read() on the metadata path (now capped at 4MB; the file download path had
limits, this one had none).
Tests: the 19 tests that drove private functions and patched
bambu_cli.printables.build_safe_opener are replaced by tests/test_printables_adapter.py
(50 tests) driving the public surface with an injected opener. It keeps every
behavior the old class covered and adds the containment sweep: 16 malformed
payloads and 7 unexpected exception types, each asserted not to raise. That
sweep is the regression net for "Printables changed their API".
Gates: 1177 passed (was 1146), coverage 86.5% (was 86.2%), ruff/ruff-format/
mypy/bandit/layers/syntax/help/workflow/compat smokes green; uv build +
package_contents_smoke confirm the new subpackage ships.
…on 3.9 The containment sweep passed HTTPError instances directly in the parametrize list. pytest derives parameter ids from the values at collection time, and probing a 3.9 HTTPError built with fp=None raised KeyError: 'file' out of tempfile.__getattr__ — so the whole module failed to collect on the 3.9 leg while every other Python passed. Pass factories with explicit ids instead: nothing is constructed until the test body runs, and pytest never inspects the objects. Verified against a real CPython 3.9.25: full suite 1177 passed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Step 2 of 4. Replaces #100, which GitHub auto-closed when its base branch was deleted on merging #99 — same branch, same commits, rebased onto
main. Reviewed there.Why
Printables has no public, documented, or versioned API. platecli talks to their GraphQL endpoint against an observed schema, which makes it the most likely part of the tool to break through no fault of ours. It was not fenced off:
No public surface. Four call sites imported the private
_is_printables_model_url.Lossy failures. Everything collapsed to
(None, None), so a caller could not distinguish "not a Printables URL" from "they changed the schema" from "network down" from "this model has no STL". All four printed the sameFailed to resolve Printables model URL.A latent crash.
get_download_linkdidresult.get("data", {}).get("getDownloadLink", {}). For the standard{"errors": [...], "data": null}envelope that returnsNone, not{}— so it raisedAttributeError, and the broadexceptshowed the user:The sibling function had a comment warning about this exact trap; this call site never got the fix.
Structure
printables.pybecomes a package with a hard inside/outside line:errors.pyunavailable/contract_changed/model_unavailable, each carrying a reason token and a remedyclient.pycheck_layers.pynow fails CI if anything outside the package imports itadapter.py__init__.pyThe guarantee
resolve()always returns aPrintablesResolution. A renamed field, a redesigned error envelope, an HTML error page, a 4 MB junk body — all become a typed result with an actionable message, never a traceback insideplate job.KeyboardInterrupt/SystemExitare deliberately the only things that still propagate, so Ctrl-C keeps working.That distinction is now user-visible: a
contract_changedresult says "Printables appears to have changed their API. Download the file from the model page in a browser and pass the local path instead" — because retrying will not help and the fix is a code change here, not anywhere else in the tool.Also fixed
dataAttributeErrorabove.resp.read()on the metadata path was unbounded; now capped at 4 MB. The file download path had size limits, this one had none.Tests
The 19 tests that drove private functions and patched
bambu_cli.printables.build_safe_openerare replaced bytests/test_printables_adapter.py(50 tests) driving the public surface with an injected opener — no module-global patching. It keeps every behavior the old class covered and adds the containment sweep:KeyError,AttributeError,MemoryError,HTTPError…)each asserted not to raise. That sweep is the regression net for "Printables changed their API", and it is the part worth reviewing.
Gates
Draft, same as #99 — the stack lands after #97.