refactored attributes / timestamps / data objects CRUD
This commit is contained in:
parent
4edaba3471
commit
2b5287188d
9 changed files with 479 additions and 108 deletions
|
|
@ -31,10 +31,39 @@ class ConnectionStatus(str, Enum):
|
|||
|
||||
class Mandate(BaseModel, ModelMixin):
|
||||
"""Data model for a mandate"""
|
||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the mandate")
|
||||
name: str = Field(description="Name of the mandate")
|
||||
language: str = Field(default="en", description="Default language of the mandate")
|
||||
enabled: bool = Field(default=True, description="Indicates whether the mandate is enabled")
|
||||
id: str = Field(
|
||||
default_factory=lambda: str(uuid.uuid4()),
|
||||
description="Unique ID of the mandate",
|
||||
frontend_type="text",
|
||||
frontend_readonly=True,
|
||||
frontend_required=False
|
||||
)
|
||||
name: str = Field(
|
||||
description="Name of the mandate",
|
||||
frontend_type="text",
|
||||
frontend_readonly=False,
|
||||
frontend_required=True
|
||||
)
|
||||
language: str = Field(
|
||||
default="en",
|
||||
description="Default language of the mandate",
|
||||
frontend_type="select",
|
||||
frontend_readonly=False,
|
||||
frontend_required=True,
|
||||
frontend_options=[
|
||||
{"value": "de", "label": {"en": "Deutsch", "fr": "Allemand"}},
|
||||
{"value": "en", "label": {"en": "English", "fr": "Anglais"}},
|
||||
{"value": "fr", "label": {"en": "Français", "fr": "Français"}},
|
||||
{"value": "it", "label": {"en": "Italiano", "fr": "Italien"}}
|
||||
]
|
||||
)
|
||||
enabled: bool = Field(
|
||||
default=True,
|
||||
description="Indicates whether the mandate is enabled",
|
||||
frontend_type="checkbox",
|
||||
frontend_readonly=False,
|
||||
frontend_required=False
|
||||
)
|
||||
|
||||
# Register labels for Mandate
|
||||
register_model_labels(
|
||||
|
|
@ -50,20 +79,83 @@ register_model_labels(
|
|||
|
||||
class UserConnection(BaseModel, ModelMixin):
|
||||
"""Data model for a user's connection to an external service"""
|
||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the connection")
|
||||
userId: str = Field(description="ID of the user this connection belongs to")
|
||||
authority: AuthAuthority = Field(description="Authentication authority")
|
||||
externalId: str = Field(description="User ID in the external system")
|
||||
externalUsername: str = Field(description="Username in the external system")
|
||||
externalEmail: Optional[EmailStr] = Field(None, description="Email in the external system")
|
||||
status: ConnectionStatus = Field(default=ConnectionStatus.ACTIVE, description="Connection status")
|
||||
connectedAt: float = Field(default_factory=get_utc_timestamp, description="When the connection was established (UTC timestamp in seconds)")
|
||||
lastChecked: float = Field(default_factory=get_utc_timestamp, description="When the connection was last verified (UTC timestamp in seconds)")
|
||||
expiresAt: Optional[float] = Field(None, description="When the connection expires (UTC timestamp in seconds)")
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert the model to a dictionary"""
|
||||
return super().to_dict()
|
||||
id: str = Field(
|
||||
default_factory=lambda: str(uuid.uuid4()),
|
||||
description="Unique ID of the connection",
|
||||
frontend_type="text",
|
||||
frontend_readonly=True,
|
||||
frontend_required=False
|
||||
)
|
||||
userId: str = Field(
|
||||
description="ID of the user this connection belongs to",
|
||||
frontend_type="text",
|
||||
frontend_readonly=True,
|
||||
frontend_required=False
|
||||
)
|
||||
authority: AuthAuthority = Field(
|
||||
description="Authentication authority",
|
||||
frontend_type="select",
|
||||
frontend_readonly=True,
|
||||
frontend_required=False,
|
||||
frontend_options=[
|
||||
{"value": "local", "label": {"en": "Local", "fr": "Local"}},
|
||||
{"value": "google", "label": {"en": "Google", "fr": "Google"}},
|
||||
{"value": "msft", "label": {"en": "Microsoft", "fr": "Microsoft"}}
|
||||
]
|
||||
)
|
||||
externalId: str = Field(
|
||||
description="User ID in the external system",
|
||||
frontend_type="text",
|
||||
frontend_readonly=True,
|
||||
frontend_required=False
|
||||
)
|
||||
externalUsername: str = Field(
|
||||
description="Username in the external system",
|
||||
frontend_type="text",
|
||||
frontend_readonly=False,
|
||||
frontend_required=False
|
||||
)
|
||||
externalEmail: Optional[EmailStr] = Field(
|
||||
None,
|
||||
description="Email in the external system",
|
||||
frontend_type="email",
|
||||
frontend_readonly=False,
|
||||
frontend_required=False
|
||||
)
|
||||
status: ConnectionStatus = Field(
|
||||
default=ConnectionStatus.ACTIVE,
|
||||
description="Connection status",
|
||||
frontend_type="select",
|
||||
frontend_readonly=False,
|
||||
frontend_required=False,
|
||||
frontend_options=[
|
||||
{"value": "active", "label": {"en": "Active", "fr": "Actif"}},
|
||||
{"value": "inactive", "label": {"en": "Inactive", "fr": "Inactif"}},
|
||||
{"value": "expired", "label": {"en": "Expired", "fr": "Expiré"}},
|
||||
{"value": "pending", "label": {"en": "Pending", "fr": "En attente"}}
|
||||
]
|
||||
)
|
||||
connectedAt: float = Field(
|
||||
default_factory=get_utc_timestamp,
|
||||
description="When the connection was established (UTC timestamp in seconds)",
|
||||
frontend_type="timestamp",
|
||||
frontend_readonly=True,
|
||||
frontend_required=False
|
||||
)
|
||||
lastChecked: float = Field(
|
||||
default_factory=get_utc_timestamp,
|
||||
description="When the connection was last verified (UTC timestamp in seconds)",
|
||||
frontend_type="timestamp",
|
||||
frontend_readonly=True,
|
||||
frontend_required=False
|
||||
)
|
||||
expiresAt: Optional[float] = Field(
|
||||
None,
|
||||
description="When the connection expires (UTC timestamp in seconds)",
|
||||
frontend_type="timestamp",
|
||||
frontend_readonly=True,
|
||||
frontend_required=False
|
||||
)
|
||||
|
||||
# Register labels for UserConnection
|
||||
register_model_labels(
|
||||
|
|
@ -135,15 +227,84 @@ register_model_labels(
|
|||
|
||||
class User(BaseModel, ModelMixin):
|
||||
"""Data model for a user"""
|
||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the user")
|
||||
username: str = Field(description="Username for login")
|
||||
email: Optional[EmailStr] = Field(None, description="Email address of the user")
|
||||
fullName: Optional[str] = Field(None, description="Full name of the user")
|
||||
language: str = Field(default="en", description="Preferred language of the user")
|
||||
enabled: bool = Field(default=True, description="Indicates whether the user is enabled")
|
||||
privilege: UserPrivilege = Field(default=UserPrivilege.USER, description="Permission level")
|
||||
authenticationAuthority: AuthAuthority = Field(default=AuthAuthority.LOCAL, description="Primary authentication authority")
|
||||
mandateId: Optional[str] = Field(None, description="ID of the mandate this user belongs to")
|
||||
id: str = Field(
|
||||
default_factory=lambda: str(uuid.uuid4()),
|
||||
description="Unique ID of the user",
|
||||
frontend_type="text",
|
||||
frontend_readonly=True,
|
||||
frontend_required=False
|
||||
)
|
||||
username: str = Field(
|
||||
description="Username for login",
|
||||
frontend_type="text",
|
||||
frontend_readonly=False,
|
||||
frontend_required=True
|
||||
)
|
||||
email: Optional[EmailStr] = Field(
|
||||
None,
|
||||
description="Email address of the user",
|
||||
frontend_type="email",
|
||||
frontend_readonly=False,
|
||||
frontend_required=True
|
||||
)
|
||||
fullName: Optional[str] = Field(
|
||||
None,
|
||||
description="Full name of the user",
|
||||
frontend_type="text",
|
||||
frontend_readonly=False,
|
||||
frontend_required=False
|
||||
)
|
||||
language: str = Field(
|
||||
default="en",
|
||||
description="Preferred language of the user",
|
||||
frontend_type="select",
|
||||
frontend_readonly=False,
|
||||
frontend_required=True,
|
||||
frontend_options=[
|
||||
{"value": "de", "label": {"en": "Deutsch", "fr": "Allemand"}},
|
||||
{"value": "en", "label": {"en": "English", "fr": "Anglais"}},
|
||||
{"value": "fr", "label": {"en": "Français", "fr": "Français"}},
|
||||
{"value": "it", "label": {"en": "Italiano", "fr": "Italien"}}
|
||||
]
|
||||
)
|
||||
enabled: bool = Field(
|
||||
default=True,
|
||||
description="Indicates whether the user is enabled",
|
||||
frontend_type="checkbox",
|
||||
frontend_readonly=False,
|
||||
frontend_required=False
|
||||
)
|
||||
privilege: UserPrivilege = Field(
|
||||
default=UserPrivilege.USER,
|
||||
description="Permission level",
|
||||
frontend_type="select",
|
||||
frontend_readonly=False,
|
||||
frontend_required=True,
|
||||
frontend_options=[
|
||||
{"value": "user", "label": {"en": "User", "fr": "Utilisateur"}},
|
||||
{"value": "admin", "label": {"en": "Admin", "fr": "Administrateur"}},
|
||||
{"value": "sysadmin", "label": {"en": "SysAdmin", "fr": "Administrateur système"}}
|
||||
]
|
||||
)
|
||||
authenticationAuthority: AuthAuthority = Field(
|
||||
default=AuthAuthority.LOCAL,
|
||||
description="Primary authentication authority",
|
||||
frontend_type="select",
|
||||
frontend_readonly=True,
|
||||
frontend_required=False,
|
||||
frontend_options=[
|
||||
{"value": "local", "label": {"en": "Local", "fr": "Local"}},
|
||||
{"value": "google", "label": {"en": "Google", "fr": "Google"}},
|
||||
{"value": "msft", "label": {"en": "Microsoft", "fr": "Microsoft"}}
|
||||
]
|
||||
)
|
||||
mandateId: Optional[str] = Field(
|
||||
None,
|
||||
description="ID of the mandate this user belongs to",
|
||||
frontend_type="text",
|
||||
frontend_readonly=True,
|
||||
frontend_required=False
|
||||
)
|
||||
|
||||
# Register labels for User
|
||||
register_model_labels(
|
||||
|
|
|
|||
|
|
@ -284,15 +284,6 @@ class AppObjects:
|
|||
result = []
|
||||
for conn_dict in connections:
|
||||
try:
|
||||
# Convert string dates to datetime objects
|
||||
for field in ['connectedAt', 'lastChecked', 'expiresAt']:
|
||||
if field in conn_dict and conn_dict[field]:
|
||||
try:
|
||||
if isinstance(conn_dict[field], str):
|
||||
conn_dict[field] = datetime.fromisoformat(conn_dict[field].replace('Z', '+00:00'))
|
||||
except (ValueError, TypeError):
|
||||
conn_dict[field] = None
|
||||
|
||||
# Create UserConnection object
|
||||
connection = UserConnection(
|
||||
id=conn_dict["id"],
|
||||
|
|
@ -831,8 +822,8 @@ class AppObjects:
|
|||
logger.warning(f"No token found for connection: {connectionId}")
|
||||
return None
|
||||
|
||||
# Sort by creation date and get the latest
|
||||
tokens.sort(key=lambda x: x.get("createdAt", ""), reverse=True)
|
||||
# Sort by expiration date and get the latest (most recent expiration)
|
||||
tokens.sort(key=lambda x: x.get("expiresAt", 0), reverse=True)
|
||||
latest_token = Token(**tokens[0])
|
||||
|
||||
# Check if token is expired
|
||||
|
|
@ -905,6 +896,35 @@ class AppObjects:
|
|||
logger.error(f"Error deleting token for connection {connectionId}: {str(e)}")
|
||||
raise
|
||||
|
||||
def cleanupExpiredTokens(self) -> int:
|
||||
"""Clean up expired tokens for all connections, returns count of cleaned tokens"""
|
||||
try:
|
||||
from modules.shared.timezoneUtils import get_utc_timestamp
|
||||
|
||||
current_time = get_utc_timestamp()
|
||||
cleaned_count = 0
|
||||
|
||||
# Get all tokens
|
||||
all_tokens = self.db.getRecordset("tokens", recordFilter={})
|
||||
|
||||
for token_data in all_tokens:
|
||||
if token_data.get("expiresAt") and token_data.get("expiresAt") < current_time:
|
||||
# Token is expired, delete it
|
||||
self.db.recordDelete("tokens", token_data["id"])
|
||||
cleaned_count += 1
|
||||
logger.debug(f"Cleaned up expired token {token_data['id']} for connection {token_data.get('connectionId')}")
|
||||
|
||||
# Clear cache to ensure fresh data
|
||||
if cleaned_count > 0:
|
||||
self._clearTableCache("tokens")
|
||||
logger.info(f"Cleaned up {cleaned_count} expired tokens")
|
||||
|
||||
return cleaned_count
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error cleaning up expired tokens: {str(e)}")
|
||||
return 0
|
||||
|
||||
def logout(self) -> None:
|
||||
"""Logout current user - clear user context and tokens"""
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -465,17 +465,86 @@ register_model_labels(
|
|||
|
||||
class ChatWorkflow(BaseModel, ModelMixin):
|
||||
"""Data model for a chat workflow"""
|
||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
|
||||
mandateId: str = Field(description="ID of the mandate this workflow belongs to")
|
||||
status: str = Field(description="Current status of the workflow")
|
||||
name: Optional[str] = Field(None, description="Name of the workflow")
|
||||
currentRound: int = Field(description="Current round number")
|
||||
lastActivity: float = Field(default_factory=get_utc_timestamp, description="Timestamp of last activity (UTC timestamp in seconds)")
|
||||
startedAt: float = Field(default_factory=get_utc_timestamp, description="When the workflow started (UTC timestamp in seconds)")
|
||||
logs: List[ChatLog] = Field(default_factory=list, description="Workflow logs")
|
||||
messages: List[ChatMessage] = Field(default_factory=list, description="Messages in the workflow")
|
||||
stats: Optional[ChatStat] = Field(None, description="Workflow statistics")
|
||||
tasks: List[TaskItem] = Field(default_factory=list, description="List of tasks in the workflow")
|
||||
id: str = Field(
|
||||
default_factory=lambda: str(uuid.uuid4()),
|
||||
description="Primary key",
|
||||
frontend_type="text",
|
||||
frontend_readonly=True,
|
||||
frontend_required=False
|
||||
)
|
||||
mandateId: str = Field(
|
||||
description="ID of the mandate this workflow belongs to",
|
||||
frontend_type="text",
|
||||
frontend_readonly=True,
|
||||
frontend_required=False
|
||||
)
|
||||
status: str = Field(
|
||||
description="Current status of the workflow",
|
||||
frontend_type="select",
|
||||
frontend_readonly=False,
|
||||
frontend_required=False,
|
||||
frontend_options=[
|
||||
{"value": "running", "label": {"en": "Running", "fr": "En cours"}},
|
||||
{"value": "completed", "label": {"en": "Completed", "fr": "Terminé"}},
|
||||
{"value": "stopped", "label": {"en": "Stopped", "fr": "Arrêté"}},
|
||||
{"value": "error", "label": {"en": "Error", "fr": "Erreur"}}
|
||||
]
|
||||
)
|
||||
name: Optional[str] = Field(
|
||||
None,
|
||||
description="Name of the workflow",
|
||||
frontend_type="text",
|
||||
frontend_readonly=False,
|
||||
frontend_required=True
|
||||
)
|
||||
currentRound: int = Field(
|
||||
description="Current round number",
|
||||
frontend_type="integer",
|
||||
frontend_readonly=True,
|
||||
frontend_required=False
|
||||
)
|
||||
lastActivity: float = Field(
|
||||
default_factory=get_utc_timestamp,
|
||||
description="Timestamp of last activity (UTC timestamp in seconds)",
|
||||
frontend_type="timestamp",
|
||||
frontend_readonly=True,
|
||||
frontend_required=False
|
||||
)
|
||||
startedAt: float = Field(
|
||||
default_factory=get_utc_timestamp,
|
||||
description="When the workflow started (UTC timestamp in seconds)",
|
||||
frontend_type="timestamp",
|
||||
frontend_readonly=True,
|
||||
frontend_required=False
|
||||
)
|
||||
logs: List[ChatLog] = Field(
|
||||
default_factory=list,
|
||||
description="Workflow logs",
|
||||
frontend_type="text",
|
||||
frontend_readonly=True,
|
||||
frontend_required=False
|
||||
)
|
||||
messages: List[ChatMessage] = Field(
|
||||
default_factory=list,
|
||||
description="Messages in the workflow",
|
||||
frontend_type="text",
|
||||
frontend_readonly=True,
|
||||
frontend_required=False
|
||||
)
|
||||
stats: Optional[ChatStat] = Field(
|
||||
None,
|
||||
description="Workflow statistics",
|
||||
frontend_type="text",
|
||||
frontend_readonly=True,
|
||||
frontend_required=False
|
||||
)
|
||||
tasks: List[TaskItem] = Field(
|
||||
default_factory=list,
|
||||
description="List of tasks in the workflow",
|
||||
frontend_type="text",
|
||||
frontend_readonly=True,
|
||||
frontend_required=False
|
||||
)
|
||||
|
||||
# Register labels for ChatWorkflow
|
||||
register_model_labels(
|
||||
|
|
|
|||
|
|
@ -1335,7 +1335,7 @@ class ChatObjects:
|
|||
retryCount=createdAction.get("retryCount", 0),
|
||||
retryMax=createdAction.get("retryMax", 3),
|
||||
processingTime=createdAction.get("processingTime"),
|
||||
timestamp=datetime.fromtimestamp(float(createdAction.get("timestamp", get_utc_timestamp()))),
|
||||
timestamp=float(createdAction.get("timestamp", get_utc_timestamp())),
|
||||
result=createdAction.get("result"),
|
||||
resultDocuments=createdAction.get("resultDocuments", [])
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,13 +16,50 @@ from modules.shared.timezoneUtils import get_utc_timestamp
|
|||
|
||||
class FileItem(BaseModel, ModelMixin):
|
||||
"""Data model for a file item"""
|
||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
|
||||
mandateId: str = Field(description="ID of the mandate this file belongs to")
|
||||
filename: str = Field(description="Name of the file")
|
||||
mimeType: str = Field(description="MIME type of the file")
|
||||
fileHash: str = Field(description="Hash of the file")
|
||||
fileSize: int = Field(description="Size of the file in bytes")
|
||||
creationDate: float = Field(default_factory=get_utc_timestamp, description="Date when the file was created (UTC timestamp in seconds)")
|
||||
id: str = Field(
|
||||
default_factory=lambda: str(uuid.uuid4()),
|
||||
description="Primary key",
|
||||
frontend_type="text",
|
||||
frontend_readonly=True,
|
||||
frontend_required=False
|
||||
)
|
||||
mandateId: str = Field(
|
||||
description="ID of the mandate this file belongs to",
|
||||
frontend_type="text",
|
||||
frontend_readonly=True,
|
||||
frontend_required=False
|
||||
)
|
||||
filename: str = Field(
|
||||
description="Name of the file",
|
||||
frontend_type="text",
|
||||
frontend_readonly=False,
|
||||
frontend_required=True
|
||||
)
|
||||
mimeType: str = Field(
|
||||
description="MIME type of the file",
|
||||
frontend_type="text",
|
||||
frontend_readonly=True,
|
||||
frontend_required=False
|
||||
)
|
||||
fileHash: str = Field(
|
||||
description="Hash of the file",
|
||||
frontend_type="text",
|
||||
frontend_readonly=True,
|
||||
frontend_required=False
|
||||
)
|
||||
fileSize: int = Field(
|
||||
description="Size of the file in bytes",
|
||||
frontend_type="integer",
|
||||
frontend_readonly=True,
|
||||
frontend_required=False
|
||||
)
|
||||
creationDate: float = Field(
|
||||
default_factory=get_utc_timestamp,
|
||||
description="Date when the file was created (UTC timestamp in seconds)",
|
||||
frontend_type="timestamp",
|
||||
frontend_readonly=True,
|
||||
frontend_required=False
|
||||
)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert model to dictionary"""
|
||||
|
|
@ -94,10 +131,31 @@ register_model_labels(
|
|||
|
||||
class Prompt(BaseModel, ModelMixin):
|
||||
"""Data model for a prompt"""
|
||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
|
||||
mandateId: str = Field(description="ID of the mandate this prompt belongs to")
|
||||
content: str = Field(description="Content of the prompt")
|
||||
name: str = Field(description="Name of the prompt")
|
||||
id: str = Field(
|
||||
default_factory=lambda: str(uuid.uuid4()),
|
||||
description="Primary key",
|
||||
frontend_type="text",
|
||||
frontend_readonly=True,
|
||||
frontend_required=False
|
||||
)
|
||||
mandateId: str = Field(
|
||||
description="ID of the mandate this prompt belongs to",
|
||||
frontend_type="text",
|
||||
frontend_readonly=True,
|
||||
frontend_required=False
|
||||
)
|
||||
content: str = Field(
|
||||
description="Content of the prompt",
|
||||
frontend_type="textarea",
|
||||
frontend_readonly=False,
|
||||
frontend_required=True
|
||||
)
|
||||
name: str = Field(
|
||||
description="Name of the prompt",
|
||||
frontend_type="text",
|
||||
frontend_readonly=False,
|
||||
frontend_required=True
|
||||
)
|
||||
|
||||
# Register labels for Prompt
|
||||
register_model_labels(
|
||||
|
|
|
|||
|
|
@ -512,7 +512,7 @@ async def refresh_token(
|
|||
appInterface.deleteTokenByConnectionId(google_connection.id)
|
||||
|
||||
# Update the connection's expiration time
|
||||
google_connection.expiresAt = datetime.fromtimestamp(refreshed_token.expiresAt)
|
||||
google_connection.expiresAt = float(refreshed_token.expiresAt)
|
||||
google_connection.lastChecked = get_utc_timestamp()
|
||||
google_connection.status = ConnectionStatus.ACTIVE
|
||||
|
||||
|
|
|
|||
|
|
@ -443,6 +443,31 @@ async def logout(
|
|||
detail=f"Failed to logout: {str(e)}"
|
||||
)
|
||||
|
||||
@router.post("/cleanup")
|
||||
@limiter.limit("5/minute")
|
||||
async def cleanup_expired_tokens(
|
||||
request: Request,
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> Dict[str, Any]:
|
||||
"""Clean up expired tokens for the current user"""
|
||||
try:
|
||||
appInterface = getInterface(currentUser)
|
||||
|
||||
# Clean up expired tokens
|
||||
cleaned_count = appInterface.cleanupExpiredTokens()
|
||||
|
||||
return {
|
||||
"message": f"Cleanup completed successfully",
|
||||
"tokens_cleaned": cleaned_count
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error cleaning up expired tokens: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to cleanup expired tokens: {str(e)}"
|
||||
)
|
||||
|
||||
@router.post("/refresh")
|
||||
@limiter.limit("10/minute")
|
||||
async def refresh_token(
|
||||
|
|
@ -473,7 +498,8 @@ async def refresh_token(
|
|||
logger.debug(f"Found Microsoft connection: {msft_connection.id}, status={msft_connection.status}")
|
||||
|
||||
# Get the token for this specific connection using the new method
|
||||
current_token = appInterface.getTokenForConnection(msft_connection.id, auto_refresh=False)
|
||||
# Enable auto-refresh to handle expired tokens gracefully
|
||||
current_token = appInterface.getTokenForConnection(msft_connection.id, auto_refresh=True)
|
||||
|
||||
if not current_token:
|
||||
raise HTTPException(
|
||||
|
|
@ -494,7 +520,7 @@ async def refresh_token(
|
|||
appInterface.deleteTokenByConnectionId(msft_connection.id)
|
||||
|
||||
# Update the connection's expiration time
|
||||
msft_connection.expiresAt = datetime.fromtimestamp(refreshed_token.expiresAt)
|
||||
msft_connection.expiresAt = float(refreshed_token.expiresAt)
|
||||
msft_connection.lastChecked = get_utc_timestamp()
|
||||
msft_connection.status = ConnectionStatus.ACTIVE
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class ModelMixin:
|
|||
"""
|
||||
Convert a Pydantic model to a dictionary.
|
||||
Handles both Pydantic v1 and v2.
|
||||
Properly serializes datetime fields to ISO format strings.
|
||||
All timestamp fields remain as float values.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Dictionary representation of the model
|
||||
|
|
@ -27,42 +27,10 @@ class ModelMixin:
|
|||
else:
|
||||
data: Dict[str, Any] = self.dict() # Pydantic v1
|
||||
|
||||
# Convert datetime fields to ISO format strings
|
||||
for key, value in data.items():
|
||||
if isinstance(value, datetime):
|
||||
data[key] = value.isoformat()
|
||||
elif isinstance(value, (int, float)) and self._is_timestamp_field(key):
|
||||
# Handle timestamp fields based on field metadata
|
||||
try:
|
||||
data[key] = datetime.fromtimestamp(value).isoformat()
|
||||
except (ValueError, TypeError):
|
||||
# If conversion fails, keep the original value
|
||||
pass
|
||||
|
||||
return data
|
||||
|
||||
def _is_timestamp_field(self, field_name: str) -> bool:
|
||||
"""
|
||||
Check if a field is a timestamp field based on field metadata.
|
||||
Looks for 'UTC timestamp' in the field description.
|
||||
"""
|
||||
try:
|
||||
# Get field info from Pydantic model
|
||||
if hasattr(self, 'model_fields'):
|
||||
# Pydantic v2
|
||||
field_info = self.model_fields.get(field_name)
|
||||
if field_info and field_info.description:
|
||||
return 'UTC timestamp' in field_info.description
|
||||
elif hasattr(self, '__fields__'):
|
||||
# Pydantic v1
|
||||
field_info = self.__fields__.get(field_name)
|
||||
if field_info and field_info.field_info and field_info.field_info.description:
|
||||
return 'UTC timestamp' in field_info.field_info.description
|
||||
except Exception:
|
||||
pass
|
||||
# All fields (including timestamps) remain in their original format
|
||||
# No conversions needed - timestamps are already float
|
||||
|
||||
# Fallback: return False for safety
|
||||
return False
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'ModelMixin':
|
||||
|
|
@ -89,6 +57,12 @@ class AttributeDefinition(BaseModel, ModelMixin):
|
|||
options: Optional[List[Any]] = None
|
||||
validation: Optional[Dict[str, Any]] = None
|
||||
ui: Optional[Dict[str, Any]] = None
|
||||
# New frontend metadata fields
|
||||
readonly: bool = False
|
||||
editable: bool = True
|
||||
visible: bool = True
|
||||
order: int = 0
|
||||
placeholder: Optional[str] = None
|
||||
|
||||
# Global registry for model labels
|
||||
MODEL_LABELS: Dict[str, Dict[str, Dict[str, str]]] = {}
|
||||
|
|
@ -194,30 +168,92 @@ def getModelAttributeDefinitions(modelClass: Type[BaseModel] = None, userLanguag
|
|||
if hasattr(modelClass, 'model_fields'): # Pydantic v2
|
||||
fields = modelClass.model_fields
|
||||
for name, field in fields.items():
|
||||
# Extract frontend metadata from field info
|
||||
field_info = field.field_info if hasattr(field, 'field_info') else None
|
||||
# Check both direct attributes and extra field for frontend metadata
|
||||
frontend_type = None
|
||||
frontend_readonly = False
|
||||
frontend_required = field.is_required()
|
||||
frontend_options = None
|
||||
|
||||
if field_info:
|
||||
# Try direct attributes first
|
||||
frontend_type = getattr(field_info, 'frontend_type', None)
|
||||
frontend_readonly = getattr(field_info, 'frontend_readonly', False)
|
||||
frontend_required = getattr(field_info, 'frontend_required', frontend_required)
|
||||
frontend_options = getattr(field_info, 'frontend_options', None)
|
||||
|
||||
# If not found, check extra field
|
||||
if hasattr(field_info, 'extra') and field_info.extra:
|
||||
if frontend_type is None:
|
||||
frontend_type = field_info.extra.get('frontend_type')
|
||||
if not frontend_readonly:
|
||||
frontend_readonly = field_info.extra.get('frontend_readonly', False)
|
||||
if frontend_required == field.is_required(): # Only override if we didn't get it from direct attribute
|
||||
frontend_required = field_info.extra.get('frontend_required', frontend_required)
|
||||
if frontend_options is None:
|
||||
frontend_options = field_info.extra.get('frontend_options')
|
||||
|
||||
# Use frontend type if available, otherwise fall back to Python type
|
||||
field_type = frontend_type if frontend_type else (field.annotation.__name__ if hasattr(field.annotation, "__name__") else str(field.annotation))
|
||||
|
||||
attributes.append({
|
||||
"name": name,
|
||||
"type": field.annotation.__name__ if hasattr(field.annotation, "__name__") else str(field.annotation),
|
||||
"required": field.is_required() if hasattr(field, "is_required") else True,
|
||||
"type": field_type,
|
||||
"required": frontend_required,
|
||||
"description": field.description if hasattr(field, "description") else "",
|
||||
"label": labels.get(name, name),
|
||||
"placeholder": f"Please enter {labels.get(name, name)}",
|
||||
"editable": True,
|
||||
"editable": not frontend_readonly,
|
||||
"visible": True,
|
||||
"order": len(attributes)
|
||||
"order": len(attributes),
|
||||
"readonly": frontend_readonly,
|
||||
"options": frontend_options
|
||||
})
|
||||
else: # Pydantic v1
|
||||
fields = modelClass.__fields__
|
||||
for name, field in fields.items():
|
||||
# Extract frontend metadata from field info
|
||||
field_info = field.field_info if hasattr(field, 'field_info') else None
|
||||
# Check both direct attributes and extra field for frontend metadata
|
||||
frontend_type = None
|
||||
frontend_readonly = False
|
||||
frontend_required = field.required
|
||||
frontend_options = None
|
||||
|
||||
if field_info:
|
||||
# Try direct attributes first
|
||||
frontend_type = getattr(field_info, 'frontend_type', None)
|
||||
frontend_readonly = getattr(field_info, 'frontend_readonly', False)
|
||||
frontend_required = getattr(field_info, 'frontend_required', frontend_required)
|
||||
frontend_options = getattr(field_info, 'frontend_options', None)
|
||||
|
||||
# If not found, check extra field
|
||||
if hasattr(field_info, 'extra') and field_info.extra:
|
||||
if frontend_type is None:
|
||||
frontend_type = field_info.extra.get('frontend_type')
|
||||
if not frontend_readonly:
|
||||
frontend_readonly = field_info.extra.get('frontend_readonly', False)
|
||||
if frontend_required == field.required: # Only override if we didn't get it from direct attribute
|
||||
frontend_required = field_info.extra.get('frontend_required', frontend_required)
|
||||
if frontend_options is None:
|
||||
frontend_options = field_info.extra.get('frontend_options')
|
||||
|
||||
# Use frontend type if available, otherwise fall back to Python type
|
||||
field_type = frontend_type if frontend_type else (field.type_.__name__ if hasattr(field.type_, "__name__") else str(field.type_))
|
||||
|
||||
attributes.append({
|
||||
"name": name,
|
||||
"type": field.type_.__name__ if hasattr(field.type_, "__name__") else str(field.type_),
|
||||
"required": field.required,
|
||||
"type": field_type,
|
||||
"required": frontend_required,
|
||||
"description": field.field_info.description if hasattr(field.field_info, "description") else "",
|
||||
"label": labels.get(name, name),
|
||||
"placeholder": f"Please enter {labels.get(name, name)}",
|
||||
"editable": True,
|
||||
"editable": not frontend_readonly,
|
||||
"visible": True,
|
||||
"order": len(attributes)
|
||||
"order": len(attributes),
|
||||
"readonly": frontend_readonly,
|
||||
"options": frontend_options
|
||||
})
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ git remote set-url origin https://valueon@github.com/valueonag/gateway
|
|||
git remote set-url origin https://valueon@github.com/valueonag/frontend_agents
|
||||
git remote set-url origin https://valueon@github.com/valueonag/wiki
|
||||
git remote set-url origin https://valueon@github.com/valueonag/customer-svbe
|
||||
git remote set-url origin https://valueon@github.com/valueonag/customer-althaus
|
||||
|
||||
### git delete workflow runs (cleanup)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue