Skip to content

Typed exceptions and logging hygiene (umbrella #46) - #52

Merged
tipatterson-dev merged 4 commits into
mainfrom
fix/error-handling-and-logging
Jul 28, 2026
Merged

Typed exceptions and logging hygiene (umbrella #46)#52
tipatterson-dev merged 4 commits into
mainfrom
fix/error-handling-and-logging

Conversation

@tipatterson-dev

Copy link
Copy Markdown
Collaborator

Closes #43, #44, #45, #47, #48, #49 — the whole of umbrella #46.

Two commits, matching the umbrella's two halves: the error contract, then logging hygiene.

1. Typed exceptions, and raising instead of failing silently

New oshconnect/exceptions.py:

OSHConnectError(Exception)
├── ConfigurationError                 # caller wiring mistakes, pre-HTTP
└── ResourceRequestError               # .status_code .response_text .resource_type .resource_label
    ├── ResourceInsertError            # create POST rejected
    │   └── MissingLocationHeaderError # 2xx, but no Location to read the id from
    └── ResourceDiscoveryError         # listing GET failed

OSHConnectError subclasses Exception, so existing except Exception: handlers are unaffected. All fields are optional and callers should check for None — not every call site has all of them.

#47 — the five bare raise Exception sites are converted. Rather than five near-identical blocks, the shared contract (POST → check ok → read Location) now lives in one helper, new_resource_id_from_response() in resources/base.py, so all five report failures identically. Note this changes the message wording from "Failed to insert system" to "Failed to create system" for uniformity across resource types.

#44 — that helper uses .get('Location'), so a 2xx without the header raises MissingLocationHeaderError explaining the resource was probably created and the caller should re-discover rather than re-POST (avoiding a duplicate), instead of a bare KeyError: 'Location' pointing at a dict lookup.

#43add_system_to_node, create_and_insert_system, and _insert_system shared an if target_node in self._nodes: guard with no else, silently returning None. All three now route through _require_registered_node() and raise ConfigurationError.

#49Node.discover_systems() raises ResourceDiscoveryError instead of returning None, so a 401 is no longer indistinguishable from an empty server (the for s in discover_systems() or []: idiom turned auth failures into zero-iteration loops). An empty server still returns []. System.discover_datastreams / discover_controlstreams were calling res.json() with no res.ok check at all — a failed listing raised a JSON decode error; both now raise the typed error too.

Bug found along the way

repr(Node) raised AttributeError. Node is a @dataclass whose generated __repr__ reads every declared field, but _basic_auth, _mqtt_client, and _nats_client were only assigned when credentials / transports were enabled. So any anonymous or transport-less node — most of them — blew up when repr'd. It was already visible in pytest failure output (<[AttributeError(...) raised in repr()] Node object>) and surfaced here when a new error message tried to interpolate the node. All three fields are now bound up front, with a regression test.

2. Logging hygiene

#48 — 24 log calls across 6 modules went to the root logger via logging.warning(...). Consequences: an app couldn't raise OSHConnect to DEBUG or silence it without reconfiguring the root logger, and the first warning auto-installed a stderr handler on the consumer's behalf. All 24 now use a module-level logger = logging.getLogger(__name__), and the package logger carries a NullHandler.

tests/test_logging.py pins this with a source-scanning guard, so a new logging.warning(...) anywhere under src/oshconnect fails CI rather than silently reintroducing the leak.

Also corrected the SchemaFetchWarning docstring, which named Node.discover_systems as a source of schema-fetch failures — that method never fetches a schema.

#45System.insert_self()'s server-facing path had no CI coverage: the only test driving it lives in the @pytest.mark.network sync module, which CI skips. tests/test_node_sync_mocked.py imports the real _ensure_dest_system helper (markers there are per-test, so it's importable) and drives it against mocked HTTP, covering reuse, create, and failure-propagation.

Verification

492 tests pass (up from 484), flake8 src/ clean, strict Sphinx build succeeds. The new exceptions module is wired into docs/source/api.rst, and README gains a Logging section documenting the oshconnect namespace.

Version 0.5.3a10.5.5a1. 0.5.4a0 is deliberately skipped — it's claimed by #51, which is still open.

Compatibility

  • except Exception: keeps working everywhere.
  • Node.discover_systems() raises where it returned None; callers relying on the falsy return were treating failures as empty results, which is the bug.
  • The insert failure message wording changed (insertcreate); anything matching on that exact string needs updating. Matching on the type is now the better option.

Closes #47, #43, #44, #49. Also fixes Node.__repr__ raising AttributeError
on nodes without credentials or transports.
Closes #48, #45. Library no longer writes to the consumer's root logger;
adds mocked CI coverage for the cross-node sync insert path.
…d-logging

# Conflicts:
#	pyproject.toml
#	uv.lock
@tipatterson-dev
tipatterson-dev merged commit c4aad54 into main Jul 28, 2026
16 of 18 checks passed
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.

OSHConnect.add_system_to_node / create_and_insert_system silently no-op on an unregistered node

1 participant