Skip to content

Extract create/update/delete helpers to api/mutation - #97

Open
ramonski wants to merge 2 commits into
refactor/typed-exceptionsfrom
refactor/api-mutation
Open

Extract create/update/delete helpers to api/mutation#97
ramonski wants to merge 2 commits into
refactor/typed-exceptionsfrom
refactor/api-mutation

Conversation

@ramonski

Copy link
Copy Markdown
Contributor

Description of the issue/feature this PR addresses

Continues the incremental split of senaite.jsonapi.api (previous: #93 users, #94 settings, #95 serialization, #96 typed exceptions). This PR extracts the biggest remaining slice — the create/update/delete surface — so api/__init__.py shrinks to the utility helpers only and the mutation code lives in one focused place. Prerequisite for wrapping these into MCP tools cleanly.

What this PR does

Moves out of api/__init__.py into a new api/mutation.py:

Route orchestrators (called by v1/routes/content.action via getattr(api, "<action>_items")):

  • create_items, update_items, patch_items, put_items, delete_items

Low-level building blocks:

  • create_object — dispatches to ICreate adapter / AnalysisRequest special-case / standard bika_api.create + update_object_with_data
  • create_analysisrequest — thin wrapper around bika.lims.utils.analysisrequest.create_analysisrequest
  • update_object_with_dataIUpdate adapter or IDataManager set-loop + validation + optional transition
  • deactivate_object — SENAITE's soft-delete via deactivate workflow transition
  • find_target_container — resolves parent_uid / parent_path from the record
  • validate_object (still @deprecated — kept for compat)

Permission checks:

  • is_creation_allowed — portal/setup/senaite_setup denylist + adapter opinion
  • is_update_allowed — same denylist against the object's parent + adapter opinion

All 13 names remain importable from senaite.jsonapi.api via explicit re-exports at the bottom of __init__.py. Route code (v1/routes/content.action) that does getattr(api, "create_items") etc. keeps working unchanged.

Semantic tightening while moving

Every fail(401, ...) inside the moved code becomes a proper ForbiddenError (HTTP 403). HTTP 401 means "log in and try again"; the fail sites (denied by permission gate, denied by adapter, container disallows type, ...) all mean "you are authenticated but may not do this" — which is 403.

The two affected doctests (create.rst, update.rst) update their expected status from 401 to 403 to match. fail(400, ...) becomes BadRequestError, fail(404, ...) becomes NotFoundError. Backward compat is preserved through the shared APIError base.

Drop unused imports from init.py

Nine imports were only used by the extracted functions:

  • copy, transaction
  • AccessControl.Unauthorized
  • bika.lims.utils.analysisrequest.create_analysisrequest as create_ar
  • senaite.jsonapi.interfaces.ICreate, IUpdate, IInfo
  • zope.component.getAdapters
  • zope.deprecation.deprecate

Base branch

Stacked on top of #96 (refactor/typed-exceptions). Merge order: #92#93#94#95#96#97.

Verify

bin/test-senaite -s senaite.jsonapi
# Ran 20 tests with 0 failures, 0 errors and 0 skipped

--
I confirm I have tested the PR thoroughly and coded it according to PEP8 standards.

Move the biggest remaining slice of api/__init__.py into a focused
module: the three top-level route orchestrators (create_items,
update_items, delete_items) plus their patch/put aliases, the
low-level building blocks (create_object, update_object_with_data,
deactivate_object, create_analysisrequest, find_target_container,
validate_object) and the two permission checks (is_creation_allowed,
is_update_allowed).

All names remain importable from senaite.jsonapi.api via explicit
re-exports at the bottom of __init__.py. v1/routes/content.action
looks up 'api.create_items' / 'api.update_items' / 'api.delete_items'
via getattr and continues to work unchanged.

Semantic tightening while moving:

- Every 'fail(401, ...)' inside the moved code becomes a proper
  ForbiddenError. HTTP 401 means 'log in and try again'; the fail
  sites (denied by permission gate, denied by adapter, container
  disallows type, ...) all mean 'you are authenticated but may not
  do this' -- which is 403. The two affected doctests
  (create.rst, update.rst) update their expected status from
  401 to 403 to match.

- 'fail(400, ...)' becomes BadRequestError, 'fail(404, ...)' becomes
  NotFoundError. Backward compat is preserved through the shared
  APIError base.

Drop now-unused imports from __init__.py: copy, transaction,
AccessControl.Unauthorized, create_ar, ICreate, IUpdate, IInfo,
getAdapters, zope.deprecation.deprecate.
@ramonski
ramonski requested a review from xispa July 25, 2026 20:01
@ramonski ramonski added the Cleanup 🧹 Code cleanup and refactoring label Jul 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Cleanup 🧹 Code cleanup and refactoring

Development

Successfully merging this pull request may close these issues.

1 participant