Skip to content

Extract User CRUD out of the crud/__init__.py monolith - #2458

Merged
akwasigroch merged 1 commit into
mainfrom
refactor/extract-user-crud
Aug 12, 2026
Merged

Extract User CRUD out of the crud/__init__.py monolith#2458
akwasigroch merged 1 commit into
mainfrom
refactor/extract-user-crud

Conversation

@akwasigroch

@akwasigroch akwasigroch commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Purpose

crud/__init__.py is a 1946-line monolith that the codebase is splitting one entity at a time, following #2411, #2412, #2438, #2439, #2441, #2442, #2449, #2450 and #2451. This takes the User block out. Per apps/backend/AGENTS.md the package only shrinks from here — nothing new goes back into __init__.py.

What Changed

  • Moved get_user, get_users, create_user, update_user, delete_user, get_user_by_email and get_user_by_id into a new crud/user.py (198 lines). The block sat exactly between the Topic and Organization banners; Organization is untouched.
  • crud/__init__.py drops 154 lines.
  • Nothing is re-exported, matching the earlier splits. All call sites across 25 files now use from rhesis.backend.app.crud import user as user_crud. The alias matters here — user is an extremely common local variable and parameter name in this codebase, so the call sites were read individually rather than find-and-replaced.
  • Nothing dropped. get_user and get_user_by_id overlap but are not interchangeable: get_user is the tenant-filtered lookup used by the user router, Celery tasks and task notifications, while get_user_by_id is the unfiltered auth lookup that runs before a tenant context exists (session/JWT/token resolution, polyphemus).
  • Four function-local imports lifted to module level in the new file — func, flag_modified, ProjectMembership, bypass_tenant_filter — plus on_user_org_assigned out of create_user's body. auth/org_membership_hook.py is deliberately dependency-free (from __future__ import annotations and a TYPE_CHECKING guard, no runtime imports beyond stdlib), so there is no cycle; verified by importing the module.
  • Dropped the now-unused from rhesis.backend.app import crud in 9 files. The rest still use crud for other entities.
  • Updated 6 prose references to crud.create_user (in org_membership_hook.py, local_init.py, ee/rbac/default_role.py and two test files) to crud.user.create_user, since they would otherwise name a function that no longer lives there.

