feat(v3-vision): implement resilient "Heal-on-Read" ID resolution

1. Hardened Event_Exhibit and Event_Exhibit_Tracking models with automatic Redis/DB fallback for missing string IDs.
2. Fully modernized Event_Person_Tracking_Base to the Vision Standard (Union IDs + Root Validator).
3. Enabled account-based search for event_person_tracking.
4. Verified all changes via e2e demo parity suite.
This commit is contained in:
Scott Idem
2026-02-07 19:27:44 -05:00
parent e8f9472c5c
commit 9715d28bd6
4 changed files with 135 additions and 104 deletions

View File

@@ -39,21 +39,45 @@ class Event_Exhibit_Base(BaseModel):
"""
Vision Transformer:
Map DB keys to clean API keys and strip internal integers during READ operations.
Falls back to Redis/DB lookups if random string IDs are missing from the view.
"""
# 1. Map Random Strings to Clean Names
from app.db_sql import get_id_random
# 1. Map Primary Object ID
rid = values.get('id_random') or values.get('event_exhibit_id_random')
if rid and isinstance(rid, str):
values['id'] = rid
values['event_exhibit_id'] = rid
if a_rid := values.get('account_id_random'): values['account_id'] = a_rid
if e_rid := values.get('event_id_random'): values['event_id'] = e_rid
if o_rid := values.get('organization_id_random'): values['organization_id'] = o_rid
if c_rid := values.get('contact_id_random'): values['contact_id'] = c_rid
if p_rid := values.get('person_id_random'): values['person_id'] = p_rid
if s_rid := values.get('status_id_random'): values['status_id'] = s_rid
# 2. Prevent leakage of integers during API responses (Vision Standard)
elif values.get('id') and isinstance(values.get('id'), int):
# Fallback for primary ID
resolved_rid = get_id_random(values['id'], 'event_exhibit')
if resolved_rid:
values['id'] = resolved_rid
values['event_exhibit_id'] = resolved_rid
values['id_random'] = resolved_rid
# 2. Map & Resolve Relational IDs
id_map = [
('account_id', 'account'),
('event_id', 'event'),
('organization_id', 'organization'),
('contact_id', 'contact'),
('person_id', 'person'),
('status_id', 'status'),
]
for field, table in id_map:
r_val = values.get(f'{field}_random')
if r_val and isinstance(r_val, str):
values[field] = r_val
elif values.get(field) and isinstance(values[field], int):
# Fallback: Resolve from Redis/DB if missing from view result
resolved_rid = get_id_random(values[field], table)
if resolved_rid:
values[field] = resolved_rid
values[f'{field}_random'] = resolved_rid
# 3. Final Vision Enforcement: Strip internal integers
for k in ['id', 'event_exhibit_id', 'account_id', 'event_id', 'organization_id', 'contact_id', 'person_id', 'status_id']:
val = values.get(k)
if val is not None and not isinstance(val, str):

View File

