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.

Files changed (66) hide show
  1. mcp_eregistrations_bpa/__init__.py +121 -0
  2. mcp_eregistrations_bpa/__main__.py +6 -0
  3. mcp_eregistrations_bpa/arazzo/__init__.py +21 -0
  4. mcp_eregistrations_bpa/arazzo/expression.py +379 -0
  5. mcp_eregistrations_bpa/audit/__init__.py +56 -0
  6. mcp_eregistrations_bpa/audit/context.py +66 -0
  7. mcp_eregistrations_bpa/audit/logger.py +236 -0
  8. mcp_eregistrations_bpa/audit/models.py +131 -0
  9. mcp_eregistrations_bpa/auth/__init__.py +64 -0
  10. mcp_eregistrations_bpa/auth/callback.py +391 -0
  11. mcp_eregistrations_bpa/auth/cas.py +409 -0
  12. mcp_eregistrations_bpa/auth/oidc.py +252 -0
  13. mcp_eregistrations_bpa/auth/permissions.py +162 -0
  14. mcp_eregistrations_bpa/auth/token_manager.py +348 -0
  15. mcp_eregistrations_bpa/bpa_client/__init__.py +84 -0
  16. mcp_eregistrations_bpa/bpa_client/client.py +740 -0
  17. mcp_eregistrations_bpa/bpa_client/endpoints.py +193 -0
  18. mcp_eregistrations_bpa/bpa_client/errors.py +276 -0
  19. mcp_eregistrations_bpa/bpa_client/models.py +203 -0
  20. mcp_eregistrations_bpa/config.py +349 -0
  21. mcp_eregistrations_bpa/db/__init__.py +21 -0
  22. mcp_eregistrations_bpa/db/connection.py +64 -0
  23. mcp_eregistrations_bpa/db/migrations.py +168 -0
  24. mcp_eregistrations_bpa/exceptions.py +39 -0
  25. mcp_eregistrations_bpa/py.typed +0 -0
  26. mcp_eregistrations_bpa/rollback/__init__.py +19 -0
  27. mcp_eregistrations_bpa/rollback/manager.py +616 -0
  28. mcp_eregistrations_bpa/server.py +152 -0
  29. mcp_eregistrations_bpa/tools/__init__.py +372 -0
  30. mcp_eregistrations_bpa/tools/actions.py +155 -0
  31. mcp_eregistrations_bpa/tools/analysis.py +352 -0
  32. mcp_eregistrations_bpa/tools/audit.py +399 -0
  33. mcp_eregistrations_bpa/tools/behaviours.py +1042 -0
  34. mcp_eregistrations_bpa/tools/bots.py +627 -0
  35. mcp_eregistrations_bpa/tools/classifications.py +575 -0
  36. mcp_eregistrations_bpa/tools/costs.py +765 -0
  37. mcp_eregistrations_bpa/tools/debug_strategies.py +351 -0
  38. mcp_eregistrations_bpa/tools/debugger.py +1230 -0
  39. mcp_eregistrations_bpa/tools/determinants.py +2235 -0
  40. mcp_eregistrations_bpa/tools/document_requirements.py +670 -0
  41. mcp_eregistrations_bpa/tools/export.py +899 -0
  42. mcp_eregistrations_bpa/tools/fields.py +162 -0
  43. mcp_eregistrations_bpa/tools/form_errors.py +36 -0
  44. mcp_eregistrations_bpa/tools/formio_helpers.py +971 -0
  45. mcp_eregistrations_bpa/tools/forms.py +1269 -0
  46. mcp_eregistrations_bpa/tools/jsonlogic_builder.py +466 -0
  47. mcp_eregistrations_bpa/tools/large_response.py +163 -0
  48. mcp_eregistrations_bpa/tools/messages.py +523 -0
  49. mcp_eregistrations_bpa/tools/notifications.py +241 -0
  50. mcp_eregistrations_bpa/tools/registration_institutions.py +680 -0
  51. mcp_eregistrations_bpa/tools/registrations.py +897 -0
  52. mcp_eregistrations_bpa/tools/role_status.py +447 -0
  53. mcp_eregistrations_bpa/tools/role_units.py +400 -0
  54. mcp_eregistrations_bpa/tools/roles.py +1236 -0
  55. mcp_eregistrations_bpa/tools/rollback.py +335 -0
  56. mcp_eregistrations_bpa/tools/services.py +674 -0
  57. mcp_eregistrations_bpa/tools/workflows.py +2487 -0
  58. mcp_eregistrations_bpa/tools/yaml_transformer.py +991 -0
  59. mcp_eregistrations_bpa/workflows/__init__.py +28 -0
  60. mcp_eregistrations_bpa/workflows/loader.py +440 -0
  61. mcp_eregistrations_bpa/workflows/models.py +336 -0
  62. mcp_eregistrations_bpa-0.8.5.dist-info/METADATA +965 -0
  63. mcp_eregistrations_bpa-0.8.5.dist-info/RECORD +66 -0
  64. mcp_eregistrations_bpa-0.8.5.dist-info/WHEEL +4 -0
  65. mcp_eregistrations_bpa-0.8.5.dist-info/entry_points.txt +2 -0
  66. 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
+ )