Additional Context

  • One of four parallel extractions: Behavior (Extract Behavior CRUD out of the crud/__init__.py monolith #2455), File (Extract File CRUD out of the crud/__init__.py monolith #2456), Model (Extract Model CRUD out of the crud/__init__.py monolith #2457) and User. The four blocks were chosen so they do not touch each other in crud/__init__.py, and git merge-tree confirms __init__.py merges clean across all six pairings.
  • This is the largest of the four at 38 files, well past the 200-400 line guidance in the PR skill. It is a pure move and cannot be split sensibly: the seven functions are one entity, and the file count is driven entirely by call sites, which reach into apps/polyphemus/ and ee/backend/ as well as the backend app. Flagging rather than hiding it.
  • This branch conflicts with the Model PR in services/explorer/embeddings.py and utils/user_model_utils.py. Both are the same trivial shape — each branch inserts its own crud submodule import at the same spot, so the resolution is to keep both lines. Once both land, the last bare crud. use disappears from each file and the from rhesis.backend.app import crud import must be dropped (ruff F401).
  • Suggested landing order across the four: Behavior and File first (conflict-free), then Model, then this one last.
  • Deliberately avoids get_test_sets and get_tests, which Add metric tuning for custom metrics [feature branch] #2446 modifies.

Testing

Pure move — behaviour is unchanged, so the existing suite is the check. User CRUD sits under the auth path, so this was run broadly rather than on the user tests alone.

cd apps/backend
uv run pytest ../../tests/backend/auth ../../tests/backend/utils ../../tests/backend/crud ../../tests/backend/ee
# 1274 passed

uv run pytest ../../tests/backend/routes ../../tests/backend/services ../../tests/backend/tasks ../../tests/backend/security ../../tests/backend/app ../../tests/backend/models
# 5019 passed, 43 skipped, 1 xfailed

uv run pytest ../../tests/backend/db ../../tests/backend/schemas ../../tests/backend/metrics ../../tests/backend/alembic
# 384 passed, 4 skipped

6677 passed, 47 skipped, 1 xfailed, 0 failures — effectively the whole backend suite. Skips are pre-existing (deferred security tests, missing PERSPECTIVE_API_KEY).

Ruff was measured against a HEAD baseline of the same file set: no new check errors and no new format drift. The 12 remaining findings and 3 format-check failures are identical at HEAD and were left alone. crud/user.py itself is clean.

A repo-wide grep for the seven old crud.* names across apps/, tests/, sdk/, ee/, packages/, penelope/ and docs/ returns nothing, over all file types rather than just .py. Confirmed at runtime too: hasattr(crud, 'get_user') and hasattr(crud, 'get_user_by_id') are both False, and every touched module plus rhesis.backend.tasks, rhesis.backend.ee.sso.user_utils and both polyphemus services import cleanly.

Two things worth a reviewer's eye:

tests/backend/tasks/test_explore.py and tests/backend/tasks/architect/test_progress.py patch rhesis.backend.tasks.endpoint.explore.crud as a whole-module mock. Since explore.py no longer has a crud attribute, those 8 patches would have raised AttributeError; they now target explore.user_crud.

The on_user_org_assigned import lift is the one change that is not purely mechanical — if that local import was defensive on purpose rather than incidental, say so and it goes back.

@peqy peqy Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. Clean extraction of User CRUD into crud/user.py with call sites/tests updated consistently, and the lifted on_user_org_assigned import stays dependency-free so cycle risk looks minimal. Ship it.

Move the seven user functions out of crud/__init__.py into crud/user.py,
following the split already done for source, task, tag and others.
Consumers import the module directly; nothing is re-exported.

All seven have callers, so nothing was dropped. get_user and
get_user_by_id do overlap, but both are live: get_user is the
tenant-filtered lookup used by the user router and Celery tasks, while
get_user_by_id is the unfiltered auth lookup that runs before a tenant
context exists.

The function-local imports in get_user_by_email and delete_user
(func, flag_modified, ProjectMembership, bypass_tenant_filter) move to
module level -- none of those modules import crud back. create_user's
import of on_user_org_assigned is lifted too; auth/org_membership_hook
is deliberately dependency-free so it can be imported from core.

Patch targets in the tests follow the functions to their new module, and
test_explore.py / test_progress.py now patch explore.user_crud instead
of explore.crud, which would otherwise have failed loudly on a missing
attribute.
@akwasigroch
akwasigroch force-pushed the refactor/extract-user-crud branch from 6e2ff1b to 4d92a09 Compare August 12, 2026 15:51
@akwasigroch
akwasigroch merged commit 378119d into main Aug 12, 2026
16 of 17 checks passed
@akwasigroch
akwasigroch deleted the refactor/extract-user-crud branch August 12, 2026 15:53

@peqy peqy Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extract looks solid and consistent with the ongoing crud split.

[Improvement] refresh_tokens() passes str(token_row.user_id) into user_crud.get_user(), which doesn’t coerce IDs before doing User.id == .... Safer to pass the UUID through (or use get_user_by_id).

[Improvement] crud/user.py::delete_user() commits internally while other CRUD helpers rely on the session context manager for commit/rollback. If this MR is meant to be a pure move, consider keeping commit ownership with callers (or making the pattern consistent).

Found 2 issues (0 critical, 2 improvements).

raise

user = crud.get_user(db, str(token_row.user_id))
user = user_crud.get_user(db, str(token_row.user_id))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Improvement] Avoid passing a string UUID into get_user here

user_crud.get_user ultimately does User.id == <passed value> without coercing the ID, so str(token_row.user_id) relies on driver/DB implicit casting. Since token_row.user_id is already a UUID, it’d be safer to pass it through directly (or just call user_crud.get_user_by_id(db, token_row.user_id)).

ValueError: If user tries to delete themselves
"""
# Security check: Prevent users from deleting themselves
if str(target_user_id) == str(user_id):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Improvement] Transaction management: avoid committing inside CRUD

delete_user() calls db.commit()/db.refresh(), while the other CRUD functions rely on the session context manager to commit/rollback. Since this file is described as a “pure move”, consider keeping the original contract and let callers own the commit (or at least be consistent across CRUD modules). Committing inside a CRUD helper can be surprising in larger transactions (e.g., when caller wants atomic multi-step changes).

akwasigroch added a commit that referenced this pull request Aug 14, 2026
get_user moved to crud/user.py in #2458, and reaching it through the crud
package raises AttributeError unless something else imported the submodule
first. Starting a tuning run for a metric with no model of its own hit that
every time.
akwasigroch added a commit that referenced this pull request Aug 19, 2026
get_user moved to crud/user.py in #2458, and reaching it through the crud
package raises AttributeError unless something else imported the submodule
first. Starting a tuning run for a metric with no model of its own hit that
every time.
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