@@ -39,21 +39,44 @@ class Event_Exhibit_Tracking_Base(BaseModel):
"""
Vision Transformer:
Map DB keys to clean API keys and strip internal integers during READ operations.
During CREATE (POST) operations, we ensure resolved integers are preserved.
Falls back to Redis/DB lookups if random string IDs are missing from the view.
"""
# 1. Map Random Strings to Clean Names
from app.db_sql import get_id_random
# 1. Map Primary Object ID
rid = values.get('id_random') or values.get('event_exhibit_tracking_id_random')
if rid and isinstance(rid, str):
values['id'] = rid
values['event_exhibit_tracking_id'] = rid
if a_rid := values.get('account_id_random'): values['account_id'] = a_rid
if e_rid := values.get('event_id_random'): values['event_id'] = e_rid
if ex_rid := values.get('event_exhibit_id_random'): values['event_exhibit_id'] = ex_rid
if ep_rid := values.get('event_person_id_random'): values['event_person_id'] = ep_rid
if eb_rid := values.get('event_badge_id_random'): values['event_badge_id'] = eb_rid
# 2. Prevent leakage of integers during API responses (Vision Standard)
elif values.get('id') and isinstance(values.get('id'), int):
# Fallback for primary ID
resolved_rid = get_id_random(values['id'], 'event_exhibit_tracking')
if resolved_rid:
values['id'] = resolved_rid
values['event_exhibit_tracking_id'] = resolved_rid
values['id_random'] = resolved_rid
# 2. Map & Resolve Relational IDs
id_map = [
('account_id', 'account'),
('event_id', 'event'),
('event_exhibit_id', 'event_exhibit'),
('event_person_id', 'event_person'),
('event_badge_id', 'event_badge'),
]
for field, table in id_map:
r_val = values.get(f'{field}_random')
if r_val and isinstance(r_val, str):
values[field] = r_val
elif values.get(field) and isinstance(values[field], int):
# Fallback: Resolve from Redis/DB if missing from view result
resolved_rid = get_id_random(values[field], table)
if resolved_rid:
values[field] = resolved_rid
values[f'{field}_random'] = resolved_rid
# 3. Final Vision Enforcement: Strip internal integers
for k in ['id', 'event_exhibit_tracking_id', 'account_id', 'event_id', 'event_exhibit_id', 'event_person_id', 'event_badge_id']:
val = values.get(k)
if val is not None and not isinstance(val, str):

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
import datetime, hashlib, logging, os, pytz, redis, secrets
from typing import Dict, List, Optional, Set, Union
from pydantic import BaseModel, EmailStr, Field, Json, PrivateAttr, ValidationError, validator
from pydantic import BaseModel, EmailStr, Field, Json, PrivateAttr, ValidationError, validator, root_validator
from app.db_sql import redis_lookup_id_random
from app.lib_general import log, logging
@@ -14,26 +14,69 @@ class Event_Person_Tracking_Base(BaseModel):
log.setLevel(logging.WARNING) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
log.debug(locals())
id_random: Optional[str] = Field(
**base_fields['event_person_tracking_id_random'],
alias = 'event_person_tracking_id_random',
default_factory = lambda:secrets.token_urlsafe(default_num_bytes),
)
id: Optional[int] = Field(
alias = 'event_person_tracking_id'
)
# --- Standardized Vision IDs (Strings for API, Integers for DB) ---
id: Optional[Union[int, str]] = Field(**base_fields['event_person_tracking_id_random'])
event_person_tracking_id: Optional[Union[int, str]] = Field(**base_fields['event_person_tracking_id_random'])
account_id: Optional[Union[int, str]] = Field(**base_fields['account_id_random'])
event_id: Optional[Union[int, str]] = Field(**base_fields['event_id_random'])
event_session_id: Optional[Union[int, str]] = Field(**base_fields['event_session_id_random'])
event_person_id: Optional[Union[int, str]] = Field(**base_fields['event_person_id_random'])
# account_id_random: Optional[str]
# account_id: Optional[int]
# --- Standardized Legacy / Internal IDs (Excluded) ---
id_random: Optional[str] = Field(None, alias='event_person_tracking_id_random', exclude=True)
account_id_random: Optional[str] = Field(None, exclude=True)
event_id_random: Optional[str] = Field(None, exclude=True)
event_session_id_random: Optional[str] = Field(None, exclude=True)
event_person_id_random: Optional[str] = Field(None, exclude=True)
event_id_random: Optional[str]
event_id: Optional[int]
@root_validator(pre=True)
def map_v3_ids(cls, values):
"""
Vision Transformer:
Map DB keys to clean API keys and strip internal integers during READ operations.
Falls back to Redis/DB lookups if random string IDs are missing from the view.
"""
from app.db_sql import get_id_random
event_session_id_random: Optional[str]
event_session_id: Optional[int]
# 1. Map Primary Object ID
rid = values.get('id_random') or values.get('event_person_tracking_id_random')
if rid and isinstance(rid, str):
values['id'] = rid
values['event_person_tracking_id'] = rid
elif values.get('id') and isinstance(values.get('id'), int):
# Fallback for primary ID
resolved_rid = get_id_random(values['id'], 'event_person_tracking')
if resolved_rid:
values['id'] = resolved_rid
values['event_person_tracking_id'] = resolved_rid
values['id_random'] = resolved_rid
event_person_id_random: Optional[str]
event_person_id: Optional[int]
# 2. Map & Resolve Relational IDs
id_map = [
('account_id', 'account'),
('event_id', 'event'),
('event_session_id', 'event_session'),
('event_person_id', 'event_person'),
]
for field, table in id_map:
r_val = values.get(f'{field}_random')
if r_val and isinstance(r_val, str):
values[field] = r_val
elif values.get(field) and isinstance(values[field], int):
# Fallback: Resolve from Redis/DB if missing from view result
resolved_rid = get_id_random(values[field], table)
if resolved_rid:
values[field] = resolved_rid
values[f'{field}_random'] = resolved_rid
# 3. Final Vision Enforcement: Strip internal integers
for k in ['id', 'event_person_tracking_id', 'account_id', 'event_id', 'event_session_id', 'event_person_id']:
val = values.get(k)
if val is not None and not isinstance(val, str):
values[k] = None
return values
check_in_out: Optional[bool]
break_in_out: Optional[bool]
@@ -43,15 +86,11 @@ class Event_Person_Tracking_Base(BaseModel):
in_datetime: Optional[datetime.datetime] # This should generally default to the created datetime and be overridden as needed
out_datetime: Optional[datetime.datetime] # This should generally default to the updated datetime and be overridden as needed
# Maybe add minutes or hours?
# Maybe add timezone?
check_in: Optional[bool] # Does this make sense to use instead?
break_out: Optional[bool] # Does this make sense to use instead?
break_in: Optional[bool] # Does this make sense to use instead?
check_out: Optional[bool] # Does this make sense to use instead?
datetime: Optional[datetime.datetime] # This should generally default to the created datetime and be overridden as needed
check_in: Optional[bool]
break_out: Optional[bool]
break_in: Optional[bool]
check_out: Optional[bool]
datetime: Optional[datetime.datetime]
enable: Optional[bool]
@@ -60,14 +99,6 @@ class Event_Person_Tracking_Base(BaseModel):
updated_on: Optional[datetime.datetime] = None
# Including convenience data
# This is only for convenience. Probably going to keep unless it causes a problem.
# full_name: Optional[str] = Field(
# alias = 'event_person_full_name'
# )
# display_name: Optional[str] = Field(
# alias = 'event_person_display_name'
# )
event_person_informal_name: Optional[str]
event_person_given_name: Optional[str]
event_person_family_name: Optional[str]
@@ -83,57 +114,10 @@ class Event_Person_Tracking_Base(BaseModel):
track_name: Optional[str] = Field(
alias = 'event_track_name'
)
# Maybe add timezone in the future?
_processed_at: datetime.datetime = PrivateAttr(default_factory=datetime.datetime.now)
#@validator('event_person_tracking_id_random', always=True)
def event_person_tracking_id_random_copy(cls, v, values, **kwargs):
log.setLevel(logging.WARNING)
log.debug(locals())
if values['id_random']:
return values['id_random']
return None
@validator('id', always=True)
def event_person_tracking_id_lookup(cls, v, values, **kwargs):
log.setLevel(logging.WARNING)
log.debug(locals())
if values.get('id_random', None):
log.debug(values['id_random'])
return redis_lookup_id_random(record_id_random=values['id_random'], table_name='event_person_tracking')
return None
@validator('event_id', always=True)
def event_id_lookup(cls, v, values, **kwargs):
log.setLevel(logging.WARNING)
log.debug(locals())
if values.get('event_id_random', None):
return redis_lookup_id_random(record_id_random=values['event_id_random'], table_name='event')
return None
@validator('event_session_id', always=True)
def event_session_id_lookup(cls, v, values, **kwargs):
log.setLevel(logging.WARNING)
log.debug(locals())
if values.get('event_session_id_random', None):
return redis_lookup_id_random(record_id_random=values['event_session_id_random'], table_name='event_session')
return None
@validator('event_person_id', always=True)
def event_person_id_lookup(cls, v, values, **kwargs):
log.setLevel(logging.WARNING)
log.debug(locals())
if values.get('event_person_id_random', None):
return redis_lookup_id_random(record_id_random=values['event_person_id_random'], table_name='event_person')
return None
class Config:
underscore_attrs_are_private = True
allow_population_by_field_name = True
fields = base_fields
fields = base_fields