Skip to content

feat(ble-extra): make Bluetooth support an optional extra (tesla-fleet-api[ble]) - #135

Draft
Bre77 wants to merge 5 commits into
mainfrom
fm/pytfa-optional-ble-extra
Draft

feat(ble-extra): make Bluetooth support an optional extra (tesla-fleet-api[ble])#135
Bre77 wants to merge 5 commits into
mainfrom
fm/pytfa-optional-ble-extra

Conversation

@Bre77

@Bre77 Bre77 commented Aug 25, 2026

Copy link
Copy Markdown
Member

Intent

Make the Bluetooth stack an optional extra in python-tesla-fleet-api so cloud-only consumers (Fleet API, Teslemetry, Tessie) stop carrying it.

Problem: pyproject.toml declared bleak, bleak-retry-connector, protobuf, cryptography and tesla-protocol as mandatory, with no optional-dependencies section at all, so every consumer carried the local Bluetooth and protobuf stack whether or not it touched Bluetooth.

Requirements:

  • Work out which modules actually import bleak/bleak-retry-connector (not just which dependency names sound Bluetooth-related), and make sure importing the cloud surface does not transitively pull them in. A base install that still imports bleak achieves nothing.
  • cryptography, protobuf, and tesla-protocol must NOT be assumed Bluetooth-only just because they appear alongside it - they are needed by the cloud/signed-command path too (vehicle/commands.py, vehicle/signed.py, funnel.py all use them), so they stay base dependencies. Only bleak and bleak-retry-connector are genuinely Bluetooth-only.
  • Name the extra per this repo's own conventions; there were no prior extras, so used "ble" (tesla-fleet-api[ble]) per explicit product direction.
  • Acceptance criterion that matters most: a test/check proves the base install does not import the Bluetooth stack, not just that the dependency is declared optional - so a future import added in the wrong module doesn't silently undo this.
  • Backwards compatible: someone already depending on this library and using Bluetooth must be told how to keep working (install the ble extra), not silently broken.
  • Do not restructure modules to make the split cleaner unless an import genuinely cannot be separated otherwise, and justify any such restructuring explicitly.
  • Do not touch the Router design, the signed-command layer, or any consumer of this library.
  • README/docs must tell consumers how to install the extra.

