mcp-eregistrations-bpa 0.8.5__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 mcp-eregistrations-bpa might be problematic. Click here for more details.
- mcp_eregistrations_bpa/__init__.py +121 -0
- mcp_eregistrations_bpa/__main__.py +6 -0
- mcp_eregistrations_bpa/arazzo/__init__.py +21 -0
- mcp_eregistrations_bpa/arazzo/expression.py +379 -0
- mcp_eregistrations_bpa/audit/__init__.py +56 -0
- mcp_eregistrations_bpa/audit/context.py +66 -0
- mcp_eregistrations_bpa/audit/logger.py +236 -0
- mcp_eregistrations_bpa/audit/models.py +131 -0
- mcp_eregistrations_bpa/auth/__init__.py +64 -0
- mcp_eregistrations_bpa/auth/callback.py +391 -0
- mcp_eregistrations_bpa/auth/cas.py +409 -0
- mcp_eregistrations_bpa/auth/oidc.py +252 -0
- mcp_eregistrations_bpa/auth/permissions.py +162 -0
- mcp_eregistrations_bpa/auth/token_manager.py +348 -0
- mcp_eregistrations_bpa/bpa_client/__init__.py +84 -0
- mcp_eregistrations_bpa/bpa_client/client.py +740 -0
- mcp_eregistrations_bpa/bpa_client/endpoints.py +193 -0
- mcp_eregistrations_bpa/bpa_client/errors.py +276 -0
- mcp_eregistrations_bpa/bpa_client/models.py +203 -0
- mcp_eregistrations_bpa/config.py +349 -0
- mcp_eregistrations_bpa/db/__init__.py +21 -0
- mcp_eregistrations_bpa/db/connection.py +64 -0
- mcp_eregistrations_bpa/db/migrations.py +168 -0
- mcp_eregistrations_bpa/exceptions.py +39 -0
- mcp_eregistrations_bpa/py.typed +0 -0
- mcp_eregistrations_bpa/rollback/__init__.py +19 -0
- mcp_eregistrations_bpa/rollback/manager.py +616 -0
- mcp_eregistrations_bpa/server.py +152 -0
- mcp_eregistrations_bpa/tools/__init__.py +372 -0
- mcp_eregistrations_bpa/tools/actions.py +155 -0
- mcp_eregistrations_bpa/tools/analysis.py +352 -0
- mcp_eregistrations_bpa/tools/audit.py +399 -0
- mcp_eregistrations_bpa/tools/behaviours.py +1042 -0
- mcp_eregistrations_bpa/tools/bots.py +627 -0
- mcp_eregistrations_bpa/tools/classifications.py +575 -0
- mcp_eregistrations_bpa/tools/costs.py +765 -0
- mcp_eregistrations_bpa/tools/debug_strategies.py +351 -0
- mcp_eregistrations_bpa/tools/debugger.py +1230 -0
- mcp_eregistrations_bpa/tools/determinants.py +2235 -0
- mcp_eregistrations_bpa/tools/document_requirements.py +670 -0
- mcp_eregistrations_bpa/tools/export.py +899 -0
- mcp_eregistrations_bpa/tools/fields.py +162 -0
- mcp_eregistrations_bpa/tools/form_errors.py +36 -0
- mcp_eregistrations_bpa/tools/formio_helpers.py +971 -0
- mcp_eregistrations_bpa/tools/forms.py +1269 -0
- mcp_eregistrations_bpa/tools/jsonlogic_builder.py +466 -0
- mcp_eregistrations_bpa/tools/large_response.py +163 -0
- mcp_eregistrations_bpa/tools/messages.py +523 -0
- mcp_eregistrations_bpa/tools/notifications.py +241 -0
- mcp_eregistrations_bpa/tools/registration_institutions.py +680 -0
- mcp_eregistrations_bpa/tools/registrations.py +897 -0
- mcp_eregistrations_bpa/tools/role_status.py +447 -0
- mcp_eregistrations_bpa/tools/role_units.py +400 -0
- mcp_eregistrations_bpa/tools/roles.py +1236 -0
- mcp_eregistrations_bpa/tools/rollback.py +335 -0
- mcp_eregistrations_bpa/tools/services.py +674 -0
- mcp_eregistrations_bpa/tools/workflows.py +2487 -0
- mcp_eregistrations_bpa/tools/yaml_transformer.py +991 -0
- mcp_eregistrations_bpa/workflows/__init__.py +28 -0
- mcp_eregistrations_bpa/workflows/loader.py +440 -0
- mcp_eregistrations_bpa/workflows/models.py +336 -0
- mcp_eregistrations_bpa-0.8.5.dist-info/METADATA +965 -0
- mcp_eregistrations_bpa-0.8.5.dist-info/RECORD +66 -0
- mcp_eregistrations_bpa-0.8.5.dist-info/WHEEL +4 -0
- mcp_eregistrations_bpa-0.8.5.dist-info/entry_points.txt +2 -0
- mcp_eregistrations_bpa-0.8.5.dist-info/licenses/LICENSE +86 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""BPA API endpoint constants.
|
|
2
|
+
|
|
3
|
+
This module defines all BPA API v3 (2016/06) endpoint URL patterns.
|
|
4
|
+
Endpoints use string formatting for path parameters.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from mcp_eregistrations_bpa.bpa_client.endpoints import SERVICES, SERVICE_BY_ID
|
|
8
|
+
|
|
9
|
+
url = SERVICES # "/service"
|
|
10
|
+
url = SERVICE_BY_ID.format(id=123) # "/service/123"
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
# Service endpoints
|
|
17
|
+
"SERVICES",
|
|
18
|
+
"SERVICE_BY_ID",
|
|
19
|
+
"SERVICE_FIELDS",
|
|
20
|
+
"SERVICE_DETERMINANTS",
|
|
21
|
+
"SERVICE_FORMS",
|
|
22
|
+
"SERVICE_ROLES",
|
|
23
|
+
"SERVICE_REGISTRATIONS",
|
|
24
|
+
# Registration endpoints
|
|
25
|
+
"REGISTRATIONS",
|
|
26
|
+
"REGISTRATION_BY_ID",
|
|
27
|
+
"REGISTRATION_FIELDS",
|
|
28
|
+
"REGISTRATION_DETERMINANTS",
|
|
29
|
+
"REGISTRATION_COSTS",
|
|
30
|
+
"REGISTRATION_DOCUMENTS",
|
|
31
|
+
# Form endpoints
|
|
32
|
+
"FORMS",
|
|
33
|
+
"FORM_BY_ID",
|
|
34
|
+
"FORM_FIELDS",
|
|
35
|
+
"FORM_DETERMINANTS",
|
|
36
|
+
# Field endpoints
|
|
37
|
+
"FIELDS",
|
|
38
|
+
"FIELD_BY_ID",
|
|
39
|
+
"FIELD_DETERMINANTS",
|
|
40
|
+
"FIELD_REGISTRATIONS",
|
|
41
|
+
# Determinant endpoints
|
|
42
|
+
"DETERMINANTS",
|
|
43
|
+
"DETERMINANT_BY_ID",
|
|
44
|
+
"DETERMINANT_FIELDS",
|
|
45
|
+
"DETERMINANT_REGISTRATIONS",
|
|
46
|
+
# Role endpoints
|
|
47
|
+
"ROLES",
|
|
48
|
+
"ROLE_BY_ID",
|
|
49
|
+
# Cost endpoints
|
|
50
|
+
"COSTS",
|
|
51
|
+
"COST_BY_ID",
|
|
52
|
+
# Document endpoints
|
|
53
|
+
"DOCUMENTS",
|
|
54
|
+
"DOCUMENT_BY_ID",
|
|
55
|
+
# Component Actions endpoints
|
|
56
|
+
"COMPONENT_ACTIONS_BY_ID",
|
|
57
|
+
"SERVICE_COMPONENT_ACTIONS",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
# =============================================================================
|
|
61
|
+
# Service Endpoints
|
|
62
|
+
# =============================================================================
|
|
63
|
+
|
|
64
|
+
#: List all services
|
|
65
|
+
SERVICES = "/service"
|
|
66
|
+
|
|
67
|
+
#: Get/update/delete service by ID
|
|
68
|
+
SERVICE_BY_ID = "/service/{id}"
|
|
69
|
+
|
|
70
|
+
#: Fields for a specific service
|
|
71
|
+
SERVICE_FIELDS = "/service/{service_id}/fields"
|
|
72
|
+
|
|
73
|
+
#: Determinants for a specific service
|
|
74
|
+
SERVICE_DETERMINANTS = "/service/{service_id}/determinant"
|
|
75
|
+
|
|
76
|
+
#: Forms for a specific service
|
|
77
|
+
SERVICE_FORMS = "/service/{service_id}/form"
|
|
78
|
+
|
|
79
|
+
#: Roles for a specific service
|
|
80
|
+
SERVICE_ROLES = "/service/{service_id}/roles"
|
|
81
|
+
|
|
82
|
+
#: Registrations for a specific service
|
|
83
|
+
SERVICE_REGISTRATIONS = "/service/{service_id}/registrations"
|
|
84
|
+
|
|
85
|
+
# =============================================================================
|
|
86
|
+
# Registration Endpoints
|
|
87
|
+
# =============================================================================
|
|
88
|
+
|
|
89
|
+
#: List all registrations
|
|
90
|
+
REGISTRATIONS = "/registration"
|
|
91
|
+
|
|
92
|
+
#: Get/update/delete registration by ID
|
|
93
|
+
REGISTRATION_BY_ID = "/registration/{registration_id}"
|
|
94
|
+
|
|
95
|
+
#: Fields for a specific registration
|
|
96
|
+
REGISTRATION_FIELDS = "/registration/{registration_id}/fields"
|
|
97
|
+
|
|
98
|
+
#: Determinants for a specific registration
|
|
99
|
+
REGISTRATION_DETERMINANTS = "/registration/{registration_id}/determinants"
|
|
100
|
+
|
|
101
|
+
#: Costs for a specific registration
|
|
102
|
+
REGISTRATION_COSTS = "/registration/{registration_id}/costs"
|
|
103
|
+
|
|
104
|
+
#: Documents for a specific registration
|
|
105
|
+
REGISTRATION_DOCUMENTS = "/registration/{registration_id}/documents"
|
|
106
|
+
|
|
107
|
+
# =============================================================================
|
|
108
|
+
# Form Endpoints
|
|
109
|
+
# =============================================================================
|
|
110
|
+
|
|
111
|
+
#: List all forms
|
|
112
|
+
FORMS = "/form"
|
|
113
|
+
|
|
114
|
+
#: Get/update/delete form by ID
|
|
115
|
+
FORM_BY_ID = "/form/{form_id}"
|
|
116
|
+
|
|
117
|
+
#: Fields for a specific form
|
|
118
|
+
FORM_FIELDS = "/form/{form_id}/fields"
|
|
119
|
+
|
|
120
|
+
#: Determinants for a specific form
|
|
121
|
+
FORM_DETERMINANTS = "/form/{form_id}/determinants"
|
|
122
|
+
|
|
123
|
+
# =============================================================================
|
|
124
|
+
# Field Endpoints
|
|
125
|
+
# =============================================================================
|
|
126
|
+
|
|
127
|
+
#: List all fields
|
|
128
|
+
FIELDS = "/field"
|
|
129
|
+
|
|
130
|
+
#: Get/update/delete field by ID
|
|
131
|
+
FIELD_BY_ID = "/field/{field_id}"
|
|
132
|
+
|
|
133
|
+
#: Determinants linked to a field (many-to-many)
|
|
134
|
+
FIELD_DETERMINANTS = "/field/{field_id}/determinants"
|
|
135
|
+
|
|
136
|
+
#: Registrations linked to a field (many-to-many)
|
|
137
|
+
FIELD_REGISTRATIONS = "/field/{field_id}/registrations"
|
|
138
|
+
|
|
139
|
+
# =============================================================================
|
|
140
|
+
# Determinant Endpoints
|
|
141
|
+
# =============================================================================
|
|
142
|
+
|
|
143
|
+
#: List all determinants
|
|
144
|
+
DETERMINANTS = "/determinant"
|
|
145
|
+
|
|
146
|
+
#: Get/update/delete determinant by ID
|
|
147
|
+
DETERMINANT_BY_ID = "/determinant/{determinant_id}"
|
|
148
|
+
|
|
149
|
+
#: Fields linked to a determinant (many-to-many)
|
|
150
|
+
DETERMINANT_FIELDS = "/determinant/{determinant_id}/fields"
|
|
151
|
+
|
|
152
|
+
#: Registrations linked to a determinant (many-to-many)
|
|
153
|
+
DETERMINANT_REGISTRATIONS = "/determinant/{determinant_id}/registrations"
|
|
154
|
+
|
|
155
|
+
# =============================================================================
|
|
156
|
+
# Role Endpoints
|
|
157
|
+
# =============================================================================
|
|
158
|
+
|
|
159
|
+
#: List all roles
|
|
160
|
+
ROLES = "/role"
|
|
161
|
+
|
|
162
|
+
#: Get/update/delete role by ID
|
|
163
|
+
ROLE_BY_ID = "/role/{role_id}"
|
|
164
|
+
|
|
165
|
+
# =============================================================================
|
|
166
|
+
# Cost Endpoints
|
|
167
|
+
# =============================================================================
|
|
168
|
+
|
|
169
|
+
#: List all costs
|
|
170
|
+
COSTS = "/cost"
|
|
171
|
+
|
|
172
|
+
#: Get/update/delete cost by ID
|
|
173
|
+
COST_BY_ID = "/cost/{cost_id}"
|
|
174
|
+
|
|
175
|
+
# =============================================================================
|
|
176
|
+
# Document Endpoints
|
|
177
|
+
# =============================================================================
|
|
178
|
+
|
|
179
|
+
#: List all documents
|
|
180
|
+
DOCUMENTS = "/document"
|
|
181
|
+
|
|
182
|
+
#: Get/update/delete document by ID
|
|
183
|
+
DOCUMENT_BY_ID = "/document/{document_id}"
|
|
184
|
+
|
|
185
|
+
# =============================================================================
|
|
186
|
+
# Component Actions Endpoints
|
|
187
|
+
# =============================================================================
|
|
188
|
+
|
|
189
|
+
#: Get/update component actions by ID
|
|
190
|
+
COMPONENT_ACTIONS_BY_ID = "/componentactions/{id}"
|
|
191
|
+
|
|
192
|
+
#: Get/update/delete component actions for a specific service component
|
|
193
|
+
SERVICE_COMPONENT_ACTIONS = "/service/{service_id}/componentactions/{component_key}"
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""BPA client error handling and translation.
|
|
2
|
+
|
|
3
|
+
This module provides error classes for BPA API interactions and
|
|
4
|
+
translates HTTP errors to AI-friendly ToolError messages.
|
|
5
|
+
|
|
6
|
+
Error translation follows the pattern:
|
|
7
|
+
"[What happened]: [Why it matters]. [Suggested action]"
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
from mcp_eregistrations_bpa.bpa_client.errors import translate_error
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
response.raise_for_status()
|
|
14
|
+
except httpx.HTTPStatusError as e:
|
|
15
|
+
raise translate_error(e)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import TYPE_CHECKING
|
|
21
|
+
|
|
22
|
+
from mcp.server.fastmcp.exceptions import ToolError
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
import httpx
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"BPAClientError",
|
|
29
|
+
"BPAConnectionError",
|
|
30
|
+
"BPATimeoutError",
|
|
31
|
+
"BPAAuthenticationError",
|
|
32
|
+
"BPAPermissionError",
|
|
33
|
+
"BPANotFoundError",
|
|
34
|
+
"BPAValidationError",
|
|
35
|
+
"BPARateLimitError",
|
|
36
|
+
"BPAServerError",
|
|
37
|
+
"translate_error",
|
|
38
|
+
"translate_http_error",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class BPAClientError(Exception):
|
|
43
|
+
"""Base exception for BPA client errors.
|
|
44
|
+
|
|
45
|
+
All BPA client errors inherit from this class.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, message: str, status_code: int | None = None) -> None:
|
|
49
|
+
"""Initialize BPA client error.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
message: Error message.
|
|
53
|
+
status_code: HTTP status code if applicable.
|
|
54
|
+
"""
|
|
55
|
+
super().__init__(message)
|
|
56
|
+
self.status_code = status_code
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class BPAConnectionError(BPAClientError):
|
|
60
|
+
"""Connection to BPA API failed."""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class BPATimeoutError(BPAClientError):
|
|
64
|
+
"""Request to BPA API timed out."""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class BPAAuthenticationError(BPAClientError):
|
|
68
|
+
"""Authentication with BPA API failed (401)."""
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class BPAPermissionError(BPAClientError):
|
|
72
|
+
"""Permission denied by BPA API (403)."""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class BPANotFoundError(BPAClientError):
|
|
76
|
+
"""Resource not found in BPA API (404)."""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class BPAValidationError(BPAClientError):
|
|
80
|
+
"""Validation error from BPA API (400)."""
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class BPARateLimitError(BPAClientError):
|
|
84
|
+
"""Rate limit exceeded (429)."""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class BPAServerError(BPAClientError):
|
|
88
|
+
"""BPA server error (5xx)."""
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def translate_http_error(
|
|
92
|
+
error: httpx.HTTPStatusError,
|
|
93
|
+
*,
|
|
94
|
+
resource_type: str | None = None,
|
|
95
|
+
resource_id: str | int | None = None,
|
|
96
|
+
) -> BPAClientError:
|
|
97
|
+
"""Translate httpx HTTP error to BPA-specific exception.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
error: The httpx HTTP status error.
|
|
101
|
+
resource_type: Optional resource type for context (e.g., "service").
|
|
102
|
+
resource_id: Optional resource ID for context.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
A BPAClientError subclass appropriate for the status code.
|
|
106
|
+
"""
|
|
107
|
+
status_code = error.response.status_code
|
|
108
|
+
response_text = error.response.text[:200] if error.response.text else ""
|
|
109
|
+
|
|
110
|
+
# Build resource context
|
|
111
|
+
resource_context = ""
|
|
112
|
+
if resource_type:
|
|
113
|
+
resource_context = f" {resource_type}"
|
|
114
|
+
if resource_id is not None:
|
|
115
|
+
resource_context = f" {resource_type} (ID: {resource_id})"
|
|
116
|
+
|
|
117
|
+
if status_code == 400:
|
|
118
|
+
# Check for "Database object not found" which BPA returns as 400 instead of 404
|
|
119
|
+
if "Database object not found" in response_text:
|
|
120
|
+
# Extract the ID from the message if possible
|
|
121
|
+
import json
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
error_data = json.loads(response_text)
|
|
125
|
+
error_msg = error_data.get("message", "")
|
|
126
|
+
# Parse "Database object not found by id = X" pattern
|
|
127
|
+
if "by id =" in error_msg:
|
|
128
|
+
obj_id = error_msg.split("by id =")[1].split(",")[0].strip()
|
|
129
|
+
return BPANotFoundError(
|
|
130
|
+
f"Resource with ID '{obj_id}' not found. "
|
|
131
|
+
"Verify the ID is correct.",
|
|
132
|
+
status_code=status_code,
|
|
133
|
+
)
|
|
134
|
+
except (json.JSONDecodeError, IndexError):
|
|
135
|
+
pass
|
|
136
|
+
return BPANotFoundError(
|
|
137
|
+
"Resource not found. Verify the ID is correct.",
|
|
138
|
+
status_code=status_code,
|
|
139
|
+
)
|
|
140
|
+
msg = f"Invalid request for{resource_context}: {response_text}".strip()
|
|
141
|
+
return BPAValidationError(msg, status_code=status_code)
|
|
142
|
+
|
|
143
|
+
if status_code == 401:
|
|
144
|
+
return BPAAuthenticationError(
|
|
145
|
+
"Authentication failed. Token may be invalid or expired.",
|
|
146
|
+
status_code=status_code,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
if status_code == 403:
|
|
150
|
+
return BPAPermissionError(
|
|
151
|
+
f"Access denied to{resource_context}. Check your permissions.",
|
|
152
|
+
status_code=status_code,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if status_code == 404:
|
|
156
|
+
if resource_type:
|
|
157
|
+
msg = f"{resource_type.capitalize()} not found"
|
|
158
|
+
if resource_id is not None:
|
|
159
|
+
msg = f"{resource_type.capitalize()} with ID {resource_id} not found"
|
|
160
|
+
else:
|
|
161
|
+
msg = "Resource not found"
|
|
162
|
+
return BPANotFoundError(msg, status_code=status_code)
|
|
163
|
+
|
|
164
|
+
if status_code == 429:
|
|
165
|
+
return BPARateLimitError(
|
|
166
|
+
"Rate limit exceeded. Please wait before retrying.",
|
|
167
|
+
status_code=status_code,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if status_code >= 500:
|
|
171
|
+
return BPAServerError(
|
|
172
|
+
f"BPA server error ({status_code}): {response_text}".strip(),
|
|
173
|
+
status_code=status_code,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Generic error for other status codes
|
|
177
|
+
return BPAClientError(
|
|
178
|
+
f"BPA API error ({status_code}): {response_text}".strip(),
|
|
179
|
+
status_code=status_code,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def translate_error(
|
|
184
|
+
error: Exception,
|
|
185
|
+
*,
|
|
186
|
+
resource_type: str | None = None,
|
|
187
|
+
resource_id: str | int | None = None,
|
|
188
|
+
) -> ToolError:
|
|
189
|
+
"""Translate any BPA client error to AI-friendly ToolError.
|
|
190
|
+
|
|
191
|
+
This is the main entry point for error translation. It handles
|
|
192
|
+
all exception types and produces consistent AI-friendly messages.
|
|
193
|
+
|
|
194
|
+
Error message format:
|
|
195
|
+
"[What happened]: [Why it matters]. [Suggested action]"
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
error: The exception to translate.
|
|
199
|
+
resource_type: Optional resource type for context.
|
|
200
|
+
resource_id: Optional resource ID for context.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
ToolError with AI-friendly message.
|
|
204
|
+
"""
|
|
205
|
+
import httpx
|
|
206
|
+
|
|
207
|
+
# First translate httpx errors to BPA-specific errors
|
|
208
|
+
if isinstance(error, httpx.HTTPStatusError):
|
|
209
|
+
error = translate_http_error(
|
|
210
|
+
error, resource_type=resource_type, resource_id=resource_id
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# Now translate to ToolError with AI-friendly message
|
|
214
|
+
if isinstance(error, BPAConnectionError):
|
|
215
|
+
return ToolError(
|
|
216
|
+
"Failed to connect to BPA API: Network or server may be unreachable. "
|
|
217
|
+
"Check your connection and BPA_INSTANCE_URL configuration."
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
if isinstance(error, BPATimeoutError):
|
|
221
|
+
return ToolError(
|
|
222
|
+
"BPA API request timed out: The server took too long to respond. "
|
|
223
|
+
"Try again or check if the BPA instance is healthy."
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
if isinstance(error, BPAAuthenticationError):
|
|
227
|
+
return ToolError(
|
|
228
|
+
"BPA authentication failed: Your session may have expired. "
|
|
229
|
+
"Run auth_login to re-authenticate."
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
if isinstance(error, BPAPermissionError):
|
|
233
|
+
return ToolError(
|
|
234
|
+
f"Permission denied: {error}. "
|
|
235
|
+
"Contact your administrator if you need access."
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
if isinstance(error, BPANotFoundError):
|
|
239
|
+
return ToolError(
|
|
240
|
+
f"Resource not found: {error}. "
|
|
241
|
+
"Verify the resource exists and you have access."
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
if isinstance(error, BPAValidationError):
|
|
245
|
+
return ToolError(f"Invalid request: {error}. Check your input parameters.")
|
|
246
|
+
|
|
247
|
+
if isinstance(error, BPARateLimitError):
|
|
248
|
+
return ToolError(
|
|
249
|
+
"Rate limit exceeded: Too many requests to BPA API. "
|
|
250
|
+
"Wait a moment and try again."
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
if isinstance(error, BPAServerError):
|
|
254
|
+
return ToolError(
|
|
255
|
+
f"BPA server error: {error}. "
|
|
256
|
+
"The BPA service may be experiencing issues. Try again later."
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
if isinstance(error, BPAClientError):
|
|
260
|
+
return ToolError(f"BPA API error: {error}")
|
|
261
|
+
|
|
262
|
+
# Handle httpx connection errors
|
|
263
|
+
if isinstance(error, httpx.ConnectError):
|
|
264
|
+
return ToolError(
|
|
265
|
+
"Failed to connect to BPA API: Network or server may be unreachable. "
|
|
266
|
+
"Check your connection and BPA_INSTANCE_URL configuration."
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
if isinstance(error, httpx.TimeoutException):
|
|
270
|
+
return ToolError(
|
|
271
|
+
"BPA API request timed out: The server took too long to respond. "
|
|
272
|
+
"Try again or check if the BPA instance is healthy."
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
# Generic fallback
|
|
276
|
+
return ToolError(f"BPA API error: {error}")
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""Pydantic models for BPA API responses.
|
|
2
|
+
|
|
3
|
+
These models provide type-safe representations of BPA API entities.
|
|
4
|
+
All models use Pydantic v2 conventions.
|
|
5
|
+
|
|
6
|
+
Note: These are base models for the most common entities. Additional
|
|
7
|
+
models for specific endpoints may be added in future stories.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"BPABaseModel",
|
|
18
|
+
"Service",
|
|
19
|
+
"Registration",
|
|
20
|
+
"FormField",
|
|
21
|
+
"Determinant",
|
|
22
|
+
"Role",
|
|
23
|
+
"Cost",
|
|
24
|
+
"Document",
|
|
25
|
+
"Action",
|
|
26
|
+
"Form",
|
|
27
|
+
"PaginatedResponse",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class BPABaseModel(BaseModel):
|
|
32
|
+
"""Base model for all BPA entities.
|
|
33
|
+
|
|
34
|
+
Provides common configuration for all models.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
model_config = ConfigDict(
|
|
38
|
+
extra="allow", # Allow unknown fields from API
|
|
39
|
+
populate_by_name=True, # Support field aliases
|
|
40
|
+
str_strip_whitespace=True, # Clean string values
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Service(BPABaseModel):
|
|
45
|
+
"""BPA Service entity.
|
|
46
|
+
|
|
47
|
+
A service represents a government procedure or registration type.
|
|
48
|
+
Services contain registrations, forms, fields, determinants, and roles.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
id: int = Field(description="Unique service identifier")
|
|
52
|
+
name: str = Field(description="Service name")
|
|
53
|
+
short_name: str | None = Field(
|
|
54
|
+
default=None, alias="shortName", description="Short name abbreviation"
|
|
55
|
+
)
|
|
56
|
+
description: str | None = Field(default=None, description="Service description")
|
|
57
|
+
status: str | None = Field(default=None, description="Service status")
|
|
58
|
+
category: str | None = Field(default=None, description="Service category")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class Registration(BPABaseModel):
|
|
62
|
+
"""BPA Registration entity.
|
|
63
|
+
|
|
64
|
+
A registration is a concrete instance of a service procedure.
|
|
65
|
+
Registrations can link to fields, determinants, costs, and documents.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
id: int = Field(
|
|
69
|
+
description="Unique registration identifier", alias="registration_id"
|
|
70
|
+
)
|
|
71
|
+
name: str = Field(description="Registration name")
|
|
72
|
+
service_id: int | None = Field(
|
|
73
|
+
default=None, alias="serviceId", description="Parent service ID"
|
|
74
|
+
)
|
|
75
|
+
description: str | None = Field(
|
|
76
|
+
default=None, description="Registration description"
|
|
77
|
+
)
|
|
78
|
+
status: str | None = Field(default=None, description="Registration status")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class FormField(BPABaseModel):
|
|
82
|
+
"""BPA Form Field entity.
|
|
83
|
+
|
|
84
|
+
A field is a data input element in a form. Fields have many-to-many
|
|
85
|
+
relationships with determinants and registrations.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
id: int = Field(description="Unique field identifier", alias="field_id")
|
|
89
|
+
name: str = Field(description="Field name")
|
|
90
|
+
label: str | None = Field(default=None, description="Field label for display")
|
|
91
|
+
type: str | None = Field(default=None, description="Field data type")
|
|
92
|
+
required: bool = Field(default=False, description="Whether field is required")
|
|
93
|
+
description: str | None = Field(default=None, description="Field description")
|
|
94
|
+
service_id: int | None = Field(
|
|
95
|
+
default=None, alias="serviceId", description="Parent service ID"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class Determinant(BPABaseModel):
|
|
100
|
+
"""BPA Determinant entity.
|
|
101
|
+
|
|
102
|
+
A determinant controls conditional logic and field visibility.
|
|
103
|
+
Determinants have many-to-many relationships with fields and registrations.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
id: int = Field(description="Unique determinant identifier", alias="determinant_id")
|
|
107
|
+
name: str = Field(description="Determinant name")
|
|
108
|
+
type: str | None = Field(default=None, description="Determinant type")
|
|
109
|
+
description: str | None = Field(default=None, description="Determinant description")
|
|
110
|
+
service_id: int | None = Field(
|
|
111
|
+
default=None, alias="serviceId", description="Parent service ID"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class Role(BPABaseModel):
|
|
116
|
+
"""BPA Role entity.
|
|
117
|
+
|
|
118
|
+
A role represents an actor in the registration process.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
id: int = Field(description="Unique role identifier", alias="role_id")
|
|
122
|
+
name: str = Field(description="Role name")
|
|
123
|
+
description: str | None = Field(default=None, description="Role description")
|
|
124
|
+
service_id: int | None = Field(
|
|
125
|
+
default=None, alias="serviceId", description="Parent service ID"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class Cost(BPABaseModel):
|
|
130
|
+
"""BPA Cost entity.
|
|
131
|
+
|
|
132
|
+
A cost represents a fee associated with a registration.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
id: int = Field(description="Unique cost identifier", alias="cost_id")
|
|
136
|
+
name: str = Field(description="Cost name")
|
|
137
|
+
amount: float | None = Field(default=None, description="Cost amount")
|
|
138
|
+
currency: str | None = Field(default=None, description="Currency code")
|
|
139
|
+
description: str | None = Field(default=None, description="Cost description")
|
|
140
|
+
registration_id: int | None = Field(
|
|
141
|
+
default=None, alias="registrationId", description="Parent registration ID"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class Document(BPABaseModel):
|
|
146
|
+
"""BPA Document entity.
|
|
147
|
+
|
|
148
|
+
A document is a file or attachment required for a registration.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
id: int = Field(description="Unique document identifier", alias="document_id")
|
|
152
|
+
name: str = Field(description="Document name")
|
|
153
|
+
type: str | None = Field(default=None, description="Document type/format")
|
|
154
|
+
required: bool = Field(default=False, description="Whether document is required")
|
|
155
|
+
description: str | None = Field(default=None, description="Document description")
|
|
156
|
+
registration_id: int | None = Field(
|
|
157
|
+
default=None, alias="registrationId", description="Parent registration ID"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class Action(BPABaseModel):
|
|
162
|
+
"""BPA Action entity.
|
|
163
|
+
|
|
164
|
+
An action represents a step or operation in a workflow.
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
id: int = Field(description="Unique action identifier", alias="action_id")
|
|
168
|
+
name: str = Field(description="Action name")
|
|
169
|
+
type: str | None = Field(default=None, description="Action type")
|
|
170
|
+
description: str | None = Field(default=None, description="Action description")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class Form(BPABaseModel):
|
|
174
|
+
"""BPA Form entity.
|
|
175
|
+
|
|
176
|
+
A form is a collection of fields presented as a UI screen.
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
id: int = Field(description="Unique form identifier", alias="form_id")
|
|
180
|
+
name: str = Field(description="Form name")
|
|
181
|
+
description: str | None = Field(default=None, description="Form description")
|
|
182
|
+
service_id: int | None = Field(
|
|
183
|
+
default=None, alias="serviceId", description="Parent service ID"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class PaginatedResponse(BPABaseModel):
|
|
188
|
+
"""Generic paginated response wrapper.
|
|
189
|
+
|
|
190
|
+
Used for API responses that return lists with pagination.
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
items: list[dict[str, Any]] = Field(
|
|
194
|
+
default_factory=list, description="List of items"
|
|
195
|
+
)
|
|
196
|
+
total: int | None = Field(default=None, description="Total number of items")
|
|
197
|
+
page: int | None = Field(default=None, description="Current page number")
|
|
198
|
+
page_size: int | None = Field(
|
|
199
|
+
default=None, alias="pageSize", description="Items per page"
|
|
200
|
+
)
|
|
201
|
+
has_more: bool | None = Field(
|
|
202
|
+
default=None, alias="hasMore", description="Whether more pages exist"
|
|
203
|
+
)
|