Skip to content
Closed
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
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Logic lives in focused packages; `bambu_cli/bambu.py` is a **thin entrypoint** (
| `argutils.py` | argparse/`Namespace` coercion helpers (`namespace_get`, `exit_code_from_system_exit`, `setup_args_provided`) |
| `commands/` | Printer subcommand handlers (`status`, `device`, `files`, `print_cmd`, `doctor`, `gcode`, thin `setup_wrappers`) |
| `download/` | URL/filename validation, HTML scraping, ZIP extraction, `download` command |
| `printables/` | Printables.com integration behind a strict adapter. `client.py` (the undocumented GraphQL wire format) is **sealed** — import only from `bambu_cli.printables`. `adapter.py` guarantees no Printables failure escapes as an exception |
| `job/` | One-shot `job`/`send` orchestration, dry-run predict, print payloads, injectable `JobSteps` |
| `setup_cmd/` | Guided/non-interactive setup, mDNS, config show/validate, preflight |
| `slicer/` | OrcaSlicer integration |
Expand Down Expand Up @@ -76,6 +77,8 @@ The rule exists because directories alone never held it: `protocols/`, `slicer/`

Accepted debt lives in `ALLOWED` in that script, each entry with a reason. Shrink it; do not grow it. One edge is currently allowlisted: `context -> printer` (`RuntimeContext` lazily constructs a `BambuPrinter`; the real fix is a composition root that installs a printer factory).

The same script also enforces `SEALED` — package internals no outside module may import. `bambu_cli.printables.client` is sealed because an adapter is only a sandbox if callers cannot reach past it. **Third-party integrations go behind an adapter that cannot raise:** `PrintablesAdapter.resolve()` returns a `PrintablesResolution` for every outcome, converting a renamed field or a redesigned error envelope into a typed `printables_contract_changed` result instead of a traceback in the middle of `plate job`. `KeyboardInterrupt`/`SystemExit` are deliberately the only things that still propagate.

**Package inventory is derived:** setuptools finds `bambu_cli*`; syntax smoke and CLI help smoke auto-discover modules/commands (`scripts/syntax_smoke.py`, `scripts/cli_help_smoke.py`). Adding a module under `bambu_cli/` or a subcommand in `cli.py` is enough — no triplicated lists.

**Typing (mypy):** CI runs `uvx mypy@<pinned> -p bambu_cli` over the **whole package** with `check_untyped_defs = true` (CI pins the tool version in `.github/workflows/ci.yml`; running it unpinned locally is fine). There is **no residual exclude blocklist** — `printer.py` and `slicer/` are included. New modules are type-checked automatically.
Expand Down
2 changes: 1 addition & 1 deletion bambu_cli/download/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,6 @@
user_agent_for_url,
)
from bambu_cli.printables import ( # noqa: F401
_is_printables_model_url,
is_printables_url,
resolve_printables_url,
)
4 changes: 2 additions & 2 deletions bambu_cli/download/downloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
from bambu_cli.paths import exception_for_message as _exception_for_message
from bambu_cli.paths import expand_path as _expand_path
from bambu_cli.paths import path_for_message as _path_for_message
from bambu_cli.printables import _is_printables_model_url, resolve_printables_url
from bambu_cli.printables import is_printables_url, resolve_printables_url
from bambu_cli.utils import _ensure_output_dir, _record_download_success, emit_json_error


