Skip to content

refactor(bfabric): decouple entities from client via BfabricSession - #566

Draft
leoschwarz wants to merge 13 commits into
mainfrom
feat/bfabric-session
Draft

refactor(bfabric): decouple entities from client via BfabricSession#566
leoschwarz wants to merge 13 commits into
mainfrom
feat/bfabric-session

Conversation

@leoschwarz

Copy link
Copy Markdown
Member

Entities embedded a live Bfabric client (the author flagged this as a "design mistake"), coupling immutable data to a connection, dragging credentials into every pickle/YAML round-trip, and forcing two cross-instance guards in EntityReader. This makes entities pure data and moves the connection into a new ambient, read-only BfabricSession.

Key changes

  • Entities are pure data: Entity(data_dict, bfabric_instance) — the client= parameter, Entity._client, and the deprecated FindMixin (find/find_all/find_by) are removed.
  • New BfabricSession (returned by client.reader) routes reads to the right connection by instance, so one process can read across multiple B-Fabric instances, and it owns the entity cache. Relationship navigation resolves the connection from the active session; the @use_client decorator opens one automatically.
  • Writes stay on the explicit Bfabric client — the session is read-only, so the acting authority is always visible.
  • The two EntityReader cross-instance guards are gone (reads are routed, not rejected); caching is now session-scoped.

Notes

Verified: bfabric / bfabric_scripts / bfabric_app_runner / bfabric_rest_proxy / bfabric_asgi_auth test suites, basedpyright, and ruff all pass.

🤖 Prepared with assistance from Claude Opus 4.8 via Claude Code.

Replace all 16 deprecated Entity.find/find_all/find_by (FindMixin) read-path
call sites in bfabric_app_runner with the modern client.reader API
(read_id/read_ids/query_one). Behavior-preserving:

- find(id) -> reader.read_id(endpoint, id, expected_type=...)
- find_by(obj) -> reader.query_one(endpoint, obj, expected_type=...)
  (callers only used the first/truthiness of the result)
- find_all(ids) -> reader.read_ids(endpoint, ids, expected_type=...)
  read_ids is URI-keyed and includes None for misses, whereas find_all was
  int-keyed and dropped misses. So:
    * .values() consumers now filter out None
    * id-indexed consumers re-key uri.components.entity_id -> entity and drop
      None, preserving the prior "missing id -> KeyError" behavior

The four possibly-None accesses that were previously grandfathered in the
basedpyright baseline (execution.dataset arg, dataset.to_polars(),
registration.workunit_id/storage_id) are now handled with targeted
# pyright: ignore comments; the baseline shrinks accordingly (no new
suppressions).

Tests updated to mock the client.reader path (real EntityUri keys for
read_ids); added focused coverage for the re-key/None-drop paths in the
dataset, resource, and resource-flow resolvers.
…pers

Deduplicate the 're-key EntityReader results by id / drop not-found' transform
that FindMixin.find_all/find_by performed inline into two reusable free functions
in bfabric.entities.core.reader_utils. FindMixin now delegates to entities_by_id,
and the app-runner FindMixin-migration sites use the helpers instead of inline
dict-comprehensions, collapsing the four bulk read_ids sites to one line each.

No behavior change. The basedpyright baseline shrinks by one entry (find_by's
grandfathered reportReturnType is now fixed via a localized cast).
read_id/read_ids/query/query_one now accept an entity class in place of the
endpoint string (e.g. client.reader.read_id(Resource, id)), inferring both the
endpoint and the result type via import_entity.entity_type_of (the class->string
inverse of import_entity, keyed on the lowercase-class-name convention). The
string form (with optional expected_type) still works.

Adopt the class form across bfabric_app_runner + a few core sites, dropping the
redundant endpoint string; narrow the read-only ResolveBfabricDatasetSpecs and
ResolveBfabricResourceDatasetSpecs to take an EntityReader instead of the full
Bfabric client.
Replace the free entities_by_id / present_entities helpers (and the
reader_utils module) with an EntityResult dict subclass returned by
read_ids / read_uris, exposing .present and .by_id views. Call sites now
read reader.read_ids(Resource, ids).present instead of wrapping the
result. query keeps its precise dict[EntityUri, Entity] return (never has
not-found entries, so no EntityResult). Also migrate the two remaining
order-fasta call sites to the class-based lookup form.
Entities become pure data (data_dict + required bfabric_instance); the
connection moves into a new ambient, read-only BfabricSession that routes
reads to the right instance and owns the entity cache. Removes the two
cross-instance guards in EntityReader, deletes FindMixin, and makes
client.reader return a BfabricSession. Writes stay on the explicit client.
…bfabric-session

Reconcile BfabricSession with #563's EntityResult refactor: BfabricSession
read_uris/read_ids now return EntityResult, and the Phase-F callers that
used the deleted reader_utils helpers switch to the .present/.by_id views
(query call sites use .values()/query_one, since query stays a plain dict).
Squash of #563 re-conflicts on the same files; my branch already contains
the final #563 content (verified byte-identical to its tip) plus the
BfabricSession work, so resolved by keeping ours.
- dataset/show.py: move the reportAny pyright-ignore onto the assignment
  line after black wrapped the call (basedpyright flagged the drifted comment).
- rest_proxy is_employee test: drop the removed client= arg from User(...).
client.reader is a cached_property, so a nested `with client.reader:` (e.g.
the app-runner resolver running inside the @use_client CLI decorator) re-enters
the same session object. The single _token/_parent fields were overwritten on
the second enter, so the outer __exit__ reset an already-used token
(RuntimeError) and leaked the session into the context var. Replace them with a
(parent, token) frame stack pushed/popped per with-block; _parent is now the
top frame's parent. Adds a same-object re-entry regression test.
@github-actions

Copy link
Copy Markdown
Contributor

📝 "TODO" Changes Detected

Summary: ✅ 1 "TODO" removed

✅ Removed "TODO"s (1)

  • bfabric/src/bfabric/entities/core/entity_reader.py:292: # TODO limitation of the current implementation

This comment is automatically updated when "TODO" changes are detected.

Re-entering an already-active session recorded no parent, so instance
delegation to an outer session was lost inside the nested with-block; keep
the existing parent instead (with a regression test). Also refresh
EntityReader docstrings that still promised the removed cross-instance
ValueError guards, and note the single-instance assumption in Users.
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.

1 participant