66from typing import Any , Callable , Iterable , TYPE_CHECKING , Generator
77
88import requests
9+ from http import HTTPStatus
10+ from urllib .parse import urlparse
911from singer_sdk .authenticators import BasicAuthenticator
1012from singer_sdk .helpers .jsonpath import extract_jsonpath
1113from singer_sdk .streams import RESTStream
2022
2123class FreshdeskStream (RESTStream ):
2224 """freshdesk stream class."""
25+
2326 name : str
2427 records_jsonpath = "$.[*]" # Or override `parse_response`.
2528 primary_keys = ["id" ]
@@ -33,11 +36,11 @@ def path(self) -> str:
3336 """
3437 'groups' -> '/groups'
3538 """
36- return f' /{ self .name } '
39+ return f" /{ self .name } "
3740
3841 @property
3942 def schema_filepath (self ) -> Path | None :
40- return SCHEMAS_DIR / f' { self .name } .json'
43+ return SCHEMAS_DIR / f" { self .name } .json"
4144
4245 # OR use a dynamic url_base:
4346 @property
@@ -46,7 +49,6 @@ def url_base(self) -> str:
4649 domain = self .config ["domain" ]
4750 return f"https://{ domain } .freshdesk.com/api/v2"
4851
49-
5052 @property
5153 def authenticator (self ) -> BasicAuthenticator :
5254 """Return a new authenticator object.
@@ -117,11 +119,11 @@ def get_url_params(
117119 A dictionary of URL query parameters.
118120 """
119121 params : dict = {}
120- embeds = self .config .get (' embeds' )
122+ embeds = self .config .get (" embeds" )
121123 if embeds :
122124 embed_fields = embeds .get (self .name , [])
123- if embed_fields : # i.e. 'stats,company,sla_policy'
124- params [' include' ] = ',' .join (embed_fields )
125+ if embed_fields : # i.e. 'stats,company,sla_policy'
126+ params [" include" ] = "," .join (embed_fields )
125127 return params
126128
127129 def prepare_request_payload (
@@ -167,33 +169,76 @@ def post_process(self, row: dict, context: dict | None = None) -> dict | None:
167169 """
168170 # TODO: Delete this method if not needed.
169171 return row
170-
172+
171173 def get_new_paginator (self ) -> SinglePagePaginator :
172174 return SinglePagePaginator ()
173-
175+
174176 def backoff_wait_generator (self ) -> Generator [float , None , None ]:
175177 return self .backoff_runtime (value = self ._wait_for )
176-
178+
177179 @staticmethod
178180 def _wait_for (exception ) -> int :
179181 """
180182 When 429 thrown, header contains the time to wait before
181183 the next call is allowed, rather than use exponential backoff"""
182- return int (exception .response .headers [' Retry-After' ])
183-
184+ return int (exception .response .headers [" Retry-After" ])
185+
184186 def backoff_jitter (self , value : float ) -> float :
185187 return value
186188
189+ # Handling error, overriding this method over RESTStream's Class
190+ def response_error_message (self , response : requests .Response ) -> str :
191+ """Build error message for invalid http statuses.
192+
193+ WARNING - Override this method when the URL path may contain secrets or PII
194+
195+ Args:
196+ response: A :class:`requests.Response` object.
197+
198+ Returns:
199+ str: The error message
200+ """
201+ full_path = urlparse (response .url ).path or self .path
202+ error_type = (
203+ "Client"
204+ if HTTPStatus .BAD_REQUEST
205+ <= response .status_code
206+ < HTTPStatus .INTERNAL_SERVER_ERROR
207+ else "Server"
208+ )
209+
210+ error_details = []
211+ if response .status_code >= 400 :
212+ print (f"Error Response: { response .status_code } { response .reason } " )
213+ try :
214+ error_data = response .json ()
215+ errors = error_data .get ("errors" )
216+ for index , error in enumerate (errors ):
217+ message = error .get ("message" , "Unknown" )
218+ field = error .get ("field" , "Unknown" )
219+ error_details .append (
220+ f"Error { index + 1 } : Message - { message } , Field - { field } "
221+ )
222+ except requests .exceptions .JSONDecodeError :
223+ return "Error: Unable to parse JSON error response"
224+
225+ return (
226+ f"{ response .status_code } { error_type } Error: "
227+ f"{ response .reason } for path: { full_path } . "
228+ f"Error via function response_error_message : { '. ' .join (error_details )} ."
229+ )
230+
231+
187232class FreshdeskPaginator (BasePageNumberPaginator ):
188233
189234 def has_more (self , response : Response ) -> bool :
190235 """
191236 There is no 'has more' indicator for this stream.
192- If there are no results on this page, then this is 'last' page,
237+ If there are no results on this page, then this is 'last' page,
193238 (even though technically the page before was the last, there was no way to tell).
194239 """
195- return len (response .json ())
196-
240+ return len (response .json ()) != 0 and self . current_value < 300
241+
197242
198243class PagedFreshdeskStream (FreshdeskStream ):
199244
@@ -213,11 +258,11 @@ def get_url_params(
213258 """
214259 context = context or {}
215260 params = super ().get_url_params (context , next_page_token )
216- params [' per_page' ] = 100
261+ params [" per_page" ] = 100
217262 if next_page_token :
218263 params ["page" ] = next_page_token
219- if ' updated_since' not in context :
220- params [' updated_since' ] = self .get_starting_timestamp (context )
264+ if " updated_since" not in context :
265+ params [" updated_since" ] = self .get_starting_timestamp (context )
221266 return params
222267
223268 def get_new_paginator (self ) -> BasePageNumberPaginator :
0 commit comments