api-to-dataframe 1.5.1__tar.gz → 2.0.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: api-to-dataframe
3
- Version: 1.5.1
3
+ Version: 2.0.0
4
4
  Summary: A package to convert API responses to pandas dataframe
5
5
  License: MIT
6
6
  Author: IvanildoBarauna
@@ -15,7 +15,6 @@ Classifier: Programming Language :: Python :: 3.10
15
15
  Classifier: Programming Language :: Python :: 3.11
16
16
  Classifier: Programming Language :: Python :: 3.8
17
17
  Requires-Dist: logging (>=0.4.9.6,<0.5.0.0)
18
- Requires-Dist: otel-wrapper (>=0.1.1,<0.2.0)
19
18
  Requires-Dist: pandas (>=2.2.3,<3.0.0)
20
19
  Requires-Dist: requests (>=2.32.3,<3.0.0)
21
20
  Project-URL: Documentation, https://github.com/IvanildoBarauna/api-to-dataframe/blob/main/README.md
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "api-to-dataframe"
3
- version = "1.5.1"
3
+ version = "2.0.0"
4
4
  description = "A package to convert API responses to pandas dataframe"
5
5
  authors = ["IvanildoBarauna <ivanildo.jnr@outlook.com>"]
6
6
  readme = "README.md"
@@ -27,7 +27,6 @@ python = "^3.9"
27
27
  pandas = "^2.2.3"
28
28
  requests = "^2.32.3"
29
29
  logging = "^0.4.9.6"
30
- otel-wrapper = "^0.1.1"
31
30
 
32
31
  [tool.poetry.group.dev.dependencies]
33
32
  poetry-dynamic-versioning = "^1.3.0"
