solace-agent-mesh 1.4.7__py3-none-any.whl → 1.4.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of solace-agent-mesh might be problematic. Click here for more details.
- solace_agent_mesh/agent/sac/app.py +3 -3
- solace_agent_mesh/agent/sac/component.py +2 -3
- solace_agent_mesh/agent/utils/artifact_helpers.py +23 -7
- solace_agent_mesh/assets/docs/404.html +3 -3
- solace_agent_mesh/assets/docs/assets/js/{0e682baa.da822665.js → 0e682baa.d054e1d8.js} +1 -1
- solace_agent_mesh/assets/docs/assets/js/166ab619.e27886d9.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/453a82a6.3c6bb61d.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/{75384d09.1e7d7cb7.js → 75384d09.c19e8b51.js} +1 -1
- solace_agent_mesh/assets/docs/assets/js/a3a92b25.af35e313.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/{bac0be12.bf0181cf.js → bac0be12.17de4316.js} +1 -1
- solace_agent_mesh/assets/docs/assets/js/d6a81ee7.829198f1.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/f284c35a.ed8dd236.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/{main.11f9f9f3.js → main.51e33228.js} +2 -2
- solace_agent_mesh/assets/docs/assets/js/runtime~main.0d2ff2b6.js +1 -0
- solace_agent_mesh/assets/docs/docs/documentation/Enterprise/installation/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/Enterprise/rbac-setup-guilde/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/Enterprise/single-sign-on/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/Migrations/A2A Upgrade To 0.3.0/a2a-gateway-upgrade-to-0.3.0/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/Migrations/A2A Upgrade To 0.3.0/a2a-technical-migration-map/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/concepts/agents/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/concepts/architecture/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/concepts/cli/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/concepts/gateways/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/concepts/orchestrator/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/concepts/plugins/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/deployment/debugging/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/deployment/deploy/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/deployment/observability/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/component-overview/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/configurations/index.html +11 -12
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/configurations/litellm_models/index.html +49 -0
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/installation/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/quick-start/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/bedrock-agents/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/custom-agent/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/event-mesh-gateway/index.html +5 -5
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/mcp-integration/index.html +5 -5
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/mongodb-integration/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/rag-integration/index.html +6 -6
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/rest-gateway/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/slack-integration/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/sql-database/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/artifact-management/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/audio-tools/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/data-analysis-tools/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/embeds/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-agents/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-gateways/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/creating-python-tools/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/creating-service-providers/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/solace-ai-connector/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/structure/index.html +4 -4
- solace_agent_mesh/assets/docs/lunr-index-1759254374040.json +1 -0
- solace_agent_mesh/assets/docs/lunr-index.json +1 -1
- solace_agent_mesh/assets/docs/search-doc-1759254374040.json +1 -0
- solace_agent_mesh/assets/docs/search-doc.json +1 -1
- solace_agent_mesh/assets/docs/sitemap.xml +1 -1
- solace_agent_mesh/cli/__init__.py +1 -1
- solace_agent_mesh/cli/commands/add_cmd/agent_cmd.py +6 -3
- solace_agent_mesh/cli/commands/init_cmd/__init__.py +3 -3
- solace_agent_mesh/cli/commands/init_cmd/broker_step.py +1 -1
- solace_agent_mesh/cli/commands/init_cmd/env_step.py +1 -1
- solace_agent_mesh/cli/commands/init_cmd/init_cmd_llm.txt +4 -4
- solace_agent_mesh/cli/commands/init_cmd/orchestrator_step.py +12 -1
- solace_agent_mesh/cli/commands/plugin_cmd/install_cmd.py +5 -5
- solace_agent_mesh/client/webui/frontend/static/assets/main-B0PHV3hm.js +339 -0
- solace_agent_mesh/client/webui/frontend/static/index.html +1 -1
- solace_agent_mesh/common/constants.py +2 -0
- solace_agent_mesh/config_portal/backend/common.py +1 -1
- solace_agent_mesh/config_portal/frontend/static/client/assets/{_index-bFMKlzKf.js → _index-BNuqpWDc.js} +1 -1
- solace_agent_mesh/config_portal/frontend/static/client/assets/{manifest-89db7c30.js → manifest-44d62be6.js} +1 -1
- solace_agent_mesh/config_portal/frontend/static/client/index.html +1 -1
- solace_agent_mesh/gateway/http_sse/component.py +21 -15
- solace_agent_mesh/gateway/http_sse/dependencies.py +84 -88
- solace_agent_mesh/gateway/http_sse/main.py +8 -2
- solace_agent_mesh/gateway/http_sse/repository/entities/message.py +3 -1
- solace_agent_mesh/gateway/http_sse/repository/entities/session.py +3 -1
- solace_agent_mesh/gateway/http_sse/repository/interfaces.py +5 -0
- solace_agent_mesh/gateway/http_sse/repository/message_repository.py +25 -23
- solace_agent_mesh/gateway/http_sse/repository/models/__init__.py +12 -4
- solace_agent_mesh/gateway/http_sse/repository/models/message_model.py +19 -1
- solace_agent_mesh/gateway/http_sse/repository/models/session_model.py +19 -1
- solace_agent_mesh/gateway/http_sse/repository/session_repository.py +46 -42
- solace_agent_mesh/gateway/http_sse/routers/artifacts.py +199 -59
- solace_agent_mesh/gateway/http_sse/routers/dto/requests/__init__.py +1 -6
- solace_agent_mesh/gateway/http_sse/routers/dto/requests/session_requests.py +3 -17
- solace_agent_mesh/gateway/http_sse/routers/people.py +4 -37
- solace_agent_mesh/gateway/http_sse/routers/sessions.py +33 -68
- solace_agent_mesh/gateway/http_sse/routers/tasks.py +54 -28
- solace_agent_mesh/gateway/http_sse/services/session_service.py +60 -28
- solace_agent_mesh/gateway/http_sse/shared/__init__.py +122 -1
- solace_agent_mesh/gateway/http_sse/shared/base_repository.py +278 -0
- solace_agent_mesh/gateway/http_sse/shared/database_exceptions.py +274 -0
- solace_agent_mesh/gateway/http_sse/shared/database_helpers.py +43 -0
- solace_agent_mesh/gateway/http_sse/shared/error_dto.py +107 -0
- solace_agent_mesh/gateway/http_sse/shared/exception_handlers.py +192 -0
- solace_agent_mesh/gateway/http_sse/shared/exceptions.py +192 -0
- solace_agent_mesh/gateway/http_sse/shared/pagination.py +138 -0
- solace_agent_mesh/gateway/http_sse/shared/response_utils.py +134 -0
- solace_agent_mesh/gateway/http_sse/shared/utils.py +22 -0
- solace_agent_mesh/templates/plugin_agent_config_template.yaml +1 -1
- solace_agent_mesh/templates/plugin_custom_config_template.yaml +1 -1
- solace_agent_mesh/templates/plugin_gateway_config_template.yaml +1 -1
- solace_agent_mesh/templates/shared_config.yaml +1 -1
- {solace_agent_mesh-1.4.7.dist-info → solace_agent_mesh-1.4.9.dist-info}/METADATA +34 -35
- {solace_agent_mesh-1.4.7.dist-info → solace_agent_mesh-1.4.9.dist-info}/RECORD +112 -101
- solace_agent_mesh/assets/docs/assets/js/166ab619.bdddc63a.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/a3a92b25.6def8980.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/beecea0d.ce915979.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/f284c35a.525933db.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/runtime~main.5922bcf0.js +0 -1
- solace_agent_mesh/assets/docs/lunr-index-1759151175744.json +0 -1
- solace_agent_mesh/assets/docs/search-doc-1759151175744.json +0 -1
- solace_agent_mesh/client/webui/frontend/static/assets/main-BKIoiLSu.js +0 -339
- /solace_agent_mesh/assets/docs/assets/js/{main.11f9f9f3.js.LICENSE.txt → main.51e33228.js.LICENSE.txt} +0 -0
- {solace_agent_mesh-1.4.7.dist-info → solace_agent_mesh-1.4.9.dist-info}/WHEEL +0 -0
- {solace_agent_mesh-1.4.7.dist-info → solace_agent_mesh-1.4.9.dist-info}/entry_points.txt +0 -0
- {solace_agent_mesh-1.4.7.dist-info → solace_agent_mesh-1.4.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Shared Utilities and Constants
|
|
3
3
|
|
|
4
|
-
Contains common utilities, constants, enums,
|
|
4
|
+
Contains common utilities, constants, enums, types, and exception handling
|
|
5
|
+
used across layers.
|
|
5
6
|
"""
|
|
6
7
|
|
|
7
8
|
from .auth_utils import get_current_user
|
|
@@ -14,7 +15,71 @@ from .timestamp_utils import (
|
|
|
14
15
|
validate_epoch_ms,
|
|
15
16
|
)
|
|
16
17
|
|
|
18
|
+
# Exception handling exports
|
|
19
|
+
from .exceptions import (
|
|
20
|
+
WebUIBackendException,
|
|
21
|
+
ValidationError,
|
|
22
|
+
EntityNotFoundError,
|
|
23
|
+
EntityAlreadyExistsError,
|
|
24
|
+
BusinessRuleViolationError,
|
|
25
|
+
ConfigurationError,
|
|
26
|
+
DataIntegrityError,
|
|
27
|
+
ExternalServiceError,
|
|
28
|
+
EntityOperation,
|
|
29
|
+
ValidationErrorBuilder,
|
|
30
|
+
)
|
|
31
|
+
from .error_dto import EventErrorDTO
|
|
32
|
+
from .exception_handlers import (
|
|
33
|
+
register_exception_handlers,
|
|
34
|
+
create_error_response,
|
|
35
|
+
validation_error_handler,
|
|
36
|
+
entity_not_found_handler,
|
|
37
|
+
entity_already_exists_handler,
|
|
38
|
+
business_rule_violation_handler,
|
|
39
|
+
configuration_error_handler,
|
|
40
|
+
data_integrity_error_handler,
|
|
41
|
+
external_service_error_handler,
|
|
42
|
+
webui_backend_exception_handler,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Repository base classes
|
|
46
|
+
from .base_repository import (
|
|
47
|
+
BaseRepository,
|
|
48
|
+
PaginatedRepository,
|
|
49
|
+
ValidationMixin,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Pagination utilities
|
|
53
|
+
from .pagination import (
|
|
54
|
+
PaginationParams,
|
|
55
|
+
PaginatedResponse,
|
|
56
|
+
DataResponse,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Database utilities
|
|
60
|
+
from .database_exceptions import (
|
|
61
|
+
DatabaseExceptionHandler,
|
|
62
|
+
DatabaseErrorDecorator,
|
|
63
|
+
handle_database_errors,
|
|
64
|
+
)
|
|
65
|
+
# Database helpers (SimpleJSON type only - transaction management removed)
|
|
66
|
+
from .database_helpers import SimpleJSON
|
|
67
|
+
|
|
68
|
+
# Generic utilities
|
|
69
|
+
from .utils import generate_uuid, to_snake_case, to_pascal_case
|
|
70
|
+
|
|
71
|
+
# Response utilities
|
|
72
|
+
from .response_utils import (
|
|
73
|
+
create_data_response,
|
|
74
|
+
create_paginated_response,
|
|
75
|
+
create_empty_data_response,
|
|
76
|
+
create_success_response,
|
|
77
|
+
create_list_response,
|
|
78
|
+
StandardResponseMixin,
|
|
79
|
+
)
|
|
80
|
+
|
|
17
81
|
__all__ = [
|
|
82
|
+
# Utilities
|
|
18
83
|
"get_current_user",
|
|
19
84
|
"now_epoch_ms",
|
|
20
85
|
"epoch_ms_to_iso8601",
|
|
@@ -22,4 +87,60 @@ __all__ = [
|
|
|
22
87
|
"datetime_to_epoch_ms",
|
|
23
88
|
"epoch_ms_to_datetime",
|
|
24
89
|
"validate_epoch_ms",
|
|
90
|
+
|
|
91
|
+
# Exception classes
|
|
92
|
+
"WebUIBackendException",
|
|
93
|
+
"ValidationError",
|
|
94
|
+
"EntityNotFoundError",
|
|
95
|
+
"EntityAlreadyExistsError",
|
|
96
|
+
"BusinessRuleViolationError",
|
|
97
|
+
"ConfigurationError",
|
|
98
|
+
"DataIntegrityError",
|
|
99
|
+
"ExternalServiceError",
|
|
100
|
+
"EntityOperation",
|
|
101
|
+
"ValidationErrorBuilder",
|
|
102
|
+
|
|
103
|
+
# Error response DTO
|
|
104
|
+
"EventErrorDTO",
|
|
105
|
+
|
|
106
|
+
# Exception handlers
|
|
107
|
+
"register_exception_handlers",
|
|
108
|
+
"create_error_response",
|
|
109
|
+
"validation_error_handler",
|
|
110
|
+
"entity_not_found_handler",
|
|
111
|
+
"entity_already_exists_handler",
|
|
112
|
+
"business_rule_violation_handler",
|
|
113
|
+
"configuration_error_handler",
|
|
114
|
+
"data_integrity_error_handler",
|
|
115
|
+
"external_service_error_handler",
|
|
116
|
+
"webui_backend_exception_handler",
|
|
117
|
+
|
|
118
|
+
# Repository base classes
|
|
119
|
+
"BaseRepository",
|
|
120
|
+
"PaginatedRepository",
|
|
121
|
+
"ValidationMixin",
|
|
122
|
+
|
|
123
|
+
# Pagination utilities
|
|
124
|
+
"PaginationParams",
|
|
125
|
+
"PaginatedResponse",
|
|
126
|
+
"DataResponse",
|
|
127
|
+
|
|
128
|
+
# Database utilities
|
|
129
|
+
"DatabaseExceptionHandler",
|
|
130
|
+
"DatabaseErrorDecorator",
|
|
131
|
+
"handle_database_errors",
|
|
132
|
+
# Database transaction functions removed - use FastAPI dependency injection
|
|
133
|
+
|
|
134
|
+
# Generic utilities
|
|
135
|
+
"generate_uuid",
|
|
136
|
+
"to_snake_case",
|
|
137
|
+
"to_pascal_case",
|
|
138
|
+
|
|
139
|
+
# Response utilities
|
|
140
|
+
"create_data_response",
|
|
141
|
+
"create_paginated_response",
|
|
142
|
+
"create_empty_data_response",
|
|
143
|
+
"create_success_response",
|
|
144
|
+
"create_list_response",
|
|
145
|
+
"StandardResponseMixin",
|
|
25
146
|
]
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base repository classes with proper transaction management.
|
|
3
|
+
|
|
4
|
+
This module provides base classes for repositories that follow FastAPI best practices
|
|
5
|
+
for database session management and transaction handling.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from typing import Dict, Any, List, Optional, TypeVar, Generic, Type
|
|
10
|
+
from sqlalchemy.orm import Session
|
|
11
|
+
from sqlalchemy.exc import NoResultFound
|
|
12
|
+
from .exceptions import EntityNotFoundError, ValidationError
|
|
13
|
+
from .types import PaginationInfo
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
T = TypeVar('T')
|
|
17
|
+
ModelType = TypeVar('ModelType')
|
|
18
|
+
EntityType = TypeVar('EntityType')
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BaseRepository(ABC, Generic[ModelType, EntityType]):
|
|
22
|
+
"""
|
|
23
|
+
Abstract base class for repositories with common database operations.
|
|
24
|
+
|
|
25
|
+
This base class provides common patterns for database operations
|
|
26
|
+
without manual transaction management, following the principle that
|
|
27
|
+
transactions should be handled at the service/API layer.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, model_class: Type[ModelType], entity_class: Type[EntityType]):
|
|
31
|
+
"""
|
|
32
|
+
Initialize repository with model and entity classes.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
model_class: SQLAlchemy model class
|
|
36
|
+
entity_class: Pydantic entity class
|
|
37
|
+
"""
|
|
38
|
+
self.model_class = model_class
|
|
39
|
+
self.entity_class = entity_class
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def entity_name(self) -> str:
|
|
44
|
+
"""Return the entity name for error messages."""
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
def create(self, session: Session, create_data: Dict[str, Any]) -> EntityType:
|
|
48
|
+
"""
|
|
49
|
+
Create a new entity.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
session: Database session (managed externally)
|
|
53
|
+
create_data: Data for creating the entity
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Created entity
|
|
57
|
+
|
|
58
|
+
Note:
|
|
59
|
+
This method does NOT commit the transaction.
|
|
60
|
+
Commit/rollback is handled by the service layer.
|
|
61
|
+
"""
|
|
62
|
+
model_instance = self.model_class(**create_data)
|
|
63
|
+
|
|
64
|
+
session.add(model_instance)
|
|
65
|
+
session.flush() # Flush to get generated IDs
|
|
66
|
+
session.refresh(model_instance)
|
|
67
|
+
|
|
68
|
+
entity = self.entity_class.model_validate(model_instance)
|
|
69
|
+
|
|
70
|
+
return entity
|
|
71
|
+
|
|
72
|
+
def get_by_id(self, session: Session, entity_id: Any) -> EntityType:
|
|
73
|
+
"""
|
|
74
|
+
Get entity by ID.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
session: Database session
|
|
78
|
+
entity_id: Entity identifier
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Entity instance
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
EntityNotFoundError: If entity not found
|
|
85
|
+
"""
|
|
86
|
+
model_instance = session.query(self.model_class).filter(
|
|
87
|
+
self.model_class.id == str(entity_id)
|
|
88
|
+
).first()
|
|
89
|
+
|
|
90
|
+
if not model_instance:
|
|
91
|
+
raise EntityNotFoundError(self.entity_name, entity_id)
|
|
92
|
+
|
|
93
|
+
return self.entity_class.model_validate(model_instance)
|
|
94
|
+
|
|
95
|
+
def get_all(self, session: Session, limit: Optional[int] = None, offset: Optional[int] = None) -> List[EntityType]:
|
|
96
|
+
"""
|
|
97
|
+
Get all entities with optional pagination.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
session: Database session
|
|
101
|
+
limit: Maximum number of results
|
|
102
|
+
offset: Number of results to skip
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
List of entities
|
|
106
|
+
"""
|
|
107
|
+
query = session.query(self.model_class)
|
|
108
|
+
|
|
109
|
+
if offset:
|
|
110
|
+
query = query.offset(offset)
|
|
111
|
+
if limit:
|
|
112
|
+
query = query.limit(limit)
|
|
113
|
+
|
|
114
|
+
model_instances = query.all()
|
|
115
|
+
return [self.entity_class.model_validate(instance) for instance in model_instances]
|
|
116
|
+
|
|
117
|
+
def update(self, session: Session, entity_id: Any, update_data: Dict[str, Any]) -> EntityType:
|
|
118
|
+
"""
|
|
119
|
+
Update an entity.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
session: Database session
|
|
123
|
+
entity_id: Entity identifier
|
|
124
|
+
update_data: Data to update
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Updated entity
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
EntityNotFoundError: If entity not found
|
|
131
|
+
"""
|
|
132
|
+
model_instance = session.query(self.model_class).filter(
|
|
133
|
+
self.model_class.id == str(entity_id)
|
|
134
|
+
).first()
|
|
135
|
+
|
|
136
|
+
if not model_instance:
|
|
137
|
+
raise EntityNotFoundError(self.entity_name, entity_id)
|
|
138
|
+
|
|
139
|
+
for key, value in update_data.items():
|
|
140
|
+
if value is not None and hasattr(model_instance, key):
|
|
141
|
+
setattr(model_instance, key, value)
|
|
142
|
+
|
|
143
|
+
session.flush() # Flush to validate constraints
|
|
144
|
+
session.refresh(model_instance)
|
|
145
|
+
|
|
146
|
+
entity = self.entity_class.model_validate(model_instance)
|
|
147
|
+
|
|
148
|
+
return entity
|
|
149
|
+
|
|
150
|
+
def delete(self, session: Session, entity_id: Any) -> None:
|
|
151
|
+
"""
|
|
152
|
+
Delete an entity.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
session: Database session
|
|
156
|
+
entity_id: Entity identifier
|
|
157
|
+
|
|
158
|
+
Raises:
|
|
159
|
+
EntityNotFoundError: If entity not found
|
|
160
|
+
"""
|
|
161
|
+
model_instance = session.query(self.model_class).filter(
|
|
162
|
+
self.model_class.id == str(entity_id)
|
|
163
|
+
).first()
|
|
164
|
+
|
|
165
|
+
if not model_instance:
|
|
166
|
+
raise EntityNotFoundError(self.entity_name, entity_id)
|
|
167
|
+
|
|
168
|
+
session.delete(model_instance)
|
|
169
|
+
session.flush() # Flush to validate constraints
|
|
170
|
+
|
|
171
|
+
def exists(self, session: Session, entity_id: Any) -> bool:
|
|
172
|
+
"""
|
|
173
|
+
Check if an entity exists.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
session: Database session
|
|
177
|
+
entity_id: Entity identifier
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
True if entity exists, False otherwise
|
|
181
|
+
"""
|
|
182
|
+
count = session.query(self.model_class).filter(
|
|
183
|
+
self.model_class.id == str(entity_id)
|
|
184
|
+
).count()
|
|
185
|
+
|
|
186
|
+
return count > 0
|
|
187
|
+
|
|
188
|
+
def count(self, session: Session) -> int:
|
|
189
|
+
"""
|
|
190
|
+
Get total count of entities.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
session: Database session
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Total number of entities
|
|
197
|
+
"""
|
|
198
|
+
return session.query(self.model_class).count()
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class PaginatedRepository(BaseRepository[ModelType, EntityType]):
|
|
202
|
+
"""
|
|
203
|
+
Base repository with enhanced pagination support.
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
def get_paginated(self, session: Session, page_number: int, page_size: int) -> tuple[List[EntityType], int]:
|
|
207
|
+
"""
|
|
208
|
+
Get paginated results.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
session: Database session
|
|
212
|
+
page_number: Page number (1-based)
|
|
213
|
+
page_size: Number of items per page
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Tuple of (entities, total_count)
|
|
217
|
+
"""
|
|
218
|
+
offset = (page_number - 1) * page_size
|
|
219
|
+
|
|
220
|
+
total_count = self.count(session)
|
|
221
|
+
|
|
222
|
+
entities = self.get_all(session, limit=page_size, offset=offset)
|
|
223
|
+
|
|
224
|
+
return entities, total_count
|
|
225
|
+
|
|
226
|
+
def get_paginated_with_info(self, session: Session, pagination: PaginationInfo) -> tuple[List[EntityType], PaginationInfo]:
|
|
227
|
+
"""
|
|
228
|
+
Get paginated results with complete pagination info.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
session: Database session
|
|
232
|
+
pagination: Pagination parameters
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
Tuple of (entities, updated_pagination_info)
|
|
236
|
+
"""
|
|
237
|
+
entities, total_count = self.get_paginated(session, pagination.page, pagination.page_size)
|
|
238
|
+
|
|
239
|
+
updated_pagination = PaginationInfo(
|
|
240
|
+
page=pagination.page,
|
|
241
|
+
page_size=pagination.page_size,
|
|
242
|
+
total_items=total_count,
|
|
243
|
+
total_pages=(total_count + pagination.page_size - 1) // pagination.page_size,
|
|
244
|
+
has_next=pagination.page * pagination.page_size < total_count,
|
|
245
|
+
has_previous=pagination.page > 1
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
return entities, updated_pagination
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class ValidationMixin:
|
|
252
|
+
"""
|
|
253
|
+
Mixin for repositories that need validation logic.
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
def validate_create_data(self, create_data: Dict[str, Any]) -> None:
|
|
257
|
+
"""
|
|
258
|
+
Validate data before creation.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
create_data: Data to validate
|
|
262
|
+
|
|
263
|
+
Raises:
|
|
264
|
+
ValidationError: If validation fails
|
|
265
|
+
"""
|
|
266
|
+
pass
|
|
267
|
+
|
|
268
|
+
def validate_update_data(self, update_data: Dict[str, Any]) -> None:
|
|
269
|
+
"""
|
|
270
|
+
Validate data before update.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
update_data: Data to validate
|
|
274
|
+
|
|
275
|
+
Raises:
|
|
276
|
+
ValidationError: If validation fails
|
|
277
|
+
"""
|
|
278
|
+
pass
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Database Exception Handling
|
|
3
|
+
|
|
4
|
+
Provides centralized handling for converting SQLAlchemy exceptions to domain exceptions.
|
|
5
|
+
This handler translates low-level database errors into meaningful business exceptions
|
|
6
|
+
that can be properly handled by the API layer.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Optional, Dict, List
|
|
10
|
+
from sqlalchemy.exc import IntegrityError, SQLAlchemyError, OperationalError, DatabaseError
|
|
11
|
+
from .exceptions import ValidationError, DataIntegrityError, EntityAlreadyExistsError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DatabaseExceptionHandler:
|
|
15
|
+
"""
|
|
16
|
+
Centralized handler for converting SQLAlchemy exceptions to domain exceptions.
|
|
17
|
+
|
|
18
|
+
This handler translates low-level database errors into meaningful business exceptions
|
|
19
|
+
that can be properly handled by the API layer.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def handle_integrity_error(e: IntegrityError, entity_type: str = "Resource") -> ValidationError:
|
|
24
|
+
"""
|
|
25
|
+
Convert SQLAlchemy integrity constraint violations to domain exceptions.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
e: The SQLAlchemy IntegrityError
|
|
29
|
+
entity_type: The type of entity being operated on
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
ValidationError with appropriate message and validation details
|
|
33
|
+
"""
|
|
34
|
+
# Try to get the most descriptive error message for constraint matching
|
|
35
|
+
full_message = str(e)
|
|
36
|
+
orig_message = str(e.orig) if hasattr(e, 'orig') and e.orig else ""
|
|
37
|
+
|
|
38
|
+
# Use the message that contains constraint information
|
|
39
|
+
if any(keyword in full_message for keyword in ['constraint', 'UNIQUE', 'NULL', 'CHECK', 'FOREIGN']):
|
|
40
|
+
# Extract the most specific constraint line from the full message
|
|
41
|
+
lines = full_message.split('\n')
|
|
42
|
+
|
|
43
|
+
# Prioritize SQL lines that contain actual constraint details
|
|
44
|
+
for line in lines:
|
|
45
|
+
if '[SQL:' in line and any(keyword in line for keyword in ['UNIQUE', 'NULL', 'CHECK', 'FOREIGN']):
|
|
46
|
+
# Extract just the SQL part without the [SQL: ] wrapper
|
|
47
|
+
if '[SQL:' in line and ']' in line:
|
|
48
|
+
error_message = line.split('[SQL:')[1].split(']')[0].strip()
|
|
49
|
+
break
|
|
50
|
+
error_message = line.strip()
|
|
51
|
+
break
|
|
52
|
+
else:
|
|
53
|
+
# Fallback to any line with constraint keywords
|
|
54
|
+
for line in lines:
|
|
55
|
+
if any(keyword in line for keyword in ['constraint', 'UNIQUE', 'NULL', 'CHECK', 'FOREIGN']):
|
|
56
|
+
error_message = line.strip()
|
|
57
|
+
break
|
|
58
|
+
else:
|
|
59
|
+
error_message = full_message
|
|
60
|
+
else:
|
|
61
|
+
# Fallback to orig if no constraint info in main message
|
|
62
|
+
error_message = orig_message or full_message
|
|
63
|
+
|
|
64
|
+
# Handle UNIQUE constraint violations and concurrent modifications
|
|
65
|
+
if ("UNIQUE constraint failed" in error_message or
|
|
66
|
+
"duplicate key value" in error_message.lower() or
|
|
67
|
+
"concurrent modification" in error_message.lower() or
|
|
68
|
+
"row was updated or deleted by another transaction" in error_message.lower()):
|
|
69
|
+
field_name = DatabaseExceptionHandler._extract_field_from_unique_error(error_message)
|
|
70
|
+
return EntityAlreadyExistsError(
|
|
71
|
+
entity_type=entity_type,
|
|
72
|
+
identifier=field_name,
|
|
73
|
+
value="provided value"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Handle NOT NULL constraint violations
|
|
77
|
+
if "NOT NULL constraint failed" in error_message or "null value in column" in error_message.lower():
|
|
78
|
+
field_name = DatabaseExceptionHandler._extract_field_from_null_error(error_message)
|
|
79
|
+
return ValidationError(
|
|
80
|
+
message="Required field is missing",
|
|
81
|
+
validation_details={field_name: ["This field is required"]},
|
|
82
|
+
entity_type=entity_type
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Handle CHECK constraint violations
|
|
86
|
+
if "CHECK constraint failed" in error_message or "check constraint" in error_message.lower():
|
|
87
|
+
constraint_name = DatabaseExceptionHandler._extract_constraint_name(error_message)
|
|
88
|
+
field_name = DatabaseExceptionHandler._map_constraint_to_field(constraint_name)
|
|
89
|
+
return ValidationError(
|
|
90
|
+
message="Field validation failed",
|
|
91
|
+
validation_details={field_name: ["Value violates validation rules"]},
|
|
92
|
+
entity_type=entity_type
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Handle FOREIGN KEY constraint violations
|
|
96
|
+
if "FOREIGN KEY constraint failed" in error_message or "foreign key constraint" in error_message.lower():
|
|
97
|
+
return ValidationError(
|
|
98
|
+
message="Referenced resource does not exist",
|
|
99
|
+
validation_details={"reference": ["Invalid reference to related resource"]},
|
|
100
|
+
entity_type=entity_type
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Generic integrity error fallback
|
|
104
|
+
return DataIntegrityError(
|
|
105
|
+
constraint="integrity_violation",
|
|
106
|
+
message=f"Database constraint violation: {error_message}"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
@staticmethod
|
|
110
|
+
def handle_operational_error(e: OperationalError, entity_type: str = "Resource") -> DataIntegrityError:
|
|
111
|
+
"""
|
|
112
|
+
Handle SQLAlchemy operational errors (connection issues, timeouts, etc.).
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
e: The SQLAlchemy OperationalError
|
|
116
|
+
entity_type: The type of entity being operated on
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
DataIntegrityError with appropriate message
|
|
120
|
+
"""
|
|
121
|
+
error_message = str(e.orig) if hasattr(e, 'orig') else str(e)
|
|
122
|
+
|
|
123
|
+
if "timeout" in error_message.lower():
|
|
124
|
+
return DataIntegrityError(
|
|
125
|
+
constraint="timeout",
|
|
126
|
+
message="Database operation timed out. Please try again."
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if "connection" in error_message.lower():
|
|
130
|
+
return DataIntegrityError(
|
|
131
|
+
constraint="connection_error",
|
|
132
|
+
message="Database connection failed. Please try again later."
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
return DataIntegrityError(
|
|
136
|
+
constraint="operational_error",
|
|
137
|
+
message="Database operation failed. Please contact support if this persists."
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
@staticmethod
|
|
141
|
+
def handle_database_error(e: DatabaseError, entity_type: str = "Resource") -> DataIntegrityError:
|
|
142
|
+
"""
|
|
143
|
+
Handle general SQLAlchemy database errors.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
e: The SQLAlchemy DatabaseError
|
|
147
|
+
entity_type: The type of entity being operated on
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
DataIntegrityError with appropriate message
|
|
151
|
+
"""
|
|
152
|
+
error_message = str(e.orig) if hasattr(e, 'orig') else str(e)
|
|
153
|
+
|
|
154
|
+
return DataIntegrityError(
|
|
155
|
+
constraint="database_error",
|
|
156
|
+
message="A database error occurred. Please contact support."
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
@staticmethod
|
|
160
|
+
def handle_sqlalchemy_error(e: SQLAlchemyError, entity_type: str = "Resource") -> DataIntegrityError:
|
|
161
|
+
"""
|
|
162
|
+
Handle any other SQLAlchemy errors not caught by specific handlers.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
e: The SQLAlchemy error
|
|
166
|
+
entity_type: The type of entity being operated on
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
DataIntegrityError with generic message
|
|
170
|
+
"""
|
|
171
|
+
error_message = str(e)
|
|
172
|
+
|
|
173
|
+
return DataIntegrityError(
|
|
174
|
+
constraint="sqlalchemy_error",
|
|
175
|
+
message="An unexpected database error occurred. Please contact support."
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
@staticmethod
|
|
179
|
+
def _extract_field_from_unique_error(error_message: str) -> str:
|
|
180
|
+
"""Extract field name from UNIQUE constraint error message."""
|
|
181
|
+
# Try to extract field name from common formats
|
|
182
|
+
if "agents.name" in error_message:
|
|
183
|
+
return "name"
|
|
184
|
+
elif "agents.id" in error_message:
|
|
185
|
+
return "id"
|
|
186
|
+
elif "." in error_message:
|
|
187
|
+
# Extract field after table.field pattern
|
|
188
|
+
parts = error_message.split(".")
|
|
189
|
+
if len(parts) > 1:
|
|
190
|
+
field_part = parts[1].split()[0] # Get first word after dot
|
|
191
|
+
return field_part.strip('()"\'')
|
|
192
|
+
|
|
193
|
+
return "field" # Generic fallback
|
|
194
|
+
|
|
195
|
+
@staticmethod
|
|
196
|
+
def _extract_field_from_null_error(error_message: str) -> str:
|
|
197
|
+
"""Extract field name from NOT NULL constraint error message."""
|
|
198
|
+
# Common patterns: "NOT NULL constraint failed: agents.name"
|
|
199
|
+
if ":" in error_message:
|
|
200
|
+
field_part = error_message.split(":")[-1].strip()
|
|
201
|
+
if "." in field_part:
|
|
202
|
+
return field_part.split(".")[-1]
|
|
203
|
+
return field_part
|
|
204
|
+
|
|
205
|
+
return "field" # Generic fallback
|
|
206
|
+
|
|
207
|
+
@staticmethod
|
|
208
|
+
def _extract_constraint_name(error_message: str) -> str:
|
|
209
|
+
"""Extract constraint name from CHECK constraint error message."""
|
|
210
|
+
# Common patterns: "CHECK constraint failed: check_name_length"
|
|
211
|
+
if ":" in error_message:
|
|
212
|
+
constraint_part = error_message.split(":")[-1].strip()
|
|
213
|
+
return constraint_part
|
|
214
|
+
|
|
215
|
+
return "unknown_constraint"
|
|
216
|
+
|
|
217
|
+
@staticmethod
|
|
218
|
+
def _map_constraint_to_field(constraint_name: str) -> str:
|
|
219
|
+
"""Map database constraint names to user-friendly field names."""
|
|
220
|
+
constraint_field_map = {
|
|
221
|
+
"check_name_length": "name",
|
|
222
|
+
"check_description_length": "description",
|
|
223
|
+
"check_system_prompt_length": "systemPrompt",
|
|
224
|
+
"check_created_by_length": "createdBy",
|
|
225
|
+
"check_updated_by_length": "updatedBy",
|
|
226
|
+
"check_id_length": "id"
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return constraint_field_map.get(constraint_name, "field")
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class DatabaseErrorDecorator:
|
|
233
|
+
"""
|
|
234
|
+
Decorator class for wrapping repository methods with database exception handling.
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
def __init__(self, entity_type: str = "Resource"):
|
|
238
|
+
self.entity_type = entity_type
|
|
239
|
+
|
|
240
|
+
def __call__(self, func):
|
|
241
|
+
"""
|
|
242
|
+
Decorator that wraps repository methods with database exception handling.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
func: The repository method to wrap
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
Wrapped function with exception handling
|
|
249
|
+
"""
|
|
250
|
+
def wrapper(*args, **kwargs):
|
|
251
|
+
try:
|
|
252
|
+
return func(*args, **kwargs)
|
|
253
|
+
except IntegrityError as e:
|
|
254
|
+
raise DatabaseExceptionHandler.handle_integrity_error(e, self.entity_type)
|
|
255
|
+
except OperationalError as e:
|
|
256
|
+
raise DatabaseExceptionHandler.handle_operational_error(e, self.entity_type)
|
|
257
|
+
except DatabaseError as e:
|
|
258
|
+
raise DatabaseExceptionHandler.handle_database_error(e, self.entity_type)
|
|
259
|
+
except SQLAlchemyError as e:
|
|
260
|
+
raise DatabaseExceptionHandler.handle_sqlalchemy_error(e, self.entity_type)
|
|
261
|
+
|
|
262
|
+
return wrapper
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def handle_database_errors(entity_type: str = "Resource"):
|
|
266
|
+
"""
|
|
267
|
+
Convenience decorator for database exception handling.
|
|
268
|
+
|
|
269
|
+
Usage:
|
|
270
|
+
@handle_database_errors("Agent")
|
|
271
|
+
def create_agent(self, ...):
|
|
272
|
+
# Repository method implementation
|
|
273
|
+
"""
|
|
274
|
+
return DatabaseErrorDecorator(entity_type)
|