Skip to content

Commit f7979f0

Browse files
authored
VED-240: Handle-PDS-Rate-Limit (#1174)
* make external service status code reusable
1 parent 5e22d5d commit f7979f0

16 files changed

Lines changed: 528 additions & 238 deletions

File tree

lambdas/id_sync/src/pds_details.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44

55
import tempfile
66

7-
from common.authentication import AppRestrictedAuth, Service
7+
from common.api_clients.authentication import AppRestrictedAuth, Service
8+
from common.api_clients.pds_service import PdsService
89
from common.cache import Cache
910
from common.clients import get_secrets_manager_client, logger
10-
from common.pds_service import PdsService
1111
from exceptions.id_sync_exception import IdSyncException
1212
from os_vars import get_pds_env
1313

lambdas/mns_subscription/src/mns_setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
import boto3
44
from botocore.config import Config
55

6-
from common.authentication import AppRestrictedAuth, Service
6+
from common.api_clients.authentication import AppRestrictedAuth, Service
7+
from common.api_clients.mns_service import MnsService
78
from common.cache import Cache
8-
from mns_service import MnsService
99

1010
logging.basicConfig(level=logging.INFO)
1111

lambdas/mns_subscription/src/models/errors.py

Lines changed: 0 additions & 99 deletions
This file was deleted.

lambdas/shared/src/common/api_clients/__init__.py

Whitespace-only changes.

lambdas/shared/src/common/authentication.py renamed to lambdas/shared/src/common/api_clients/authentication.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from common.clients import logger
1111
from common.models.errors import UnhandledResponseError
1212

13-
from .cache import Cache
13+
from ..cache import Cache
1414

1515

1616
class Service(Enum):
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""Constants used by API clients"""
2+
3+
4+
class Constants:
5+
"""Constants used for the API clients"""
6+
7+
RETRYABLE_STATUS_CODES = {429, 500, 502, 503, 504}
8+
DEFAULT_API_CLIENTS_TIMEOUT = 5
9+
API_CLIENTS_MAX_RETRIES = 2
10+
API_CLIENTS_BACKOFF_SECONDS = 0.5
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import uuid
2+
from dataclasses import dataclass
3+
4+
from common.clients import logger
5+
from common.models.errors import Code, Severity, create_operation_outcome
6+
7+
8+
@dataclass
9+
class UnauthorizedError(RuntimeError):
10+
response: dict | str
11+
message: str
12+
13+
def __str__(self):
14+
return f"{self.message}\n{self.response}"
15+
16+
@staticmethod
17+
def to_operation_outcome() -> dict:
18+
msg = "Unauthorized request"
19+
return create_operation_outcome(
20+
resource_id=str(uuid.uuid4()),
21+
severity=Severity.error,
22+
code=Code.forbidden,
23+
diagnostics=msg,
24+
)
25+
26+
27+
@dataclass
28+
class TokenValidationError(RuntimeError):
29+
response: dict | str
30+
message: str
31+
32+
def __str__(self):
33+
return f"{self.message}\n{self.response}"
34+
35+
@staticmethod
36+
def to_operation_outcome() -> dict:
37+
msg = "Missing/Invalid Token"
38+
return create_operation_outcome(
39+
resource_id=str(uuid.uuid4()),
40+
severity=Severity.error,
41+
code=Code.invalid,
42+
diagnostics=msg,
43+
)
44+
45+
46+
@dataclass
47+
class ForbiddenError(Exception):
48+
response: dict | str
49+
message: str
50+
51+
def __str__(self):
52+
return f"{self.message}\n{self.response}"
53+
54+
@staticmethod
55+
def to_operation_outcome() -> dict:
56+
msg = "Forbidden"
57+
return create_operation_outcome(
58+
resource_id=str(uuid.uuid4()),
59+
severity=Severity.error,
60+
code=Code.forbidden,
61+
diagnostics=msg,
62+
)
63+
64+
65+
@dataclass
66+
class ConflictError(RuntimeError):
67+
response: dict | str
68+
message: str
69+
70+
def __str__(self):
71+
return f"{self.message}\n{self.response}"
72+
73+
@staticmethod
74+
def to_operation_outcome() -> dict:
75+
msg = "Conflict"
76+
return create_operation_outcome(
77+
resource_id=str(uuid.uuid4()),
78+
severity=Severity.error,
79+
code=Code.duplicate,
80+
diagnostics=msg,
81+
)
82+
83+
84+
@dataclass
85+
class BadRequestError(RuntimeError):
86+
"""Use when payload is missing required parameters"""
87+
88+
response: dict | str
89+
message: str
90+
91+
def __str__(self):
92+
return f"{self.message}\n{self.response}"
93+
94+
def to_operation_outcome(self) -> dict:
95+
return create_operation_outcome(
96+
resource_id=str(uuid.uuid4()),
97+
severity=Severity.error,
98+
code=Code.incomplete,
99+
diagnostics=self.__str__(),
100+
)
101+
102+
103+
@dataclass
104+
class ServerError(RuntimeError):
105+
"""Use when there is a server error"""
106+
107+
response: dict | str
108+
message: str
109+
110+
def __str__(self):
111+
return f"{self.message}\n{self.response}"
112+
113+
def to_operation_outcome(self) -> dict:
114+
return create_operation_outcome(
115+
resource_id=str(uuid.uuid4()),
116+
severity=Severity.error,
117+
code=Code.server_error,
118+
diagnostics=self.__str__(),
119+
)
120+
121+
122+
@dataclass
123+
class ResourceNotFoundError(RuntimeError):
124+
"""Return this error when the requested FHIR resource does not exist"""
125+
126+
response: dict | str
127+
message: str
128+
129+
def __str__(self):
130+
return f"{self.message}\n{self.response}"
131+
132+
def to_operation_outcome(self) -> dict:
133+
return create_operation_outcome(
134+
resource_id=str(uuid.uuid4()),
135+
severity=Severity.error,
136+
code=Code.not_found,
137+
diagnostics=self.__str__(),
138+
)
139+
140+
141+
@dataclass
142+
class UnhandledResponseError(RuntimeError):
143+
"""Use this error when the response from an external service (ex: dynamodb) can't be handled"""
144+
145+
response: dict | str
146+
message: str
147+
148+
def __str__(self):
149+
return f"{self.message}\n{self.response}"
150+
151+
def to_operation_outcome(self) -> dict:
152+
return create_operation_outcome(
153+
resource_id=str(uuid.uuid4()),
154+
severity=Severity.error,
155+
code=Code.exception,
156+
diagnostics=self.__str__(),
157+
)
158+
159+
160+
def raise_error_response(response):
161+
error_mapping = {
162+
401: (TokenValidationError, "Token validation failed for the request"),
163+
400: (BadRequestError, "Bad request"),
164+
403: (ForbiddenError, "Forbidden: You do not have permission to access this resource"),
165+
404: (ResourceNotFoundError, "Resource not found"),
166+
408: (ServerError, "Request Timeout"),
167+
409: (ConflictError, "Conflict: Resource already exists"),
168+
429: (ServerError, "Too Many Requests"),
169+
500: (ServerError, "Internal Server Error"),
170+
502: (ServerError, "Bad Gateway"),
171+
503: (ServerError, "Service Unavailable"),
172+
504: (ServerError, "Gateway Timeout"),
173+
}
174+
175+
exception_class, error_message = error_mapping.get(
176+
response.status_code,
177+
(UnhandledResponseError, f"Unhandled error: {response.status_code}"),
178+
)
179+
180+
logger.info(f"{error_message}. Status={response.status_code}. Body={response.text}")
181+
182+
raise exception_class(response=response.json(), message=error_message)

0 commit comments

Comments
 (0)