@@ -0,0 +1,95 @@
1
+ from api_to_dataframe.models.retainer import retry_strategies, Strategies
2
+ from api_to_dataframe.models.get_data import GetData
3
+ from api_to_dataframe.utils.logger import logger
4
+
5
+
6
+ class ClientBuilder:
7
+ def __init__( # pylint: disable=too-many-positional-arguments,too-many-arguments
8
+ self,
9
+ endpoint: str,
10
+ headers: dict = None,
11
+ retry_strategy: Strategies = Strategies.NO_RETRY_STRATEGY,
12
+ retries: int = 3,
13
+ initial_delay: int = 1,
14
+ connection_timeout: int = 1,
15
+ ):
16
+ """
17
+ Initializes the ClientBuilder object.
18
+
19
+ Args:
20
+ endpoint (str): The API endpoint to connect to.
21
+ headers (dict, optional): The headers to use for the API request. Defaults to None.
22
+ retry_strategy (Strategies, optional): Defaults to Strategies.NO_RETRY_STRATEGY.
23
+ retries (int): The number of times to retry a failed request. Defaults to 3.
24
+ initial_delay (int): The delay between retries in seconds. Defaults to 1.
25
+ connection_timeout (int): The timeout for the connection in seconds. Defaults to 1.
26
+
27
+ Raises:
28
+ ValueError: If endpoint is an empty string.
29
+ ValueError: If retries is not a non-negative integer.
30
+ ValueError: If delay is not a non-negative integer.
31
+ ValueError: If connection_timeout is not a non-negative integer.
32
+ """
33
+
34
+ if headers is None:
35
+ headers = {}
36
+ if endpoint == "":
37
+ error_msg = "endpoint cannot be an empty string"
38
+ logger.error(error_msg)
39
+ raise ValueError
40
+ if not isinstance(retries, int) or retries < 0:
41
+ error_msg = "retries must be a non-negative integer"
42
+ logger.error(error_msg)
43
+ raise ValueError
44
+ if not isinstance(initial_delay, int) or initial_delay < 0:
45
+ error_msg = "initial_delay must be a non-negative integer"
46
+ logger.error(error_msg)
47
+ raise ValueError
48
+ if not isinstance(connection_timeout, int) or connection_timeout < 0:
49
+ error_msg = "connection_timeout must be a non-negative integer"
50
+ logger.error(error_msg)
51
+ raise ValueError
52
+
53
+ self.endpoint = endpoint
54
+ self.retry_strategy = retry_strategy
55
+ self.connection_timeout = connection_timeout
56
+ self.headers = headers
57
+ self.retries = retries
58
+ self.delay = initial_delay
59
+
60
+ @retry_strategies
61
+ def get_api_data(self):
62
+ """
63
+ Retrieves data from the API using the defined endpoint and retry strategy.
64
+
65
+ This function sends a request to the API using the endpoint, headers, and
66
+ connection timeout specified in the instance attributes. It uses the
67
+ defined retry strategy to handle potential failures and retries.
68
+
69
+ Returns:
70
+ dict: The JSON response from the API as a dictionary.
71
+ """
72
+ response = GetData.get_response(
73
+ endpoint=self.endpoint,
74
+ headers=self.headers,
75
+ connection_timeout=self.connection_timeout,
76
+ )
77
+
78
+ return response.json()
79
+
80
+ @staticmethod
81
+ def api_to_dataframe(response: dict):
82
+ """
83
+ Converts an API response to a DataFrame.
84
+
85
+ This function takes a dictionary response from an API,
86
+ uses the `to_dataframe` function from the `GetData` class
87
+ to convert it into a DataFrame, and logs the operation as successful.
88
+
89
+ Args:
90
+ response (dict): The dictionary containing the API response.
91
+
92
+ Returns:
93
+ DataFrame: A pandas DataFrame containing the data from the API response.
94
+ """
95
+ return GetData.to_dataframe(response)
@@ -0,0 +1,27 @@
1
+ import requests
2
+ import pandas as pd
3
+ from api_to_dataframe.utils.logger import logger
4
+
5
+
6
+ class GetData:
7
+ @staticmethod
8
+ def get_response(endpoint: str, headers: dict, connection_timeout: int):
9
+ # Make the request
10
+ response = requests.get(endpoint, timeout=connection_timeout, headers=headers)
11
+
12
+ # Attempt to raise for status to catch errors
13
+ response.raise_for_status()
14
+
15
+ return response
16
+
17
+ @staticmethod
18
+ def to_dataframe(response):
19
+ df = pd.DataFrame(response)
20
+
21
+ # Check if DataFrame is empty
22
+ if df.empty:
23
+ error_msg = "::: DataFrame is empty :::"
24
+ logger.error(error_msg)
25
+ raise ValueError(error_msg)
26
+
27
+ return df
@@ -0,0 +1,39 @@
1
+ import time
2
+ from enum import Enum
3
+
4
+ from requests.exceptions import RequestException
5
+ from api_to_dataframe.utils.logger import logger
6
+ from api_to_dataframe.utils import Constants
7
+
8
+
9
+ class Strategies(Enum):
10
+ NO_RETRY_STRATEGY = 0
11
+ LINEAR_RETRY_STRATEGY = 1
12
+ EXPONENTIAL_RETRY_STRATEGY = 2
13
+
14
+
15
+ def retry_strategies(func):
16
+ def wrapper(*args, **kwargs): # pylint: disable=inconsistent-return-statements
17
+ retry_number = 0
18
+ while retry_number < args[0].retries:
19
+ try:
20
+ if retry_number > 0:
21
+ logger.info(
22
+ f"Trying for the {retry_number} of {Constants.MAX_OF_RETRIES} retries. Using {args[0].retry_strategy}"
23
+ )
24
+ return func(*args, **kwargs)
25
+ except RequestException as e:
26
+ retry_number += 1
27
+
28
+ if args[0].retry_strategy == Strategies.NO_RETRY_STRATEGY:
29
+ raise e
30
+ if args[0].retry_strategy == Strategies.LINEAR_RETRY_STRATEGY:
31
+ time.sleep(args[0].delay)
32
+ elif args[0].retry_strategy == Strategies.EXPONENTIAL_RETRY_STRATEGY:
33
+ time.sleep(args[0].delay * retry_number)
34
+
35
+ if retry_number in (args[0].retries, Constants.MAX_OF_RETRIES):
36
+ logger.error(f"Failed after {retry_number} retries")
37
+ raise e
38
+
39
+ return wrapper
@@ -1,7 +1,5 @@
1
1
  import logging
