feat(v3-standardization): implement ID Vision for Event models and update docs

This commit is contained in:
Scott Idem
2026-02-05 17:37:52 -05:00
parent 907ff9a2f8
commit 12d725f468
3 changed files with 117 additions and 222 deletions

View File

@@ -79,7 +79,7 @@
- Reduced `hosted_file.py` (1596 -> 361 lines).
- Implemented Zoom Events OAuth2 and Sync backend.
- Verified Full File Lifecycle (Upload -> Download -> Delete) via standardized E2E test.
- **Immediate Next Step:** Verify V3 Search and CRUD for Events modules.
- **Immediate Next Step:** Await further instructions.
---

View File

@@ -1,7 +1,7 @@
import datetime, pytz
from typing import Dict, List, Optional, Set, Union
from pydantic import BaseModel, EmailStr, Field, Json, PrivateAttr, ValidationError, validator
from typing import Dict, List, Optional, Set, Union, ClassVar
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
@@ -17,38 +17,70 @@ from app.models.person_models import Person_Base
from app.models.user_models import User_Base
# ### BEGIN ### API Event Models ### Event_Base() ###
class Event_Base(BaseModel):
log.setLevel(logging.WARNING) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
log.debug(locals())
id_random: Optional[str] = Field(
**base_fields['event_id_random'],
alias = 'event_id_random',
)
id: Optional[int] = Field(
alias = 'event_id'
)
# --- Standardized Vision IDs (Strings) ---
id: Optional[str] = Field(None, **base_fields['event_id_random'])
event_id: Optional[str] = Field(None, **base_fields['event_id_random'])
account_id: Optional[str] = Field(None, **base_fields['account_id_random'])
poc_event_person_id: Optional[str] = Field(None, **base_fields['event_person_id_random'])
poc_person_id: Optional[str] = Field(None, **base_fields['person_id_random'])
user_id: Optional[str] = Field(None, **base_fields['user_id_random'])
address_location_id: Optional[str] = Field(None, **base_fields['address_id_random'])
contact_1_id: Optional[str] = Field(None, **base_fields['contact_id_random'])
contact_2_id: Optional[str] = Field(None, **base_fields['contact_id_random'])
contact_3_id: Optional[str] = Field(None, **base_fields['contact_id_random'])
# --- Standardized Legacy / Internal IDs (Excluded) ---
id_random: Optional[str] = Field(None, alias='event_id_random', exclude=True)
account_id_random: Optional[str] = Field(None, exclude=True)
poc_event_person_id_random: Optional[str] = Field(None, exclude=True)
poc_person_id_random: Optional[str] = Field(None, exclude=True)
user_id_random: Optional[str] = Field(None, exclude=True)
address_location_id_random: Optional[str] = Field(None, exclude=True)
contact_1_id_random: Optional[str] = Field(None, exclude=True)
contact_2_id_random: Optional[str] = Field(None, exclude=True)
contact_3_id_random: Optional[str] = Field(None, exclude=True)
@root_validator(pre=True)
def map_v3_ids(cls, values):
"""
Vision Transformer:
Map DB keys to clean API keys and strip internal integers.
"""
# 1. Map Random Strings to Clean Names
rid = values.get('id_random') or values.get('event_id_random')
if rid and isinstance(rid, str):
values['id'] = rid
values['event_id'] = rid
if a_rid := values.get('account_id_random'): values['account_id'] = a_rid
if pep_rid := values.get('poc_event_person_id_random'): values['poc_event_person_id'] = pep_rid
if pp_rid := values.get('poc_person_id_random'): values['poc_person_id'] = pp_rid
if u_rid := values.get('user_id_random'): values['user_id'] = u_rid
if al_rid := values.get('address_location_id_random'): values['address_location_id'] = al_rid
if c1_rid := values.get('contact_1_id_random'): values['contact_1_id'] = c1_rid
if c2_rid := values.get('contact_2_id_random'): values['contact_2_id'] = c2_rid
if c3_rid := values.get('contact_3_id_random'): values['contact_3_id'] = c3_rid
# 2. Prevent "Collision Population" or leakage of integers
for k in ['id', 'event_id', 'account_id', 'poc_event_person_id', 'poc_person_id', 'user_id', 'address_location_id', 'contact_1_id', 'contact_2_id', 'contact_3_id']:
if k in values and not isinstance(values[k], str):
del values[k]
return values
code: Optional[str] = Field(
alias = 'event_code'
)
account_id_random: Optional[str]
account_id: Optional[int]
poc_event_person_id_random: Optional[str]
poc_event_person_id: Optional[int]
poc_person_id_random: Optional[str]
poc_person_id: Optional[int]
external_person_id: Optional[str] # Person ID generated by external system (should be stable and not change)
user_id_random: Optional[str]
user_id: Optional[int]
lu_event_type_id: Optional[int]
lu_event_type_id: Optional[int] = Field(None, exclude=True)
#lu_event_type: Optional[str] # Needs to be reviewed
conference: Optional[bool] # Also in Event_Cfg_Base model
@@ -82,8 +114,6 @@ class Event_Base(BaseModel):
weekday_friday: Optional[bool]
weekday_saturday: Optional[bool]
address_location_id_random: Optional[str]
address_location_id: Optional[int]
location_address_json: Optional[Union[Json, None]]
location_text: Optional[str]
@@ -101,12 +131,6 @@ class Event_Base(BaseModel):
physical: Optional[bool] # physical in person event
virtual: Optional[bool] # virtual remote access event
contact_1_id_random: Optional[str]
contact_1_id: Optional[int]
contact_2_id_random: Optional[str]
contact_2_id: Optional[int]
contact_3_id_random: Optional[str]
contact_3_id: Optional[int]
contact_li_json: Optional[Union[Json, None]] # list of dicts (custom for client); this is SQL FULLTEXT() indexed
attend_url: Optional[str]
@@ -180,95 +204,6 @@ class Event_Base(BaseModel):
_processed_at: datetime.datetime = PrivateAttr(default_factory=datetime.datetime.now)
#@validator('event_id_random', always=True)
def event_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_id_lookup(cls, v, values, **kwargs):
log.setLevel(logging.WARNING)
log.debug(locals())
if values['id_random']:
log.debug(values['id_random'])
return redis_lookup_id_random(record_id_random=values['id_random'], table_name='event')
return None
@validator('account_id', always=True)
def account_id_lookup(cls, v, values, **kwargs):
if isinstance(v, int) and v > 0: return v
elif id_random := values.get('account_id_random'):
return redis_lookup_id_random(record_id_random=id_random, table_name='account')
return None
@validator('poc_event_person_id', always=True)
def poc_event_person_id_lookup(cls, v, values, **kwargs):
log.setLevel(logging.WARNING)
log.debug(locals())
if values['poc_event_person_id_random']:
return redis_lookup_id_random(record_id_random=values['poc_event_person_id_random'], table_name='event_person')
return None
@validator('poc_person_id', always=True)
def poc_person_id_lookup(cls, v, values, **kwargs):
log.setLevel(logging.WARNING)
log.debug(locals())
if values['poc_person_id_random']:
return redis_lookup_id_random(record_id_random=values['poc_person_id_random'], table_name='person')
return None
@validator('user_id', always=True)
def user_id_lookup(cls, v, values, **kwargs):
log.setLevel(logging.WARNING)
log.debug(locals())
if values['user_id_random']:
return redis_lookup_id_random(record_id_random=values['user_id_random'], table_name='user')
return None
@validator('address_location_id', always=True)
def address_location_id_lookup(cls, v, values, **kwargs):
log.setLevel(logging.WARNING)
log.debug(locals())
if values['address_location_id_random']:
return redis_lookup_id_random(record_id_random=values['address_location_id_random'], table_name='address')
return None
@validator('contact_1_id', always=True)
def contact_1_id_lookup(cls, v, values, **kwargs):
log.setLevel(logging.WARNING)
log.debug(locals())
if values['contact_1_id_random']:
return redis_lookup_id_random(record_id_random=values['contact_1_id_random'], table_name='contact')
return None
@validator('contact_2_id', always=True)
def contact_2_id_lookup(cls, v, values, **kwargs):
log.setLevel(logging.WARNING)
log.debug(locals())
if values['contact_2_id_random']:
return redis_lookup_id_random(record_id_random=values['contact_2_id_random'], table_name='contact')
return None
@validator('contact_3_id', always=True)
def contact_3_id_lookup(cls, v, values, **kwargs):
log.setLevel(logging.WARNING)
log.debug(locals())
if values['contact_3_id_random']:
return redis_lookup_id_random(record_id_random=values['contact_3_id_random'], table_name='contact')
return None
@validator('created_on', always=True)
def created_on_utc(cls, v, values, **kwargs):
if isinstance(v, datetime.datetime):
@@ -300,31 +235,66 @@ class Event_Meeting_Flat_Base(BaseModel):
log.setLevel(logging.WARNING) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
log.debug(locals())
id_random: Optional[str] = Field(
**base_fields['event_id_random'],
alias = 'event_id_random',
)
id: Optional[int] = Field(
alias = 'event_id'
)
# --- Standardized Vision IDs (Strings) ---
id: Optional[str] = Field(None, **base_fields['event_id_random'])
event_id: Optional[str] = Field(None, **base_fields['event_id_random'])
account_id: Optional[str] = Field(None, **base_fields['account_id_random'])
poc_event_person_id: Optional[str] = Field(None, **base_fields['event_person_id_random'])
poc_person_id: Optional[str] = Field(None, **base_fields['person_id_random'])
user_id: Optional[str] = Field(None, **base_fields['user_id_random'])
address_location_id: Optional[str] = Field(None, **base_fields['address_id_random'])
contact_1_id: Optional[str] = Field(None, **base_fields['contact_id_random'])
contact_2_id: Optional[str] = Field(None, **base_fields['contact_id_random'])
contact_3_id: Optional[str] = Field(None, **base_fields['contact_id_random'])
# --- Standardized Legacy / Internal IDs (Excluded) ---
id_random: Optional[str] = Field(None, alias='event_id_random', exclude=True)
account_id_random: Optional[str] = Field(None, exclude=True)
poc_event_person_id_random: Optional[str] = Field(None, exclude=True)
poc_person_id_random: Optional[str] = Field(None, exclude=True)
user_id_random: Optional[str] = Field(None, exclude=True)
address_location_id_random: Optional[str] = Field(None, exclude=True)
contact_1_id_random: Optional[str] = Field(None, exclude=True)
contact_2_id_random: Optional[str] = Field(None, exclude=True)
contact_3_id_random: Optional[str] = Field(None, exclude=True)
@root_validator(pre=True)
def map_v3_ids(cls, values):
"""
Vision Transformer:
Map DB keys to clean API keys and strip internal integers.
"""
# 1. Map Random Strings to Clean Names
rid = values.get('id_random') or values.get('event_id_random')
if rid and isinstance(rid, str):
values['id'] = rid
values['event_id'] = rid
if a_rid := values.get('account_id_random'): values['account_id'] = a_rid
if pep_rid := values.get('poc_event_person_id_random'): values['poc_event_person_id'] = pep_rid
if pp_rid := values.get('poc_person_id_random'): values['poc_person_id'] = pp_rid
if u_rid := values.get('user_id_random'): values['user_id'] = u_rid
if al_rid := values.get('address_location_id_random'): values['address_location_id'] = al_rid
if c1_rid := values.get('contact_1_id_random'): values['contact_1_id'] = c1_rid
if c2_rid := values.get('contact_2_id_random'): values['contact_2_id'] = c2_rid
if c3_rid := values.get('contact_3_id_random'): values['contact_3_id'] = c3_rid
# 2. Prevent "Collision Population" or leakage of integers
for k in ['id', 'event_id', 'account_id', 'poc_event_person_id', 'poc_person_id', 'user_id', 'address_location_id', 'contact_1_id', 'contact_2_id', 'contact_3_id']:
if k in values and not isinstance(values[k], str):
del values[k]
return values
code: Optional[str] = Field(
alias = 'event_code'
)
account_id_random: Optional[str]
account_id: Optional[int]
external_person_id: Optional[str] # Person ID generated by external system (should be stable and not change)
# poc_event_person_id_random: Optional[str]
# poc_event_person_id: Optional[int]
# poc_person_id_random: Optional[str]
# poc_person_id: Optional[int]
# user_id_random: Optional[str]
# user_id: Optional[int]
lu_event_type_id: Optional[int]
lu_event_type_id: Optional[int] = Field(None, exclude=True)
#lu_event_type: Optional[str] # Needs to be reviewed
conference: Optional[bool] # Also in Event_Cfg_Base model
@@ -358,8 +328,6 @@ class Event_Meeting_Flat_Base(BaseModel):
weekday_friday: Optional[bool]
weekday_saturday: Optional[bool]
address_location_id_random: Optional[str]
address_location_id: Optional[int]
location_address_json: Optional[Union[Json, None]]
location_text: Optional[str]
@@ -377,14 +345,6 @@ class Event_Meeting_Flat_Base(BaseModel):
physical: Optional[bool] # physical in person event
virtual: Optional[bool] # virtual remote access event
external_person_id: Optional[str] # Person ID generated by external system (should be stable and not change)
contact_1_id_random: Optional[str]
contact_1_id: Optional[int]
contact_2_id_random: Optional[str]
contact_2_id: Optional[int]
contact_3_id_random: Optional[str]
contact_3_id: Optional[int]
contact_li_json: Optional[Union[Json, None]] # list of dicts (custom for client); this is SQL FULLTEXT() indexed
attend_url: Optional[str]
@@ -431,9 +391,11 @@ class Event_Meeting_Flat_Base(BaseModel):
created_on: Optional[datetime.datetime] = None
updated_on: Optional[datetime.datetime] = None
# Including convenience data
address_id_random: Optional[str]
address_id: Optional[int]
# --- IDAA Recovery Meetings: Convenience Data (Flat) ---
# These fields are primarily for the flat "Meeting" view used by the IDAA mobile/web apps.
# Note: We prioritize string IDs (id_random) for all external API consumers.
address_id_random: Optional[str] = Field(None, **base_fields['address_id_random'])
address_name: Optional[str]
address_line_1: Optional[str]
address_line_2: Optional[str]
@@ -445,8 +407,6 @@ class Event_Meeting_Flat_Base(BaseModel):
address_country_alpha_2_code: Optional[str]
address_country_name: Optional[str]
contact_1_id_random: Optional[str]
contact_1_id: Optional[int]
contact_1_name: Optional[str] # Avoid using or use as something different?
contact_1_full_name: Optional[str] # Yes... it is the same as "name"
contact_1_email: Optional[str]
@@ -458,8 +418,6 @@ class Event_Meeting_Flat_Base(BaseModel):
contact_1_phone_other: Optional[str]
contact_1_other_text: Optional[str]
contact_2_id_random: Optional[str]
contact_2_id: Optional[int]
contact_2_name: Optional[str] # Avoid using or use as something different?
contact_2_full_name: Optional[str] # Yes... it is the same as "name"
contact_2_email: Optional[str]
@@ -473,70 +431,6 @@ class Event_Meeting_Flat_Base(BaseModel):
_processed_at: datetime.datetime = PrivateAttr(default_factory=datetime.datetime.now)
#@validator('event_id_random', always=True)
def event_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_id_lookup(cls, v, values, **kwargs):
log.setLevel(logging.WARNING)
log.debug(locals())
if values['id_random']:
log.debug(values['id_random'])
return redis_lookup_id_random(record_id_random=values['id_random'], table_name='event')
return None
# @validator('account_id', always=True)
# def account_id_lookup(cls, v, values, **kwargs):
# log.setLevel(logging.WARNING)
# log.debug(locals())
# if values['account_id_random']:
# return redis_lookup_id_random(record_id_random=values['account_id_random'], table_name='account')
# return None
# @validator('address_id', always=True)
# def address_id_lookup(cls, v, values, **kwargs):
# log.setLevel(logging.WARNING)
# log.debug(locals())
# if values['address_id_random']:
# return redis_lookup_id_random(record_id_random=values['address_id_random'], table_name='address')
# return None
# @validator('contact_1_id', always=True)
# def contact_1_id_lookup(cls, v, values, **kwargs):
# log.setLevel(logging.WARNING)
# log.debug(locals())
# if values['contact_1_id_random']:
# return redis_lookup_id_random(record_id_random=values['contact_1_id_random'], table_name='contact')
# return None
# @validator('contact_2_id', always=True)
# def contact_2_id_lookup(cls, v, values, **kwargs):
# log.setLevel(logging.WARNING)
# log.debug(locals())
# if values['contact_2_id_random']:
# return redis_lookup_id_random(record_id_random=values['contact_2_id_random'], table_name='contact')
# return None
# @validator('contact_3_id', always=True)
# def contact_3_id_lookup(cls, v, values, **kwargs):
# log.setLevel(logging.WARNING)
# log.debug(locals())
# if values['contact_3_id_random']:
# return redis_lookup_id_random(record_id_random=values['contact_3_id_random'], table_name='contact')
# return None
@validator('created_on', always=True)
def created_on_utc(cls, v, values, **kwargs):
if isinstance(v, datetime.datetime):

View File

@@ -169,8 +169,9 @@ The following endpoints are maintained for backward compatibility but should be
V3 uses a **String-Only ID Vision**. The frontend NEVER handles or stores database integers.
1. **Automatic Mapping:** Internal `id_random` fields are mapped to `id` and `account_id` in JSON responses.
1. **Automatic Mapping:** Internal `id_random` fields are mapped to clean names (e.g., `id`, `event_id`, `account_id`) in JSON responses.
2. **Intelligent Resolution:** You can send random string IDs back to the API in any `*_id` field; the API resolves them to integers before database insertion.
3. **Suffix Compatibility:** The `_random` suffix (e.g., `event_id_random`) is maintained for backward compatibility with V2 clients. Standard V3 frontend integration should prioritize fields without the suffix.
---