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,162 @@
1
+ """Permission enforcement for MCP tools.
2
+
3
+ This module provides guards and utility functions to enforce
4
+ BPA permissions before tool execution. All permission checks
5
+ are server-side and based on JWT token claims.
6
+
7
+ Usage in MCP tools:
8
+ @mcp.tool()
9
+ async def service_create(name: str) -> dict[str, object]:
10
+ # Ensure write permission before proceeding
11
+ access_token = await ensure_write_permission()
12
+ # ... use access_token for BPA API call
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ from typing import TYPE_CHECKING
19
+
20
+ from mcp.server.fastmcp.exceptions import ToolError
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ if TYPE_CHECKING:
25
+ from mcp_eregistrations_bpa.auth.token_manager import TokenManager
26
+
27
+ __all__ = [
28
+ "PERMISSION_VIEWER",
29
+ "PERMISSION_SERVICE_DESIGNER",
30
+ "WRITE_PERMISSIONS",
31
+ "ensure_authenticated",
32
+ "ensure_write_permission",
33
+ "check_permission",
34
+ ]
35
+
36
+ # Permission constants
37
+ PERMISSION_VIEWER = "viewer"
38
+ PERMISSION_SERVICE_DESIGNER = "service_designer"
39
+
40
+ # Roles that allow write operations
41
+ WRITE_PERMISSIONS: list[str] = [PERMISSION_SERVICE_DESIGNER]
42
+
43
+
44
+ def get_token_manager() -> TokenManager:
45
+ """Get the global token manager instance.
46
+
47
+ Uses late import to avoid circular dependency with server.py.
48
+
49
+ Returns:
50
+ The global TokenManager instance.
51
+ """
52
+ from mcp_eregistrations_bpa.server import get_token_manager as _get_tm
53
+
54
+ return _get_tm()
55
+
56
+
57
+ async def ensure_authenticated() -> str:
58
+ """Ensure user is authenticated and return access token.
59
+
60
+ Automatically triggers browser-based login if not authenticated.
61
+ Checks authentication status and token expiry. Calls
62
+ token_manager.get_access_token() which handles automatic
63
+ refresh if the token is near expiry.
64
+
65
+ Returns:
66
+ Valid access token for BPA API calls.
67
+
68
+ Raises:
69
+ ToolError: If authentication fails or token expired and cannot refresh.
70
+ """
71
+ token_manager = get_token_manager()
72
+ logger.debug("Checking authentication status...")
73
+
74
+ # Auto-trigger login if not authenticated
75
+ if not token_manager.is_authenticated():
76
+ logger.info("Not authenticated - initiating browser login flow")
77
+ from mcp_eregistrations_bpa.config import AuthProvider, load_config
78
+ from mcp_eregistrations_bpa.exceptions import AuthenticationError
79
+
80
+ try:
81
+ config = load_config()
82
+ if config.auth_provider == AuthProvider.CAS:
83
+ from mcp_eregistrations_bpa.auth.cas import perform_cas_browser_login
84
+
85
+ result = await perform_cas_browser_login()
86
+ else:
87
+ from mcp_eregistrations_bpa.auth.oidc import perform_browser_login
88
+
89
+ result = await perform_browser_login()
90
+
91
+ if result.get("error"):
92
+ logger.error("Authentication failed: %s", result.get("message"))
93
+ raise ToolError(str(result.get("message", "Authentication failed")))
94
+ logger.info("Authentication successful for user: %s", result.get("user"))
95
+ except AuthenticationError as e:
96
+ logger.error("Authentication error: %s", e)
97
+ raise ToolError(str(e))
98
+
99
+ # Check for expired token (shouldn't happen right after login, but handle it)
100
+ if token_manager.is_token_expired():
101
+ logger.warning("Token expired - user needs to re-authenticate")
102
+ raise ToolError("Session expired. Please run auth_login again.")
103
+
104
+ logger.debug("User authenticated: %s", token_manager.user_email)
105
+ # This will auto-refresh if within 5 minutes of expiry
106
+ return await token_manager.get_access_token()
107
+
108
+
109
+ async def ensure_write_permission() -> str:
110
+ """Ensure user has write permission and return access token.
111
+
112
+ Combines authentication check with write permission verification.
113
+ Checks if user has any role in WRITE_PERMISSIONS.
114
+
115
+ Returns:
116
+ Valid access token for BPA API calls.
117
+
118
+ Raises:
119
+ ToolError: If not authenticated or lacks write permission.
120
+ """
121
+ # First ensure authenticated (raises if not)
122
+ token = await ensure_authenticated()
123
+
124
+ # Check for write permission
125
+ token_manager = get_token_manager()
126
+ user_permissions = set(token_manager.permissions)
127
+
128
+ if not user_permissions.intersection(WRITE_PERMISSIONS):
129
+ raise ToolError(
130
+ "Permission denied: You don't have write access. "
131
+ "Contact your administrator."
132
+ )
133
+
134
+ return token
135
+
136
+
137
+ def check_permission(required_permission: str) -> None:
138
+ """Check if user has a specific permission.
139
+
140
+ This is a synchronous check that also validates authentication status.
141
+ Raises ToolError if not authenticated or if permission is missing.
142
+
143
+ Args:
144
+ required_permission: The permission role required.
145
+
146
+ Raises:
147
+ ToolError: If not authenticated or user lacks the required permission.
148
+ """
149
+ token_manager = get_token_manager()
150
+
151
+ # Verify authentication first (sync check - doesn't refresh)
152
+ if not token_manager.is_authenticated():
153
+ raise ToolError("Not authenticated. Run auth_login first.")
154
+
155
+ if token_manager.is_token_expired():
156
+ raise ToolError("Session expired. Please run auth_login again.")
157
+
158
+ if required_permission not in token_manager.permissions:
159
+ raise ToolError(
160
+ f"Permission denied: You need '{required_permission}' permission. "
161
+ "Contact your administrator."
162
+ )
@@ -0,0 +1,348 @@
1
+ """In-memory token storage and automatic refresh.
2
+
3
+ This module handles token lifecycle management including storage,
4
+ refresh, and expiry checks. Tokens are stored in memory only
5
+ (never persisted to disk) per NFR2 and NFR6.
6
+ """
7
+
8
+ import base64
9
+ import json
10
+ from datetime import UTC, datetime, timedelta
11
+
12
+ import httpx
13
+ from pydantic import BaseModel
14
+
15
+ from mcp_eregistrations_bpa.exceptions import AuthenticationError
16
+
17
+ # Refresh tokens 5 minutes before expiry (NFR3)
18
+ REFRESH_THRESHOLD_SECONDS = 300
19
+
20
+
21
+ class TokenResponse(BaseModel):
22
+ """Response from token endpoint."""
23
+
24
+ access_token: str
25
+ refresh_token: str | None = None
26
+ expires_in: int
27
+ token_type: str = "Bearer"
28
+
29
+
30
+ class TokenManager:
31
+ """In-memory token storage with automatic refresh.
32
+
33
+ Tokens are stored in memory only and never persisted to disk.
34
+ Automatic refresh is triggered when the access token is within
35
+ 5 minutes of expiry (NFR3).
36
+ """
37
+
38
+ def __init__(self) -> None:
39
+ """Initialize token manager with empty state."""
40
+ self._access_token: str | None = None
41
+ self._refresh_token: str | None = None
42
+ self._expires_at: datetime | None = None
43
+ self._token_endpoint: str | None = None
44
+ self._client_id: str | None = None
45
+ self._cas_client_secret: str | None = None # CAS uses Basic Auth, no PKCE
46
+ self._user_email: str | None = None
47
+ self._permissions: list[str] = []
48
+
49
+ @property
50
+ def user_email(self) -> str | None:
51
+ """Get the authenticated user's email."""
52
+ return self._user_email
53
+
54
+ @property
55
+ def expires_in_minutes(self) -> int:
56
+ """Get remaining session time in minutes."""
57
+ if self._expires_at is None:
58
+ return 0
59
+ remaining = self._expires_at - datetime.now(UTC)
60
+ return max(0, int(remaining.total_seconds() / 60))
61
+
62
+ @property
63
+ def permissions(self) -> list[str]:
64
+ """Get the authenticated user's permissions/roles.
65
+
66
+ Returns a copy to prevent external mutation of internal state.
67
+ """
68
+ return list(self._permissions)
69
+
70
+ def is_token_expired(self) -> bool:
71
+ """Check if a stored token has already expired (without triggering refresh).
72
+
73
+ This method should only be called after verifying is_authenticated() is True.
74
+ When no token exists (_expires_at is None), returns False because there's
75
+ nothing to consider "expired" - use is_authenticated() to check presence.
76
+
77
+ Returns:
78
+ True if token exists AND is expired, False otherwise.
79
+ """
80
+ if self._expires_at is None:
81
+ return False
82
+ return datetime.now(UTC) >= self._expires_at
83
+
84
+ def is_authenticated(self) -> bool:
85
+ """Check if we have valid tokens.
86
+
87
+ Returns:
88
+ True if we have tokens (may need refresh), False if not authenticated.
89
+ """
90
+ return self._access_token is not None
91
+
92
+ def store_tokens(
93
+ self,
94
+ access_token: str,
95
+ refresh_token: str | None,
96
+ expires_in: int,
97
+ token_endpoint: str | None = None,
98
+ client_id: str | None = None,
99
+ ) -> None:
100
+ """Store tokens in memory.
101
+
102
+ Args:
103
+ access_token: The access token from Keycloak.
104
+ refresh_token: The refresh token from Keycloak.
105
+ expires_in: Token lifetime in seconds.
106
+ token_endpoint: The token endpoint for refresh (optional).
107
+ client_id: The client ID for refresh (optional).
108
+ """
109
+ self._access_token = access_token
110
+ self._refresh_token = refresh_token
111
+ self._expires_at = datetime.now(UTC) + timedelta(seconds=expires_in)
112
+
113
+ if token_endpoint:
114
+ self._token_endpoint = token_endpoint
115
+ if client_id:
116
+ self._client_id = client_id
117
+
118
+ # Extract user email and permissions from access token
119
+ self._user_email = self._extract_email_from_token(access_token)
120
+ self._permissions = self._extract_permissions_from_token(access_token)
121
+
122
+ def clear_tokens(self) -> None:
123
+ """Clear all stored tokens (logout)."""
124
+ self._access_token = None
125
+ self._refresh_token = None
126
+ self._expires_at = None
127
+ self._user_email = None
128
+ self._permissions = []
129
+
130
+ def _needs_refresh(self) -> bool:
131
+ """Check if token needs refresh (within 5 minutes of expiry).
132
+
133
+ Returns:
134
+ True if token should be refreshed, False otherwise.
135
+ """
136
+ if self._expires_at is None:
137
+ return False
138
+ threshold = datetime.now(UTC) + timedelta(seconds=REFRESH_THRESHOLD_SECONDS)
139
+ return self._expires_at <= threshold
140
+
141
+ async def get_access_token(self) -> str:
142
+ """Get valid access token, refreshing if needed.
143
+
144
+ Returns:
145
+ A valid access token.
146
+
147
+ Raises:
148
+ AuthenticationError: If not authenticated or refresh fails.
149
+ """
150
+ if self._access_token is None:
151
+ raise AuthenticationError("Not authenticated. Run auth_login first.")
152
+
153
+ if self._needs_refresh():
154
+ await self._refresh()
155
+
156
+ if self._access_token is None:
157
+ raise AuthenticationError("Session expired. Please run auth_login again.")
158
+
159
+ return self._access_token
160
+
161
+ async def _refresh(self) -> None:
162
+ """Refresh the access token using refresh token.
163
+
164
+ Raises:
165
+ AuthenticationError: If refresh fails.
166
+ """
167
+ if self._refresh_token is None:
168
+ raise AuthenticationError("Session expired. Please run auth_login again.")
169
+
170
+ if self._token_endpoint is None or self._client_id is None:
171
+ raise AuthenticationError("Session expired. Please run auth_login again.")
172
+
173
+ async with httpx.AsyncClient() as client:
174
+ try:
175
+ response = await client.post(
176
+ self._token_endpoint,
177
+ data={
178
+ "grant_type": "refresh_token",
179
+ "refresh_token": self._refresh_token,
180
+ "client_id": self._client_id,
181
+ },
182
+ timeout=10.0,
183
+ )
184
+ response.raise_for_status()
185
+ except httpx.HTTPStatusError as e:
186
+ # Refresh token is invalid/expired
187
+ self.clear_tokens()
188
+ raise AuthenticationError(
189
+ "Session expired. Please run auth_login again."
190
+ ) from e
191
+ except httpx.RequestError as e:
192
+ raise AuthenticationError(
193
+ f"Cannot refresh session: {e}. Please try again."
194
+ ) from e
195
+
196
+ data = response.json()
197
+ self.store_tokens(
198
+ access_token=data["access_token"],
199
+ refresh_token=data.get("refresh_token", self._refresh_token),
200
+ expires_in=data["expires_in"],
201
+ )
202
+
203
+ def _decode_jwt_payload(self, token: str) -> dict[str, object] | None:
204
+ """Decode and return JWT payload claims.
205
+
206
+ Args:
207
+ token: The JWT access token.
208
+
209
+ Returns:
210
+ Decoded claims dict, or None if decoding fails.
211
+ """
212
+ try:
213
+ # JWT is three base64-encoded parts separated by dots
214
+ parts = token.split(".")
215
+ if len(parts) != 3:
216
+ return None
217
+
218
+ # Decode the payload (second part)
219
+ payload = parts[1]
220
+ # Add padding if needed
221
+ padding = 4 - len(payload) % 4
222
+ if padding != 4:
223
+ payload += "=" * padding
224
+
225
+ decoded = base64.urlsafe_b64decode(payload)
226
+ claims: dict[str, object] = json.loads(decoded)
227
+ return claims
228
+ except Exception:
229
+ return None
230
+
231
+ def _extract_email_from_token(self, token: str) -> str | None:
232
+ """Extract user email from JWT access token.
233
+
234
+ Args:
235
+ token: The JWT access token.
236
+
237
+ Returns:
238
+ The user email if found, None otherwise.
239
+ """
240
+ claims = self._decode_jwt_payload(token)
241
+ if claims is None:
242
+ return None
243
+
244
+ # Try common email claim names
245
+ email = claims.get("email") or claims.get("preferred_username")
246
+ return str(email) if email else None
247
+
248
+ def _extract_permissions_from_token(self, token: str) -> list[str]:
249
+ """Extract permissions/roles from JWT access token.
250
+
251
+ Parses Keycloak standard claims for realm and resource access roles.
252
+
253
+ Args:
254
+ token: The JWT access token.
255
+
256
+ Returns:
257
+ List of role names from realm_access and resource_access claims.
258
+ """
259
+ claims = self._decode_jwt_payload(token)
260
+ if claims is None:
261
+ return []
262
+
263
+ roles: list[str] = []
264
+
265
+ # Extract realm roles (Keycloak standard)
266
+ realm_access = claims.get("realm_access")
267
+ if isinstance(realm_access, dict) and "roles" in realm_access:
268
+ roles.extend(realm_access["roles"])
269
+
270
+ # Extract client-specific roles (Keycloak resource_access)
271
+ resource_access = claims.get("resource_access")
272
+ if isinstance(resource_access, dict):
273
+ for _client, access in resource_access.items():
274
+ if isinstance(access, dict) and "roles" in access:
275
+ roles.extend(access["roles"])
276
+
277
+ # Deduplicate while preserving order
278
+ seen: set[str] = set()
279
+ unique_roles: list[str] = []
280
+ for role in roles:
281
+ if role not in seen:
282
+ seen.add(role)
283
+ unique_roles.append(role)
284
+
285
+ return unique_roles
286
+
287
+
288
+ async def exchange_code_for_tokens(
289
+ token_endpoint: str,
290
+ code: str,
291
+ code_verifier: str,
292
+ redirect_uri: str,
293
+ client_id: str,
294
+ ) -> TokenResponse:
295
+ """Exchange authorization code for tokens.
296
+
297
+ Args:
298
+ token_endpoint: The Keycloak token endpoint.
299
+ code: The authorization code from callback.
300
+ code_verifier: The PKCE code verifier.
301
+ redirect_uri: The redirect URI used in authorization.
302
+ client_id: The OIDC client ID.
303
+
304
+ Returns:
305
+ TokenResponse with access and refresh tokens.
306
+
307
+ Raises:
308
+ AuthenticationError: If token exchange fails.
309
+ """
310
+ async with httpx.AsyncClient() as client:
311
+ try:
312
+ response = await client.post(
313
+ token_endpoint,
314
+ data={
315
+ "grant_type": "authorization_code",
316
+ "code": code,
317
+ "redirect_uri": redirect_uri,
318
+ "client_id": client_id,
319
+ "code_verifier": code_verifier,
320
+ },
321
+ timeout=10.0,
322
+ )
323
+ response.raise_for_status()
324
+ except httpx.HTTPStatusError as e:
325
+ error_detail = ""
326
+ try:
327
+ error_data = e.response.json()
328
+ error_detail = error_data.get(
329
+ "error_description", error_data.get("error", "")
330
+ )
331
+ except Exception:
332
+ pass
333
+ raise AuthenticationError(
334
+ f"Authentication failed: {error_detail or 'Token exchange failed'}. "
335
+ "Please try again."
336
+ ) from e
337
+ except httpx.RequestError as e:
338
+ raise AuthenticationError(
339
+ f"Authentication failed: {e}. Please try again."
340
+ ) from e
341
+
342
+ data = response.json()
343
+ return TokenResponse(
344
+ access_token=data["access_token"],
345
+ refresh_token=data.get("refresh_token"),
346
+ expires_in=data["expires_in"],
347
+ token_type=data.get("token_type", "Bearer"),
348
+ )
@@ -0,0 +1,84 @@
1
+ """BPA API client module.
2
+
3
+ This module provides the BPAClient for interacting with the BPA REST API.
4
+
5
+ Main exports:
6
+ BPAClient: Async HTTP client with retry logic and auth integration.
7
+
8
+ Example:
9
+ from mcp_eregistrations_bpa.bpa_client import BPAClient
10
+
11
+ async with BPAClient() as client:
12
+ services = await client.get("/service")
13
+ service = await client.get("/service/{id}", path_params={"id": 123})
14
+ """
15
+
16
+ from mcp_eregistrations_bpa.bpa_client.client import (
17
+ BASE_DELAY,
18
+ DEFAULT_TIMEOUT,
19
+ MAX_DELAY,
20
+ MAX_RETRIES,
21
+ NON_RETRYABLE_STATUS_CODES,
22
+ RETRYABLE_STATUS_CODES,
23
+ BPAClient,
24
+ )
25
+ from mcp_eregistrations_bpa.bpa_client.errors import (
26
+ BPAAuthenticationError,
27
+ BPAClientError,
28
+ BPAConnectionError,
29
+ BPANotFoundError,
30
+ BPAPermissionError,
31
+ BPARateLimitError,
32
+ BPAServerError,
33
+ BPATimeoutError,
34
+ BPAValidationError,
35
+ translate_error,
36
+ )
37
+ from mcp_eregistrations_bpa.bpa_client.models import (
38
+ Action,
39
+ BPABaseModel,
40
+ Cost,
41
+ Determinant,
42
+ Document,
43
+ Form,
44
+ FormField,
45
+ PaginatedResponse,
46
+ Registration,
47
+ Role,
48
+ Service,
49
+ )
50
+
51
+ __all__ = [
52
+ # Client
53
+ "BPAClient",
54
+ # Client constants
55
+ "MAX_RETRIES",
56
+ "BASE_DELAY",
57
+ "MAX_DELAY",
58
+ "DEFAULT_TIMEOUT",
59
+ "RETRYABLE_STATUS_CODES",
60
+ "NON_RETRYABLE_STATUS_CODES",
61
+ # Errors
62
+ "BPAClientError",
63
+ "BPAConnectionError",
64
+ "BPATimeoutError",
65
+ "BPAAuthenticationError",
66
+ "BPAPermissionError",
67
+ "BPANotFoundError",
68
+ "BPAValidationError",
69
+ "BPARateLimitError",
70
+ "BPAServerError",
71
+ "translate_error",
72
+ # Models
73
+ "BPABaseModel",
74
+ "Service",
75
+ "Registration",
76
+ "FormField",
77
+ "Determinant",
78
+ "Role",
79
+ "Cost",
80
+ "Document",
81
+ "Action",
82
+ "Form",
83
+ "PaginatedResponse",
84
+ ]