2
- from otel_wrapper.deps_injector import wrapper_builder
3
2
 
4
- # Configure logging once at the start of your program
5
3
  logging.basicConfig(
6
4
  encoding="utf-8",
7
5
  format="%(asctime)s :: api-to-dataframe[%(levelname)s] :: %(message)s",
@@ -11,6 +9,3 @@ logging.basicConfig(
11
9
 
12
10
  # Initialize traditional logger
13
11
  logger = logging.getLogger("api-to-dataframe")
14
-
15
- # Initialize OpenTelemetry wrapper
16
- telemetry = wrapper_builder("api-to-dataframe")
@@ -1,251 +0,0 @@
1
- from api_to_dataframe.models.retainer import retry_strategies, Strategies
2
- from api_to_dataframe.models.get_data import GetData
3
- from api_to_dataframe.utils.logger import logger, telemetry
4
- import time
5
-
6
-
7
- class ClientBuilder:
8
- def __init__( # pylint: disable=too-many-positional-arguments,too-many-arguments
9
- self,
10
- endpoint: str,
11
- headers: dict = None,
12
- retry_strategy: Strategies = Strategies.NO_RETRY_STRATEGY,
13
- retries: int = 3,
14
- initial_delay: int = 1,
15
- connection_timeout: int = 1,
16
- ):
17
- """
18
- Initializes the ClientBuilder object.
19
-
20
- Args:
21
- endpoint (str): The API endpoint to connect to.
22
- headers (dict, optional): The headers to use for the API request. Defaults to None.
23
- retry_strategy (Strategies, optional): Defaults to Strategies.NO_RETRY_STRATEGY.
24
- retries (int): The number of times to retry a failed request. Defaults to 3.
25
- initial_delay (int): The delay between retries in seconds. Defaults to 1.
26
- connection_timeout (int): The timeout for the connection in seconds. Defaults to 1.
27
-
28
- Raises:
29
- ValueError: If endpoint is an empty string.
30
- ValueError: If retries is not a non-negative integer.
31
- ValueError: If delay is not a non-negative integer.
32
- ValueError: If connection_timeout is not a non-negative integer.
33
- """
34
-
35
- if headers is None:
36
- headers = {}
37
- if endpoint == "":
38
- error_msg = "endpoint cannot be an empty string"
39
- logger.error(error_msg)
40
- telemetry.logs().new_log(
41
- msg=error_msg,
42
- tags={"component": "ClientBuilder", "method": "__init__"},
43
- level=40 # ERROR level
44
- )
45
- raise ValueError
46
- if not isinstance(retries, int) or retries < 0:
47
- error_msg = "retries must be a non-negative integer"
48
- logger.error(error_msg)
49
- telemetry.logs().new_log(
50
- msg=error_msg,
51
- tags={"component": "ClientBuilder", "method": "__init__"},
52
- level=40 # ERROR level
53
- )
54
- raise ValueError
55
- if not isinstance(initial_delay, int) or initial_delay < 0:
56
- error_msg = "initial_delay must be a non-negative integer"
57
- logger.error(error_msg)
58
- telemetry.logs().new_log(
59
- msg=error_msg,
60
- tags={"component": "ClientBuilder", "method": "__init__"},
61
- level=40 # ERROR level
62
- )
63
- raise ValueError
64
- if not isinstance(connection_timeout, int) or connection_timeout < 0:
65
- error_msg = "connection_timeout must be a non-negative integer"
66
- logger.error(error_msg)
67
- telemetry.logs().new_log(
68
- msg=error_msg,
69
- tags={"component": "ClientBuilder", "method": "__init__"},
70
- level=40 # ERROR level
71
- )
72
- raise ValueError
73
-
74
- self.endpoint = endpoint
75
- self.retry_strategy = retry_strategy
76
- self.connection_timeout = connection_timeout
77
- self.headers = headers
78
- self.retries = retries
79
- self.delay = initial_delay
80
-
81
- # Record client initialization metric
82
- telemetry.metrics().metric_increment(
83
- name="client.initialization",
84
- tags={
85
- "endpoint": endpoint,
86
- "retry_strategy": retry_strategy.name,
87
- "connection_timeout": str(connection_timeout)
88
- }
89
- )
90
-
91
- # Log initialization
92
- telemetry.logs().new_log(
93
- msg=f"ClientBuilder initialized with endpoint {endpoint}",
94
- tags={
95
- "endpoint": endpoint,
96
- "retry_strategy": retry_strategy.name,
97
- "connection_timeout": str(connection_timeout),
98
- "component": "ClientBuilder"
99
- },
100
- level=20 # INFO level
101
- )
102
-
103
- @retry_strategies
104
- def get_api_data(self):
105
- """
106
- Retrieves data from the API using the defined endpoint and retry strategy.
107
-
108
- This function sends a request to the API using the endpoint, headers, and
109
- connection timeout specified in the instance attributes. It uses the
110
- defined retry strategy to handle potential failures and retries.
111
-
112
- Returns:
113
- dict: The JSON response from the API as a dictionary.
114
- """
115
- # Use the telemetry spans with context manager
116
- with telemetry.traces().span_in_context("get_api_data") as (span, _):
117
- # Add span attributes
118
- span.set_attribute("endpoint", self.endpoint)
119
- span.set_attribute("retry_strategy", self.retry_strategy.name)
120
- span.set_attribute("connection_timeout", self.connection_timeout)
121
-
122
- # Log the API request
123
- telemetry.logs().new_log(
124
- msg=f"Making API request to {self.endpoint}",
125
- tags={
126
- "endpoint": self.endpoint,
127
- "component": "ClientBuilder",
128
- "method": "get_api_data"
129
- },
130
- level=20 # INFO level
131
- )
132
-
133
- # Record the start time for response time measurement
134
- start_time = time.time()
135
-
136
- # Make the API request
137
- response = GetData.get_response(
138
- endpoint=self.endpoint,
139
- headers=self.headers,
140
- connection_timeout=self.connection_timeout,
141
- )
142
-
143
- # Calculate response time
144
- response_time = time.time() - start_time
145
-
146
- # Record response time as histogram
147
- telemetry.metrics().record_histogram(
148
- name="api.response_time",
149
- tags={"endpoint": self.endpoint},
150
- value=response_time
151
- )
152
-
153
- # Record successful request metric
154
- telemetry.metrics().metric_increment(
155
- name="api.request.success",
156
- tags={"endpoint": self.endpoint}
157
- )
158
-
159
- # Log success
160
- telemetry.logs().new_log(
161
- msg=f"API request to {self.endpoint} successful",
162
- tags={
163
- "endpoint": self.endpoint,
164
- "response_status": response.status_code,
165
- "response_time": response_time,
166
- "component": "ClientBuilder",
167
- "method": "get_api_data"
168
- },
169
- level=20 # INFO level
170
- )
171
-
172
- return response.json()
173
-
174
- @staticmethod
175
- def api_to_dataframe(response: dict):
176
- """
177
- Converts an API response to a DataFrame.
178
-
179
- This function takes a dictionary response from an API,
180
- uses the `to_dataframe` function from the `GetData` class
181
- to convert it into a DataFrame, and logs the operation as successful.
182
-
183
- Args:
184
- response (dict): The dictionary containing the API response.
185
-
186
- Returns:
187
- DataFrame: A pandas DataFrame containing the data from the API response.
188
- """
189
- # Use telemetry for this operation
190
- with telemetry.traces().span_in_context("api_to_dataframe") as (span, _):
191
- response_size = len(response) if isinstance(response, list) else 1
192
- span.set_attribute("response_size", response_size)
193
-
194
- # Log conversion start
195
- telemetry.logs().new_log(
196
- msg="Converting API response to DataFrame",
197
- tags={
198
- "response_size": response_size,
199
- "response_type": type(response).__name__,
200
- "component": "ClientBuilder",
201
- "method": "api_to_dataframe"
202
- },
203
- level=20 # INFO level
204
- )
205
-
206
- try:
207
- # Convert to dataframe
208
- df = GetData.to_dataframe(response)
209
-
210
- # Record metrics
211
- telemetry.metrics().metric_increment(
212
- name="dataframe.conversion.success",
213
- tags={"size": len(df)}
214
- )
215
-
216
- # Log success
217
- telemetry.logs().new_log(
218
- msg="Successfully converted API response to DataFrame",
219
- tags={
220
- "dataframe_rows": len(df),
221
- "dataframe_columns": len(df.columns),
222
- "component": "ClientBuilder",
223
- "method": "api_to_dataframe"
224
- },
225
- level=20 # INFO level
226
- )
227
-
228
- return df
229
-
230
- except Exception as e:
231
- # Record failure metric
232
- telemetry.metrics().metric_increment(
233
- name="dataframe.conversion.failure",
234
- tags={"error_type": type(e).__name__}
235
- )
236
-
237
- # Log error
238
- error_msg = f"Failed to convert API response to DataFrame: {str(e)}"
239
- telemetry.logs().new_log(
240
- msg=error_msg,
241
- tags={
242
- "error": str(e),
243
- "error_type": type(e).__name__,
244
- "component": "ClientBuilder",
245
- "method": "api_to_dataframe"
246
- },
247
- level=40 # ERROR level
248
- )
249
-
250
- # Re-raise the exception
251
- raise
@@ -1,215 +0,0 @@
1
- import requests
2
- import pandas as pd
3
- from api_to_dataframe.utils.logger import logger, telemetry
4
-
5
-
6
- class GetData:
7
- @staticmethod
8
- def get_response(endpoint: str, headers: dict, connection_timeout: int):
9
- # Start a span for the API request
10
- with telemetry.traces().span_in_context("http_request") as (span, _):
11
- span.set_attribute("http.url", endpoint)
12
- span.set_attribute("http.method", "GET")
13
- span.set_attribute("http.timeout", connection_timeout)
14
-
15
- # Log the request
16
- telemetry.logs().new_log(
17
- msg=f"Sending HTTP GET request to {endpoint}",
18
- tags={
19
- "endpoint": endpoint,
20
- "timeout": connection_timeout,
21
- "component": "GetData",
22
- "method": "get_response"
23
- },
24
- level=20 # INFO level
25
- )
26
-
27
- try:
28
- # Make the request
29
- response = requests.get(endpoint, timeout=connection_timeout, headers=headers)
30
-
31
- # Set response attributes on span
32
- span.set_attribute("http.status_code", response.status_code)
33
- span.set_attribute("http.response_content_length", len(response.content))
34
-
35
- # Attempt to raise for status to catch errors
36
- response.raise_for_status()
37
-
38
- # Log successful response
39
- telemetry.logs().new_log(
40
- msg=f"Received HTTP {response.status_code} response from {endpoint}",
41
- tags={
42
- "endpoint": endpoint,
43
- "status_code": response.status_code,
44
- "response_size": len(response.content),
45
- "component": "GetData",
46
- "method": "get_response"
47
- },
48
- level=20 # INFO level
49
- )
50
-
51
- # Record successful request metric
52
- telemetry.metrics().metric_increment(
53
- name="http.request.success",
54
- tags={
55
- "endpoint": endpoint,
56
- "status_code": response.status_code
57
- }
58
- )
59
-
60
- return response
61
-
62
- except requests.exceptions.RequestException as e:
63
- # Record the exception on the span
64
- span.record_exception(e)
65
- span.set_attribute("error", True)
66
- span.set_attribute("error.type", type(e).__name__)
67
- span.set_attribute("error.message", str(e))
68
-
69
- # Log the error
70
- telemetry.logs().new_log(
71
- msg=f"HTTP request failed: {str(e)}",
72
- tags={
73
- "endpoint": endpoint,
74
- "error": str(e),
75
- "error_type": type(e).__name__,
76
- "component": "GetData",
77
- "method": "get_response"
78
- },
79
- level=40 # ERROR level
80
- )
81
-
82
- # Record failure metric
83
- telemetry.metrics().metric_increment(
84
- name="http.request.failure",
85
- tags={
86
- "endpoint": endpoint,
87
- "error_type": type(e).__name__
88
- }
89
- )
90
-
91
- # Re-raise the exception
92
- raise
93
-
94
- @staticmethod
95
- def to_dataframe(response):
96
- # Start a span for dataframe conversion
97
- with telemetry.traces().span_in_context("convert_to_dataframe") as (span, _):
98
- # Set attributes about the data
99
- data_size = len(response) if isinstance(response, list) else 1
100
- span.set_attribute("data.size", data_size)
101
- span.set_attribute("data.type", type(response).__name__)
102
-
103
- # Log conversion attempt
104
- telemetry.logs().new_log(
105
- msg="Converting data to DataFrame",
106
- tags={
107
- "data_size": data_size,
108
- "data_type": type(response).__name__,
109
- "component": "GetData",
110
- "method": "to_dataframe"
111
- },
112
- level=20 # INFO level
113
- )
114
-
115
- try:
116
- # Convert to DataFrame
117
- df = pd.DataFrame(response)
118
-
119
- # Check if DataFrame is empty
120
- if df.empty:
121
- error_msg = "::: DataFrame is empty :::"
122
- logger.error(error_msg)
123
-
124
- # Log the error with OpenTelemetry
125
- telemetry.logs().new_log(
126
- msg=error_msg,
127
- tags={
128
- "data_size": data_size,
129
- "data_type": type(response).__name__,
130
- "component": "GetData",
131
- "method": "to_dataframe"
132
- },
133
- level=40 # ERROR level
134
- )
135
-
136
- # Record empty DataFrame metric
137
- telemetry.metrics().metric_increment(
138
- name="dataframe.empty",
139
- tags={"data_type": type(response).__name__}
140
- )
141
-
142
- # Set span as error
143
- span.set_attribute("error", True)
144
- span.set_attribute("error.type", "ValueError")
145
- span.set_attribute("error.message", error_msg)
146
-
147
- raise ValueError(error_msg)
148
-
149
- # Log success
150
- telemetry.logs().new_log(
151
- msg="Successfully converted data to DataFrame",
152
- tags={
153
- "rows": len(df),
154
- "columns": len(df.columns),
155
- "component": "GetData",
156
- "method": "to_dataframe"
157
- },
158
- level=20 # INFO level
159
- )
160
-
161
- # Record dataframe metrics
162
- telemetry.metrics().record_gauge(
163
- name="dataframe.rows",
164
- tags={"data_type": type(response).__name__},
165
- value=float(len(df))
166
- )
167
-
168
- telemetry.metrics().record_gauge(
169
- name="dataframe.columns",
170
- tags={"data_type": type(response).__name__},
171
- value=float(len(df.columns))
172
- )
173
-
174
- # Set additional span attributes
175
- span.set_attribute("dataframe.rows", len(df))
176
- span.set_attribute("dataframe.columns", len(df.columns))
177
-
178
- return df
179
-
180
- except ValueError:
181
- # Re-raise ValueErrors (like empty DataFrame)
182
- raise
183
-
184
- except Exception as err:
185
- # Log the error
186
- error_msg = f"Invalid response for transform in dataframe: {err}"
187
- logger.error(error_msg)
188
-
189
- # Log with OpenTelemetry
190
- telemetry.logs().new_log(
191
- msg=error_msg,
192
- tags={
193
- "error": str(err),
194
- "error_type": type(err).__name__,
195
- "data_type": type(response).__name__,
196
- "component": "GetData",
197
- "method": "to_dataframe"
198
- },
199
- level=40 # ERROR level
200
- )
201
-
202
- # Record conversion failure metric
203
- telemetry.metrics().metric_increment(
204
- name="dataframe.conversion.error",
205
- tags={"error_type": type(err).__name__}
206
- )
207
-
208
- # Record the exception on the span
209
- span.record_exception(err)
210
- span.set_attribute("error", True)
211
- span.set_attribute("error.type", type(err).__name__)
212
- span.set_attribute("error.message", str(err))
213
-
214
- # Raise TypeError with original error as cause
215
- raise TypeError(error_msg) from err
@@ -1,188 +0,0 @@
1
- import time
2
- from enum import Enum
3
-
4
- from requests.exceptions import RequestException
5
- from api_to_dataframe.utils.logger import logger, telemetry
6
- from api_to_dataframe.utils import Constants
7
-
8
-
9
- class Strategies(Enum):
10
- NO_RETRY_STRATEGY = 0
11
- LINEAR_RETRY_STRATEGY = 1
12
- EXPONENTIAL_RETRY_STRATEGY = 2
13
-
14
-
15
- def retry_strategies(func):
16
- def wrapper(*args, **kwargs): # pylint: disable=inconsistent-return-statements
17
- retry_number = 0
18
-
19
- # Get the endpoint for better observability context
20
- endpoint = args[0].endpoint if hasattr(args[0], 'endpoint') else 'unknown'
21
-
22
- # Start a span for this entire retry operation
23
- with telemetry.traces().span_in_context("retry_operation") as (span, _):
24
- span.set_attribute("endpoint", endpoint)
25
- span.set_attribute("retry_strategy", args[0].retry_strategy.name)
26
- span.set_attribute("max_retries", args[0].retries)
27
-
28
- while retry_number < args[0].retries:
29
- try:
30
- # Log retry attempt if not the first attempt
31
- if retry_number > 0:
32
- # Log using traditional logger
33
- logger.info(f"Trying for the {retry_number} of {Constants.MAX_OF_RETRIES} retries. Using {args[0].retry_strategy}")
34
-
35
- # Log using OpenTelemetry
36
- telemetry.logs().new_log(
37
- msg=f"Retry attempt {retry_number} of {args[0].retries}",
38
- tags={
39
- "endpoint": endpoint,
40
- "retry_number": retry_number,
41
- "max_retries": args[0].retries,
42
- "retry_strategy": args[0].retry_strategy.name,
43
- "component": "RetryStrategy"
44
- },
45
- level=20 # INFO level
46
- )
47
-
48
- # Record retry metric
49
- telemetry.metrics().metric_increment(
50
- name="api.request.retry",
51
- tags={
52
- "endpoint": endpoint,
53
- "retry_strategy": args[0].retry_strategy.name
54
- }
55
- )
56
-
57
- # Update span with current retry count
58
- span.set_attribute("current_retry", retry_number)
59
-
60
- # Execute the wrapped function
61
- result = func(*args, **kwargs)
62
-
63
- # If we got here, it succeeded - record success metric after retries
64
- if retry_number > 0:
65
- telemetry.metrics().metric_increment(
66
- name="api.request.retry.success",
67
- tags={
68
- "endpoint": endpoint,
69
- "retry_count": retry_number,
70
- "retry_strategy": args[0].retry_strategy.name
71
- }
72
- )
73
-
74
- telemetry.logs().new_log(
75
- msg=f"Request succeeded after {retry_number} retries",
76
- tags={
77
- "endpoint": endpoint,
78
- "retry_count": retry_number,
79
- "retry_strategy": args[0].retry_strategy.name,
80
- "component": "RetryStrategy"
81
- },
82
- level=20 # INFO level
83
- )
84
-
85
- return result
86
-
87
- except RequestException as e:
88
- retry_number += 1
89
-
90
- # Record exception in the span
91
- span.record_exception(e)
92
- span.set_attribute("failed_attempt", retry_number)
93
-
94
- # Handle different retry strategies
95
- if args[0].retry_strategy == Strategies.NO_RETRY_STRATEGY:
96
- # Log failure with OpenTelemetry
97
- telemetry.logs().new_log(
98
- msg=f"Request failed with {type(e).__name__}, no retry strategy configured",
99
- tags={
100
- "endpoint": endpoint,
101
- "error": str(e),
102
- "error_type": type(e).__name__,
103
- "component": "RetryStrategy"
104
- },
105
- level=40 # ERROR level
106
- )
107
-
108
- # Record failure metric
109
- telemetry.metrics().metric_increment(
110
- name="api.request.failure",
111
- tags={
112
- "endpoint": endpoint,
113
- "error_type": type(e).__name__,
114
- "retry_strategy": "none"
115
- }
116
- )
117
-
118
- raise e
119
-
120
- # Apply delay based on strategy
121
- if args[0].retry_strategy == Strategies.LINEAR_RETRY_STRATEGY:
122
- delay = args[0].delay
123
- strategy_name = "linear"
124
- elif args[0].retry_strategy == Strategies.EXPONENTIAL_RETRY_STRATEGY:
125
- delay = args[0].delay * retry_number
126
- strategy_name = "exponential"
127
-
128
- # Log retry delay
129
- telemetry.logs().new_log(
130
- msg=f"Request failed, retrying in {delay}s (strategy: {strategy_name})",
131
- tags={
132
- "endpoint": endpoint,
133
- "retry_count": retry_number,
134
- "delay": delay,
135
- "strategy": strategy_name,
136
- "error": str(e),
137
- "error_type": type(e).__name__,
138
- "component": "RetryStrategy"
139
- },
140
- level=30 # WARNING level
141
- )
142
-
143
- # Record the delay time in metrics
144
- telemetry.metrics().record_gauge(
145
- name="api.retry.delay",
146
- tags={
147
- "endpoint": endpoint,
148
- "retry_count": retry_number,
149
- "strategy": strategy_name
150
- },
151
- value=float(delay)
152
- )
153
-
154
- # Sleep for the calculated delay
155
- time.sleep(delay)
156
-
157
- # Check if we've reached max retries
158
- if retry_number in (args[0].retries, Constants.MAX_OF_RETRIES):
159
- # Log final failure
160
- error_msg = f"Failed after {retry_number} retries"
161
- logger.error(error_msg)
162
-
163
- telemetry.logs().new_log(
164
- msg=error_msg,
165
- tags={
166
- "endpoint": endpoint,
167
- "retry_count": retry_number,
168
- "max_retries": args[0].retries,
169
- "error": str(e),
170
- "error_type": type(e).__name__,
171
- "component": "RetryStrategy"
172
- },
173
- level=40 # ERROR level
174
- )
175
-
176
- # Record final failure metric
177
- telemetry.metrics().metric_increment(
178
- name="api.request.retry.exhausted",
179
- tags={
180
- "endpoint": endpoint,
181
- "retry_strategy": args[0].retry_strategy.name,
182
- "error_type": type(e).__name__
183
- }
184
- )
185
-
186
- raise e
187
-
188
- return wrapper