What I did:

  • pyproject.toml: moved bleak/bleak-retry-connector into [project.optional-dependencies] ble; left cryptography/protobuf/tesla-protocol as base deps. Regenerated uv.lock.
  • Found only three modules import bleak: tesla/bluetooth.py, tesla/vehicle/bluetooth.py, and tesla/vehicle/vehicles.py (the last one is the base Vehicles class shared by every cloud client, so it was the real leak).
  • vehicles.py: removed its top-level bleak import; Vehicles.Bluetooth and VehiclesBluetooth.Bluetooth became properties that import tesla.vehicle.bluetooth lazily (with a friendly ImportError naming the ble extra if bleak is missing), since no code overrides that class attribute today. This is the one deliberate restructuring beyond pure import-guarding: the class attribute couldn't stay a plain top-level default without pulling in bleak eagerly.
  • Moved DEFAULT_KEEPALIVE_INTERVAL (a plain float constant, not bleak-derived) from tesla/vehicle/bluetooth.py to const.py so vehicles.py's createBluetooth() can keep it as a real default parameter value without importing the bluetooth module.
  • tesla_fleet_api/init.py, tesla_fleet_api/tesla/init.py, and tesla_fleet_api/tesla/vehicle/init.py each eagerly imported TeslaBluetooth/VehicleBluetooth as package-init side effects (these package init.py files run whenever any submodule is imported, e.g. via from tesla_fleet_api.tesla.fleet import TeslaFleetApi), so switched those three re-exports to TYPE_CHECKING + a module-level getattr that imports bleak-backed classes on first attribute access only.
  • Did not touch router/*.py, funnel.py's BleBroadcastPublisher (already TYPE_CHECKING-only for VehicleBluetooth), or any consumer-facing command signatures.
  • Added tests/test_ble_optional_extra.py: runs subprocesses with sys.modules["bleak"]/["bleak_retry_connector"] set to None (which makes any import bleak raise ModuleNotFoundError exactly like an actual missing package) and asserts the cloud surface (TeslaFleetApi, Teslemetry, Tessie, Router/VehicleRouter/EnergySiteRouter, ObservationFunnel, Vehicles.createFleet/createSigned) still imports and works, and that createBluetooth()/TeslaBluetooth raise a clear ImportError pointing at the ble extra when bleak is absent.
  • Updated README.md and docs/bluetooth_vehicles.md with pip install tesla-fleet-api[ble] instructions, and AGENTS.md with a durable-knowledge note about the optional-dependency boundary and lazy-import pattern.
  • Verified: ruff check/format clean, pyright strict 0 errors, full pytest suite (712 tests, including the new ones) passes, uv sync --locked --extra ble matches the regenerated lockfile.

Constraint: do not merge the resulting PR - open it and report only.

What Changed

  • pyproject.toml: moved bleak/bleak-retry-connector out of base dependencies into a new [project.optional-dependencies] ble group (pip install tesla-fleet-api[ble]); cryptography, protobuf, and tesla-protocol stay base deps since the cloud/signed-command path also needs them. uv.lock regenerated to match.
  • Removed the bleak leak in the base install: tesla_fleet_api/__init__.py, tesla_fleet_api/tesla/__init__.py, and tesla_fleet_api/tesla/vehicle/__init__.py now expose TeslaBluetooth/VehicleBluetooth via TYPE_CHECKING + a module-level __getattr__ that lazily imports the bleak-backed module on first access (via a new import_ble_class helper in util.py that raises a friendly ImportError pointing at the ble extra when bleak is missing), instead of importing them eagerly at package-init time.
  • tesla_fleet_api/tesla/vehicle/vehicles.py: dropped its top-level bleak/VehicleBluetooth import; Vehicles.Bluetooth and VehiclesBluetooth.Bluetooth became properties that resolve VehicleBluetooth on demand instead of class-attribute defaults.
  • Moved DEFAULT_KEEPALIVE_INTERVAL from tesla/vehicle/bluetooth.py to const.py so createBluetooth() can keep it as a real default parameter value without importing the bluetooth module.
  • Added tests/test_ble_optional_extra.py, which runs subprocess checks with bleak/bleak_retry_connector poisoned in sys.modules to prove the cloud surface (TeslaFleetApi, Teslemetry, Tessie, Router/VehicleRouter/EnergySiteRouter, ObservationFunnel, Vehicles.createFleet/createSigned) still imports and works without bleak, and that createBluetooth()/TeslaBluetooth raise a clear ImportError naming the ble extra when it's absent.
  • Updated README.md and docs/bluetooth_vehicles.md with pip install tesla-fleet-api[ble] install instructions, and AGENTS.md with the optional-dependency boundary and lazy-import pattern.

Risk Assessment

✅ Low: Both prior-round findings (star-import breakage without bleak, and inconsistent ImportError messaging on the top-level lazy accessors) are now fixed correctly and consistently via a single shared import_ble_class helper in util.py, with no new issues introduced by the fix commits; the optional-extra split matches the stated intent (bleak/bleak-retry-connector only, base deps unchanged, ble extra, lockfile/docs updated) and the new tests exercise real subprocess import/instantiation behavior rather than source-text matching.

Testing

All targeted automated tests pass and were independently corroborated by manual end-to-end reproduction in a genuinely bleak-free venv (not just the test suite's sys.modules poisoning), confirming the base install drops bleak, the cloud surface works fully without it, backward-compat ImportErrors correctly name the ble extra on every lazy accessor (including star-imports), and the ble-extra path is unaffected when bleak is installed; no issues found.

Evidence: New optional-extra pytest run (7 tests)
tests/test_ble_optional_extra.py::TestBluetoothIsOptional::test_cloud_surface_imports_and_works_without_bleak PASSED
tests/test_ble_optional_extra.py::TestBluetoothIsOptional::test_creating_bluetooth_vehicle_without_bleak_raises_clear_error PASSED
tests/test_ble_optional_extra.py::TestBluetoothIsOptional::test_tesla_bluetooth_attribute_without_bleak_raises_clear_error PASSED
tests/test_ble_optional_extra.py::TestBluetoothIsOptional::test_tesla_vehicle_bluetooth_attribute_without_bleak_raises_clear_error PASSED
tests/test_ble_optional_extra.py::TestBluetoothIsOptional::test_teslemetry_and_tessie_subpackages_import_without_bleak PASSED
tests/test_ble_optional_extra.py::TestBluetoothIsOptional::test_top_level_package_imports_without_bleak PASSED
tests/test_ble_optional_extra.py::TestBluetoothIsOptional::test_vehicle_bluetooth_attribute_without_bleak_raises_clear_error PASSED
7 passed in 1.34s
Evidence: Manual base-install reproduction (fresh venv, no ble extra, bleak genuinely absent)
$ pip list | grep -i bleak -> (no output, not installed)
CLOUD SURFACE OK: imports and basic vehicle creation work with no bleak installed

--- Named import of TeslaBluetooth without ble extra ---
Got ImportError: Bluetooth support requires the 'ble' extra: install with `pip install tesla-fleet-api[ble]`.

--- createBluetooth() without ble extra ---
Got ImportError: Bluetooth support requires the 'ble' extra: install with `pip install tesla-fleet-api[ble]`.

--- star import (from tesla_fleet_api import *) without ble extra ---
star import from tesla_fleet_api OK, TeslaFleetApi = <class 'tesla_fleet_api.tesla.fleet.TeslaFleetApi'>

--- star import from tesla_fleet_api.tesla without ble extra ---
star import from tesla_fleet_api.tesla OK, TeslaFleetApi = <class 'tesla_fleet_api.tesla.fleet.TeslaFleetApi'>
Evidence: ble extra installed: TeslaBluetooth/createBluetooth still work as before
ble extra installed: TeslaBluetooth = <class 'tesla_fleet_api.tesla.bluetooth.TeslaBluetooth'>
createBluetooth with ble extra installed OK: VehicleBluetooth

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 2 issues found → auto-fixed (2) ✅
  • ⚠️ tesla_fleet_api/__init__.py:40 - __all__ in tesla_fleet_api/__init__.py, tesla_fleet_api/tesla/__init__.py, and tesla_fleet_api/tesla/vehicle/__init__.py still lists TeslaBluetooth/VehicleBluetooth, but those names are now only real module attributes at runtime via the module-level __getattr__ (they're TYPE_CHECKING-only otherwise). CPython's from module import * resolves every name in __all__ via getattr(module, name), which invokes __getattr__ and propagates any exception it raises — it does not skip on failure. Verified by running (in this repo's own uv env, with bleak/bleak_retry_connector poisoned to simulate an uninstalled ble extra): from tesla_fleet_api import * and from tesla_fleet_api.tesla import * both raise ModuleNotFoundError: ... bleak and abort the whole import, even though the caller never touches Bluetooth. Before this change, these same wildcard imports worked in a base install because bleak was a mandatory dependency — so this is a regression for that access pattern, and it directly undercuts the PR's core goal ('a base install that still imports bleak achieves nothing') for any cloud-only consumer using import *. The added test suite only exercises explicit named imports (from tesla_fleet_api import (TeslaFleetApi, ...)), so it doesn't catch this.
  • ℹ️ tesla_fleet_api/tesla/vehicle/vehicles.py:117 - Vehicles.Bluetooth/VehiclesBluetooth.Bluetooth changed from a plain class attribute (Bluetooth: type[VehicleBluetooth[...]] = VehicleBluetooth) to an instance @property. Accessed through an instance (as the two internal call sites self.Bluetooth(...) do) this works fine, but accessed at the class level — e.g. Vehicles.Bluetooth for an isinstance check or a pre-instantiation reference, a pattern still supported by the sibling Fleet/Signed attributes which remain plain class attrs — now returns the property descriptor object instead of the VehicleBluetooth type, which would fail loudly (TypeError) rather than with the intended friendly ImportError. No code in this repo does this today, per the PR's own stated check, so this is a narrow residual risk from the one deliberate restructuring the author already called out and justified, not a required fix.

🔧 Fix: fix(ble-extra): stop all from breaking wildcard import without bleak
1 warning still open:

  • ⚠️ tesla_fleet_api/__init__.py:63 - The user intent's required backward-compatibility constraint states 'someone already depending on this library and using Bluetooth must be told how to keep working (install the ble extra), not silently broken,' and the author's own 'What I did' claims 'createBluetooth()/TeslaBluetooth raise a clear ImportError pointing at the ble extra when bleak is absent.' That's true for Vehicles.Bluetooth/createBluetooth() (vehicles.py's _import_vehicle_bluetooth() explicitly catches ImportError and re-raises with 'Bluetooth support requires the ble extra: install with pip install tesla-fleet-api[ble]'), but it is NOT true for the top-level lazy re-exports. tesla_fleet_api/__init__.py:63 (and the equivalent __getattr__ in tesla_fleet_api/tesla/__init__.py:53 and tesla_fleet_api/tesla/vehicle/__init__.py:29) just does from tesla_fleet_api.tesla.bluetooth import TeslaBluetooth with no try/except, so a caller doing from tesla_fleet_api import TeslaBluetooth (or tesla_fleet_api.TeslaBluetooth) without the ble extra gets Python's raw ModuleNotFoundError: import of bleak halted; None in sys.modules (verified by running the poisoned-bleak repro directly: uv run python -c &#34;import sys; sys.modules[&#39;bleak&#39;]=None; sys.modules[&#39;bleak_retry_connector&#39;]=None; import tesla_fleet_api; tesla_fleet_api.TeslaBluetooth&#34;), never mentioning the ble extra or install instructions. The new regression test tests/test_ble_optional_extra.py::test_tesla_bluetooth_attribute_without_bleak_raises_clear_error (line 101) asserts &#34;ble&#34; in str(err) or &#34;bleak&#34; in str(err) — since "ble" is literally a substring of the package name "bleak", this passes for any bleak-related error regardless of clarity, so it doesn't actually catch this gap. Recommend wrapping the three __getattr__ bodies with the same try/except ImportError -> friendly-message pattern already used in vehicles.py's _import_vehicle_bluetooth(), and tightening the test assertion to require the actual extra-install text.

🔧 Fix: fix(ble-extra): unify friendly ImportError across lazy bluetooth accessors
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • uv sync --extra ble
  • uv run pytest tests/test_ble_optional_extra.py -v
  • Manual: fresh venv via uv pip install -e . (no ble extra) — verified bleak absent from pip list
  • Manual: cloud surface import + Vehicles.createFleet/createSigned with bleak absent
  • Manual: tesla_fleet_api.TeslaBluetooth and Vehicles.createBluetooth() raise friendly ble-extra ImportError with bleak absent
  • Manual: from tesla_fleet_api import * and from tesla_fleet_api.tesla import * succeed with bleak absent
  • uv sync --locked --extra ble (lockfile consistency)
  • Manual: TeslaBluetooth import and Vehicles.createBluetooth() succeed normally with the ble extra installed
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Bre77 added 4 commits August 25, 2026 22:09
bleak and bleak-retry-connector move to an optional-dependencies "ble"
extra so cloud-only consumers (Fleet API, Teslemetry, Tessie, and the
read-side ObservationFunnel/router) no longer carry the local Bluetooth
stack. cryptography, protobuf, and tesla-protocol stay base dependencies
since the cloud signed-command path needs them too, not just Bluetooth.

Vehicles.Bluetooth/VehiclesBluetooth.Bluetooth and the TeslaBluetooth/
VehicleBluetooth re-exports now resolve tesla.vehicle.bluetooth lazily
instead of importing it at module load, so importing the cloud surface
never requires bleak to be installed; missing-extra access raises a
clear ImportError pointing at the extra. DEFAULT_KEEPALIVE_INTERVAL
moves to const.py so it can stay a real default value on the bleak-free
paths.

tests/test_ble_optional_extra.py proves this by poisoning bleak/
bleak_retry_connector in sys.modules in a subprocess and asserting the
cloud surface still imports and works.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@Bre77 Bre77 added the fm Opened by a Firstmate crewmate label Aug 25, 2026
@Bre77
Bre77 marked this pull request as draft August 26, 2026 05:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fm Opened by a Firstmate crewmate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant