Files
zeekay 1b367e2b62 chore: rebase the tree onto upstream master
The tree was a debranded snapshot of upstream taken around 2026-02-20, and
upstream has moved about five months since. This replaces it with upstream
master, mechanically re-derived: bin/debrand does the rename, and the
pipeline and squashed migrations are restored from the shipped tree.

Not a merge. The previous import replaced the tree wholesale in a single
commit, so there is no shared content history to merge against. Reproducing
the rename on the new tree is what actually converges.

What survives: .hanzo/workflows, the Dockerfiles and bin/docker* entrypoints,
compose files, LLM.md, and insights/migrations. The squashed initial
migration is load-bearing — the live database records it by name, so
upstream's ~918 individual migrations cannot come back.

Nine products are dropped as unreferenced. Twenty-one more that we do not run
are kept anyway because the core app still imports them; unpicking those is a
refactor and does not belong in a rebase.

This base does not build yet, and the reasons are not in the rename:
upstream now wants Python 3.13.13 against our 3.12.12 and Django 5.2 against
our 4.2, and the dependency manifests still name upstream packages that we
republish under our own names. Those are decisions, not renames.
2026-08-01 15:44:40 -07:00

339 lines
13 KiB
Python

from typing import Any
from insights.api.team import CachingTeamSerializer
from insights.models.team import Team
from insights.models.team.team_caching import get_team_in_cache, set_team_in_cache
def check_team_cache_consistency(team_id_or_token: str) -> dict[str, Any]:
"""
Check for inconsistencies between the database and cache for a specific team's settings.
Args:
team_id_or_token: The team ID or API token
Returns:
Dict with information about the team and any inconsistencies
"""
result: dict[str, Any] = {
"team_id": None,
"team_name": None,
"has_inconsistency": False,
"inconsistent_fields": [],
"db_values": {},
"cache_values": {},
"fixed": False,
}
# Try to get team by ID or token
try:
try:
team_id = int(team_id_or_token)
team = Team.objects.get(id=team_id)
except ValueError:
team = Team.objects.get(api_token=team_id_or_token)
result["team_id"] = team.id
result["team_name"] = team.name
# Get team from cache
cached_team = get_team_in_cache(team.api_token)
if cached_team:
# Get serialized data for both DB and cache
db_serialized = CachingTeamSerializer(team).data
cache_serialized = CachingTeamSerializer(cached_team).data
# Check for inconsistencies across all fields
for key in db_serialized:
db_value = db_serialized[key]
cache_value = cache_serialized.get(key)
if db_value != cache_value:
result["has_inconsistency"] = True
result["inconsistent_fields"].append(key)
result["db_values"][key] = db_value
result["cache_values"][key] = cache_value
if result["has_inconsistency"]:
print(f"Inconsistency found for team {team.id} ({team.name}):") # noqa: T201
for field in result["inconsistent_fields"]:
print(f" - {field}: DB={result['db_values'][field]}, Cache={result['cache_values'][field]}") # noqa: T201
else:
print(f"Team {team.id} not found in cache") # noqa: T201
except Team.DoesNotExist:
print(f"Team with ID/token {team_id_or_token} not found") # noqa: T201
return result
def fix_team_cache_consistency(team_id_or_token: str) -> dict[str, Any]:
"""
Fix inconsistencies between the database and cache for a specific team's settings.
Args:
team_id_or_token: The team ID or API token
Returns:
Dict with information about the team and the fix operation
"""
result = check_team_cache_consistency(team_id_or_token)
if result["has_inconsistency"]:
try:
# Get the team again to ensure we have the latest data
team = Team.objects.get(id=result["team_id"])
# Update the cache
set_team_in_cache(team.api_token, team)
result["fixed"] = True
print(f"Fixed cache for team {team.id}") # noqa: T201
# Verify the fix
cached_team = get_team_in_cache(team.api_token)
if cached_team:
# Re-check if all fields are now consistent
db_serialized = CachingTeamSerializer(team).data
cache_serialized = CachingTeamSerializer(cached_team).data
all_fixed = True
remaining_inconsistencies: list[str] = []
for key in result["inconsistent_fields"]:
if db_serialized[key] != cache_serialized.get(key):
all_fixed = False
remaining_inconsistencies.append(key)
if all_fixed:
print("Verified: Cache now has correct values for all fields") # noqa: T201
else:
print(f"Warning: Cache still has inconsistencies for fields: {remaining_inconsistencies}") # noqa: T201
else:
print("Warning: Team not found in cache after fix attempt") # noqa: T201
except Exception as e:
print(f"Error fixing cache: {str(e)}") # noqa: T201
else:
print("No inconsistency found, no fix needed") # noqa: T201
return result
def find_teams_with_cache_inconsistencies(
batch_size: int = 100, only_active_surveys: bool = True
) -> list[dict[str, Any]]:
"""
Find all teams with cache inconsistencies between database and cache.
Args:
batch_size: Number of teams to process in each batch.
only_active_surveys: If True, only check teams with active surveys.
Returns:
List of dictionaries with information about teams with inconsistencies
"""
from products.surveys.backend.models import Survey
inconsistent_teams: list[dict[str, Any]] = []
teams_checked = 0
teams_in_cache = 0
inconsistency_counts: dict[str, int] = {}
# Variables used in both branches
inconsistent_fields: list[str] = []
db_values: dict[str, Any] = {}
cache_values: dict[str, Any] = {}
team_info: dict[str, Any] = {}
# Get teams to check
if only_active_surveys:
# Get teams with active surveys using Django ORM
team_ids_with_active_surveys = (
Survey.objects.filter(start_date__isnull=False, end_date__isnull=True, archived=False)
.values_list("team_id", flat=True)
.distinct()
)
# Count teams with active surveys
total_teams = len(team_ids_with_active_surveys)
print(f"Checking cache consistency for {total_teams} teams with active surveys...") # noqa: T201
# Process teams in batches to avoid memory issues
for i in range(0, len(team_ids_with_active_surveys), batch_size):
batch_ids = team_ids_with_active_surveys[i : i + batch_size]
teams_batch = Team.objects.filter(id__in=batch_ids)
for team in teams_batch:
teams_checked += 1
# Print progress
if teams_checked % 50 == 0 or teams_checked == total_teams:
progress_pct = (teams_checked / total_teams) * 100 if total_teams > 0 else 0
print(f"Progress: {teams_checked}/{total_teams} teams checked ({progress_pct:.1f}%)") # noqa: T201
# Get team from cache
cached_team = get_team_in_cache(team.api_token)
# Skip teams not in cache
if not cached_team:
continue
teams_in_cache += 1
# Check for inconsistencies across all fields
db_serialized = CachingTeamSerializer(team).data
cache_serialized = CachingTeamSerializer(cached_team).data
inconsistent_fields.clear()
db_values.clear()
cache_values.clear()
for key in db_serialized:
if db_serialized[key] != cache_serialized.get(key):
inconsistent_fields.append(key)
db_values[key] = db_serialized[key]
cache_values[key] = cache_serialized.get(key)
# Track total inconsistencies by field
inconsistency_counts[key] = inconsistency_counts.get(key, 0) + 1
if inconsistent_fields:
team_info = {
"team_id": team.id,
"team_name": team.name,
"inconsistent_fields": inconsistent_fields.copy(),
"db_values": db_values.copy(),
"cache_values": cache_values.copy(),
}
inconsistent_teams.append(team_info)
else:
# Count all teams
total_teams = Team.objects.count()
print(f"Checking cache consistency for all {total_teams} teams...") # noqa: T201
# Process all teams in batches using Django's pagination
offset = 0
while True:
teams_batch = Team.objects.all().order_by("id")[offset : offset + batch_size]
# Exit loop if no more teams
if not teams_batch:
break
for team in teams_batch:
teams_checked += 1
# Print progress every 500 teams
if teams_checked % 500 == 0 or teams_checked == total_teams:
progress_pct = (teams_checked / total_teams) * 100 if total_teams > 0 else 0
print(f"Progress: {teams_checked}/{total_teams} teams checked ({progress_pct:.1f}%)") # noqa: T201
# Get team from cache
cached_team = get_team_in_cache(team.api_token)
# Skip teams not in cache
if not cached_team:
continue
teams_in_cache += 1
# Check for inconsistencies across all fields
db_serialized = CachingTeamSerializer(team).data
cache_serialized = CachingTeamSerializer(cached_team).data
inconsistent_fields.clear()
db_values.clear()
cache_values.clear()
for key in db_serialized:
if db_serialized[key] != cache_serialized.get(key):
inconsistent_fields.append(key)
db_values[key] = db_serialized[key]
cache_values[key] = cache_serialized.get(key)
# Track total inconsistencies by field
inconsistency_counts[key] = inconsistency_counts.get(key, 0) + 1
if inconsistent_fields:
team_info = {
"team_id": team.id,
"team_name": team.name,
"inconsistent_fields": inconsistent_fields.copy(),
"db_values": db_values.copy(),
"cache_values": cache_values.copy(),
}
inconsistent_teams.append(team_info)
# Move to next batch
offset += batch_size
# Print summary
print("\nSummary:") # noqa: T201
print(f"Teams checked: {teams_checked}") # noqa: T201
print(f"Teams in cache: {teams_in_cache}") # noqa: T201
print(f"Teams with inconsistencies: {len(inconsistent_teams)}") # noqa: T201
# Print inconsistency breakdown by field
if inconsistency_counts:
print("\nInconsistencies by field:") # noqa: T201
for field, count in sorted(inconsistency_counts.items(), key=lambda x: x[1], reverse=True):
print(f" - {field}: {count} teams") # noqa: T201
# Print the first 10 inconsistent teams as a preview
if inconsistent_teams:
print("\nFirst 10 inconsistent teams:") # noqa: T201
for idx, team_info in enumerate(inconsistent_teams[:10]):
print(f"{idx + 1}. Team {team_info['team_id']} ({team_info['team_name']})") # noqa: T201
for field in team_info["inconsistent_fields"]:
print(f" - {field}: DB={team_info['db_values'][field]}, Cache={team_info['cache_values'][field]}") # noqa: T201
return inconsistent_teams
def fix_all_teams_cache_consistency(batch_size: int = 100, only_active_surveys: bool = True) -> dict[str, Any]:
"""
Find and fix all teams with cache inconsistencies.
Args:
batch_size: Number of teams to process in each batch.
only_active_surveys: If True, only check teams with active surveys.
Returns:
Dict with statistics about the fixed teams
"""
# First find all teams with inconsistencies
inconsistent_teams = find_teams_with_cache_inconsistencies(batch_size, only_active_surveys)
if not inconsistent_teams:
print("No inconsistencies found.") # noqa: T201
return {"teams_fixed": 0, "total_inconsistencies": 0, "field_fixes": {}}
# Ask for confirmation before fixing
confirmation = input(f"\nFound {len(inconsistent_teams)} teams with inconsistencies. Fix them? (y/n): ")
if confirmation.lower() != "y":
print("Operation canceled.") # noqa: T201
return {"teams_fixed": 0, "total_inconsistencies": len(inconsistent_teams), "field_fixes": {}}
# Track fixes by field
field_fixes: dict[str, int] = {}
for team_info in inconsistent_teams:
for field in team_info["inconsistent_fields"]:
field_fixes[field] = field_fixes.get(field, 0) + 1
# Fix the inconsistencies
fixed_count = 0
for team_info in inconsistent_teams:
try:
team = Team.objects.get(id=team_info["team_id"])
set_team_in_cache(team.api_token, team)
fixed_count += 1
# Print progress every 10 teams
if fixed_count % 10 == 0:
print(f"Fixed {fixed_count}/{len(inconsistent_teams)} teams...") # noqa: T201
except Exception as e:
print(f"Error fixing team {team_info['team_id']}: {str(e)}") # noqa: T201
print(f"\nFixed {fixed_count} out of {len(inconsistent_teams)} teams with inconsistencies.") # noqa: T201
# Print breakdown of fixes by field
print("\nFields fixed:") # noqa: T201
for field, count in sorted(field_fixes.items(), key=lambda x: x[1], reverse=True):
print(f" - {field}: {count} teams") # noqa: T201
return {"teams_fixed": fixed_count, "total_inconsistencies": len(inconsistent_teams), "field_fixes": field_fixes}