Repository navigation
refactor(user-management): move byte-identical base_service to autobot_shared (#12647) - #12972
Merged
Merged
Conversation
…t_shared (#12647) First step of #12645's user_management de-fork. The package is forked across both backends with 25 shared-but-divergent files, so the risk is reconciling semantic drift -- except base_service.py, which is byte-identical in both and imports nothing backend-specific (uuid, dataclasses, sqlalchemy, and autobot_shared.logging_manager). That makes it the one move with no drift to reconcile. Note the issue lists base_service as 6 diff lines; it is now 0, so this step is smaller and safer than when #12647 was written. Creates autobot_shared/user_management/ as the consolidation target, and leaves a re-export shim in each backend so all 12 existing importers keep working -- the fork goes, not the callers. Same pattern as auth_rbac re-exporting is_admin_role (#12948). Tests assert both shims resolve to the SAME class objects rather than merely equivalent ones: two same-named classes are not interchangeable, which is the trap #12913 fixed for CircuitState and #12932 records for the memory graph. Verified: 6 new tests pass, 1137 passed across autobot_shared, the three user_management service modules still import, flake8 clean.
Contributor
✅ SSOT Configuration Compliance: Passing🎉 No hardcoded values detected that have SSOT config equivalents! |
This was referenced Jul 29, 2026
mrveiss
added a commit
that referenced
this pull request
Jul 31, 2026
) (#13126) * refactor(user-management): design new canonical declarative base (#12647) Resolve the models/base.py fork per the owner's 2026-07-31 decision on #12645/#12647: a new canonical base that preserves both sides' properties deliberately, not an adoption of either fork. Backend's AsyncAttrs and eager_defaults (#4300/#11684) are kept; SLM's postgresql.UUID column typing is kept (verified identical compiled DDL to the generic Uuid type on both Postgres and SQLite, so no migration is implied by the type itself); timestamps stay baked into Base unconditionally, matching backend's existing, #10636-hardened design (backend's Base also backs ~40 non-user_management models, so making timestamps opt-in would silently drop created_at/updated_at from all of them). Canonical implementation lives in autobot_shared/user_management/models/base.py; both backends keep re-export shims so existing importers are unaffected, following the base_service (#12972) / schemas.user (#13007) precedent. * refactor(user-management): converge backend model UUID typing to canonical (#12647) Replace sqlalchemy.types.Uuid with sqlalchemy.dialects.postgresql.UUID in the 8 gated model files' explicit column declarations, matching the new canonical base's type_annotation_map (SLM's existing typing) and eliminating the last mechanical divergence in these files. Also add the OKTA provider type SLM already has (feature-parity union, #12647 contract) to sso.py's SSOProviderType and is_enterprise() check. Verified: postgresql.UUID is a subclass of the generic Uuid in SQLAlchemy 2.0 (Emulated/NativeForEmulated) and compiles to identical DDL on Postgres and SQLite — no migration or behavior change from the type swap itself. Remaining diff in these files is now only legitimate per-side content: user.py's activity-tracking relationships and organization.py's LLC/PM-sync extension columns are backend-only features, left untouched. * refactor(user-management): converge SLM models onto canonical base (#12647) Drop the now-redundant TimestampMixin from class bases across the 7 model files that declared it — the new canonical Base bakes created_at/updated_at in unconditionally, so TimestampMixin is a no-op alias; combining it with Base maps the same columns once, not twice (no metaclass conflict, #4300). Adopt now_utc() (autobot_shared.time_utils) in place of inline datetime.now(timezone.utc) calls, matching the backend's existing pattern. Also: fix sso.py's is_enterprise() (SLM added SSOProviderType.OKTA but never added it to the enterprise check) and adopt the shared CategoryDefaults.ROLE_USER constant in place of the literal "user" default, matching backend's existing SSOT usage. After this, role.py, team.py, api_key.py, mfa.py, sso.py, and audit.py are identical to their backend counterparts except cosmetic typing style (Optional[X] vs X | None) and, for sso.py, SLM's accurate SystemSecret documentation — both legitimate, left as-is. user.py and organization.py retain backend's activity-tracking relationships and LLC/PM-sync columns respectively, which are real per-side features, not fork residue. * refactor(user-management): add SLM timestamp migration for role_permissions/audit_logs (#12647) The new canonical base bakes created_at/updated_at into every model unconditionally (backend's existing design). SLM's RolePermission and AuditLog models never opted into TimestampMixin and have no matching columns in the live SLM database, so they now expect columns that don't exist yet. Add an idempotent, guard-checked migration (mirrors the backend's own #10636 fix for the identical drift) and wire it into the runner's migration list. Forward-only, additive, no data loss: ADD COLUMN ... DEFAULT NOW() backfills existing rows via the server-side default, matching #10636's proven pattern. * fix(redis): guard RedisConnectionManager.__init__ against re-entrant recursion (#13128) migration-gate's test_baseline_states.py (and every test in autobot-backend/tests/migrations/) failed with a RecursionError surfacing as "Failed to get sync Redis client for database 'main': maximum recursion depth exceeded" once import models started reaching this code path earlier in the import graph (via the new models/base.py shim pulling in base_service.py's module-level get_logger() call). Root cause (confirmed via a captured traceback from a monkeypatched get_sync_client, no live app/DB/Redis involved): constructing RedisConnectionManager for the first time resolves its own Redis host via NetworkConstants.REDIS_VM_IP, which falls through to ConfigRegistry.get()'s Redis-backed cache tier, which constructs a Redis client, which constructs this same singleton again. __new__ correctly returns the same not-yet-initialized instance, but Python still calls __init__ on it again — and the old _initialized guard was set only at the very end, so the re-entrant call saw no guard and reran every init step from scratch, recursing without bound. Mark _initializing = True before running any init step; a re-entrant call now returns immediately, letting its caller (ConfigRegistry._get_redis(), already wrapped in a broad except) degrade gracefully to the env/registry-defaults tier — one clean failure instead of an unbounded recursive stack. Filed as #13128 (pre-existing, exposed rather than introduced by this PR's import-order change). * fix(user-management): make autobot_shared.user_management re-exports lazy (#12647) migration-gate's deliberately thin dependency set (no email_validator, same defect class as the jsonschema coupling PR #13053 fixed) broke because importing autobot_shared.user_management.models.base (SQLAlchemy + time_utils only) forced Python to first execute the PARENT package's __init__.py in full — which eagerly imported schemas.user (pre-existing, PR #13007), whose EmailStr fields build a pydantic CoreSchema requiring email_validator at class-definition time. That pulled email_validator into `import models`'s chain for the first time via the models/base.py shim. Confirmed statically (blocked `import email_validator` in a scratch script, no app/service execution): the shim import failed against the old eager __init__.py with the exact ImportError from CI, and succeeds against this fix. Convert the package's re-exports to lazy attribute resolution (PEP 562 module __getattr__), mirroring the pattern autobot_shared/__init__.py itself already uses. Each submodule's dependency footprint now stays scoped to callers that actually touch it — importing one mover no longer drags in the others. Identity (`pkg.Base is Base`, etc.) is preserved since __getattr__ resolves to the same module objects. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This was referenced Jul 31, 2026
mrveiss
added a commit
that referenced
this pull request
Aug 1, 2026
…t_shared (#12647) (#13164) * refactor(user-management): move byte-identical team_service to autobot_shared (#12647) team_service.py was byte-identical across autobot-backend and autobot-slm-backend — 701 duplicated lines, so a fix landed in one service or the other but never both by accident. It is the first user_management *service* that could move: its entire runtime dependency set already lives in autobot_shared — Team/TeamMembership and the audit vocabulary (#13130), BaseService/TenantContext (#12972). Its only mentions of User and Organization are in docstrings and error strings, never as classes, so it does not need the concrete per-backend models. Placed flat as autobot_shared/user_management/team_service.py, matching the existing base_service.py rather than inventing a second services/ location. Both backends keep a re-export shim so all existing importers, including each services/__init__.py, are untouched. organization_service.py is also byte-identical but deliberately not moved: it imports the concrete User and Organization at runtime, and those stay backend-local by design. * docs(session): handoff for issue-12647 team_service slice (#12647) * fix(user-management): correct two latent type errors exposed by strict mypy (#12647) team_service.py moved into autobot_shared/, which CI type-checks strictly; the backend copy never was. Two latent errors surfaced: - _apply_team_name_update declared name: str but update_team passes str | None. The body already guards with 'if name and ...', so this was a wrong annotation rather than a live crash — widened to str | None. - changes needed an explicit annotation; typed as dict[str, dict[str, Any]] to match the {'old': ..., 'new': ...} audit payload it carries. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #12647, part of #12645. First step of the user_management de-fork — the one file with no drift to reconcile. The issue stays open.
Thinking Path
#12647's difficulty is semantic drift: 25 shared-but-divergent files, with real behavioural differences in
user_service,rbac_middlewareand the models. Its own recommendation is to sub-step, starting withmodels/base+base_service.Measuring that sub-step first changed its size:
The issue lists
base_serviceas 6 diff lines; it is now 0. So half of sub-step (a) is a move with nothing to reconcile — no drift, no feature union to decide, no risk of losing either side's behaviour. Its imports areuuid,dataclasses,sqlalchemyandautobot_shared.logging_manager— nothing backend-specific, so it is portable as-is.models/base.py(64 diff lines) is left alone: that one carries the declarative-base question this issue and #12645 both flag, and it is the decision I should not make unilaterally.What Changed
autobot_shared/user_management/created as the consolidation target tech-debt(backend): de-fork the whole user_management package — forked across both backends with real drift #12647 names, withbase_service.pymoved in verbatim.auth_rbacre-exportingis_admin_role(tech-debt(auth): hand-rolled role == "admin" checks reject superadmin that require_role() admits — convert and gate the class #12948).Verification
The tests assert both shims resolve to the same class objects, not merely equivalent ones:
That identity check is the point. Two same-named classes are not interchangeable — the trap #12913 fixed for
CircuitStateand #12932 records for the memory graph — so a "de-fork" that left two equivalent-but-distinct classes would reintroduce exactly the defect this umbrella exists to remove.Why the issue stays open
This is 1 of 25 divergent files, deliberately the easiest. What remains is the actual work:
models/base.py(64 lines, gated on the declarative-base decision),rbac_middleware.py(331),database.py(440),config.py(241),user_service.py(111), and the rest — each needing drift reconciled under the issue's "union of features, zero loss" contract.The value here is establishing the target package and the shim pattern, so subsequent moves are mechanical rather than architectural.
Model Used
claude-opus-5