Expand Down Expand Up @@ -88,7 +88,7 @@ def _cmd_download(
normalized_source_report = _redact_url_credentials(normalized_source)
max_download_bytes = _validate_max_download_mb_or_exit(args)
_validate_download_url_or_exit(args, source_url, normalized_source, url, "validate", "Invalid URL source")
is_printables_model = _is_printables_model_url(url)
is_printables_model = is_printables_url(url)
if not is_printables_model:
_reject_unsupported_download_extension(args, source_url, normalized_source, url, urlparse(url).path)

Expand Down
4 changes: 2 additions & 2 deletions bambu_cli/job/orchestrate.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
_extract_zip_model,
_file_extension,
_is_http_url,
_is_printables_model_url,
_known_unsupported_download_extension,
_looks_like_url,
_max_download_mb_error,
Expand Down Expand Up @@ -60,6 +59,7 @@
from bambu_cli.paths import exception_for_message as _exception_for_message
from bambu_cli.paths import expand_path as _expand_path
from bambu_cli.paths import path_for_message as _path_for_message
from bambu_cli.printables import is_printables_url
from bambu_cli.slicer import _directory_input_message, _is_directory_input, _validate_slice_options
from bambu_cli.utils import emit_json

Expand Down Expand Up @@ -165,7 +165,7 @@ def _run_job(ctx, args, steps=None):

try:
if getattr(args, "dry_run", False) and _is_http_url(source):
if not _is_printables_model_url(source):
if not is_printables_url(source):
unsupported_ext = _known_unsupported_download_extension(urlparse(source).path)
if unsupported_ext:
summary["extension"] = unsupported_ext
Expand Down
4 changes: 2 additions & 2 deletions bambu_cli/job/predict.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
_download_source_extension,
_download_target_filename,
_file_extension,
_is_printables_model_url,
_portable_basename,
_sanitize_download_filename,
)
from bambu_cli.printables import is_printables_url
from bambu_cli.slicer import _sliced_output_path


Expand Down Expand Up @@ -122,7 +122,7 @@ def _predicted_url_remote_name(url, args):
not resolve Printables pages, HTML pages, redirects, or ZIP members because
doing so would require network I/O or archive extraction.
"""
if _is_printables_model_url(url):
if is_printables_url(url):
return None
predicted_ext = _predicted_url_download_extension(url, args)
if predicted_ext in ARCHIVE_DOWNLOAD_EXTENSIONS:
Expand Down
187 changes: 0 additions & 187 deletions bambu_cli/printables.py

This file was deleted.

68 changes: 68 additions & 0 deletions bambu_cli/printables/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""Printables.com integration, behind a strict adapter.

Printables has no public, documented, versioned API — platecli talks to their
GraphQL endpoint against an observed schema. That makes this the most likely
part of the tool to break through no fault of its own, so it is fenced off:

* ``client.py`` is the only module that knows the wire format. Nothing outside
this package may import it.
* ``adapter.py`` guarantees that no Printables failure escapes as an exception.
A schema change becomes a typed, reportable outcome — never a traceback in
the middle of ``plate job``.
* This module is the entire public surface. Import from here.

The names below are what the rest of the codebase may use:

``is_printables_url(url)``
Cheap URL test. Never raises, never touches the network.

``resolve_printables_url(url)``
``(download_url, filename)``, or ``(None, None)`` on any failure. The
long-standing shape, kept so callers that only branch on "did it resolve"
need no changes.

``PrintablesAdapter`` / ``PrintablesResolution``
The richer surface: *why* a resolution failed, and what to tell the user.
"""

from __future__ import annotations

from bambu_cli.printables.adapter import PrintablesAdapter, PrintablesResolution
from bambu_cli.printables.errors import (
PrintablesContractChanged,
PrintablesError,
PrintablesModelUnavailable,
PrintablesUnavailable,
)

__all__ = [
"PrintablesAdapter",
"PrintablesContractChanged",
"PrintablesError",
"PrintablesModelUnavailable",
"PrintablesResolution",
"PrintablesUnavailable",
"is_printables_url",
"resolve_printables",
"resolve_printables_url",
]


def is_printables_url(url):
"""True if *url* is a printables.com model page. Never raises."""
return PrintablesAdapter.handles(url)


def resolve_printables(url, adapter=None):
"""Resolve *url*, returning a :class:`PrintablesResolution` with failure detail."""
return (adapter or PrintablesAdapter()).resolve(url)


def resolve_printables_url(url, adapter=None):
"""Resolve *url* to ``(download_url, filename)``, or ``(None, None)``.

Kept as the default entry point because every existing caller branches on
``if not resolved_url``. Use :func:`resolve_printables` when you want to
report *why* it failed.
"""
return resolve_printables(url, adapter=adapter).as_tuple()
Loading