1- import binascii
2- import filetype
31import io
42import json
5- import logging
6- from base64 import b64encode , b64decode
3+ from base64 import b64encode
74from datetime import datetime
8- from functools import singledispatch
95from pathlib import Path
10- from json . decoder import JSONDecodeError
11- from typing import Any , Callable , Dict , List , Optional , Sequence , Union
6+
7+ from typing import Callable , Dict , List , Optional , Sequence , Union
128from urllib .parse import urlparse , quote
139
1410import requests
15- from backoff import expo , on_exception # type: ignore
1611from requests .exceptions import RequestException
1712
1813from .credentials import Credentials , guess_credentials
14+ from .content import parse_content
15+ from .log import setup_logging
16+ from .backoff import exponential_backoff
17+ from .response import decode_response , TooManyRequestsException , EmptyRequestError
1918
2019
21- logger = logging .getLogger (__name__ )
22- handler = logging .StreamHandler ()
23- handler .setFormatter (logging .Formatter ('%(asctime)s %(name)-12s %(levelname)-8s %(message)s' ))
24- logger .addHandler (handler )
25-
20+ logger = setup_logging (__name__ )
2621Content = Union [bytes , bytearray , str , Path , io .IOBase ]
2722Queryparam = Union [str , List [str ]]
2823
@@ -44,146 +39,15 @@ def _fatal_code(e):
4439 return 400 <= e .response .status_code < 500
4540
4641
47- def _decode_response (response , return_json = True ):
48- try :
49- response .raise_for_status ()
50- if return_json :
51- return response .json ()
52- else :
53- return response .content
54- except JSONDecodeError as e :
55-
56- if response .status_code == 204 :
57- return {'Your request executed successfully' : '204' }
58-
59- logger .error ('Status code {} body:\n {}' .format (response .status_code , response .text ))
60- raise e
61- except Exception as e :
62- logger .error ('Status code {} body:\n {}' .format (response .status_code , response .text ))
63-
64- if response .status_code == 400 :
65- message = response .json ().get ('message' , response .text )
66- raise BadRequest (message )
67-
68- if response .status_code == 403 and 'Forbidden' in response .json ().values ():
69- raise InvalidCredentialsException ('Credentials provided are not valid.' )
70-
71- if response .status_code == 404 :
72- message = response .json ().get ('message' , response .text )
73- raise NotFound (message )
74-
75- if response .status_code == 429 and 'Too Many Requests' in response .json ().values ():
76- raise TooManyRequestsException ('You have reached the limit of requests per second.' )
77-
78- if response .status_code == 429 and 'Limit Exceeded' in response .json ().values ():
79- raise LimitExceededException ('You have reached the limit of total requests per month.' )
80-
81- raise e
82-
83-
84- def _guess_content_type (raw ):
85- guessed_type = filetype .guess (raw )
86- assert guessed_type , 'Could not determine content type of document. ' \
87- 'Please provide it by specifying content_type'
88- return guessed_type .mime
89-
90-
91- def _parsed_content (raw , find_content_type , base_64_encode ):
92- content_type = _guess_content_type (raw ) if find_content_type else None
93- parsed_content = b64encode (raw ).decode () if base_64_encode else raw
94- return parsed_content , content_type
95-
96-
97- @singledispatch
98- def parse_content (content , find_content_type = False , base_64_encode = True ):
99- raise TypeError (
100- '\n ' .join ([
101- f'Could not parse content { content } of type { type (content )} ' ,
102- 'Specify content by using one of the options below:' ,
103- '1. Path to a file either as a string or as a Path object' ,
104- '2. Bytes object with b64encoding' ,
105- '3. Bytes object without b64encoding' ,
106- '4. IO Stream of either bytes or text' ,
107- ])
108- )
109-
110-
111- @parse_content .register (str )
112- @parse_content .register (Path )
113- def _ (content , find_content_type = False , base_64_encode = True ):
114- raw = Path (content ).read_bytes ()
115- return _parsed_content (raw , find_content_type , base_64_encode )
116-
117-
118- @parse_content .register (bytes )
119- @parse_content .register (bytearray )
120- def _ (content , find_content_type = False , base_64_encode = True ):
121- try :
122- raw = b64decode (content , validate = True )
123- except binascii .Error :
124- raw = content
125- return _parsed_content (raw , find_content_type , base_64_encode )
126-
127-
128- @parse_content .register (io .IOBase )
129- def _ (content , find_content_type = False , base_64_encode = True ):
130- raw = content .read ()
131- raw = raw .encode () if isinstance (raw , str ) else raw
132- return _parsed_content (raw , find_content_type , base_64_encode )
133-
134-
135- class EmptyRequestError (ValueError ):
136- """An EmptyRequestError is raised if the request body is empty when expected not to be empty."""
137- pass
138-
139-
140- class ClientException (Exception ):
141- """A ClientException is raised if the client refuses to
142- send request due to incorrect usage or bad request data."""
143- pass
144-
145-
146- class InvalidCredentialsException (ClientException ):
147- """An InvalidCredentialsException is raised if api key, access key id or secret access key is invalid."""
148- pass
149-
150-
151- class TooManyRequestsException (ClientException ):
152- """A TooManyRequestsException is raised if you have reached the number of requests per second limit
153- associated with your credentials."""
154- pass
155-
156-
157- class LimitExceededException (ClientException ):
158- """A LimitExceededException is raised if you have reached the limit of total requests per month
159- associated with your credentials."""
160- pass
161-
162-
163- class BadRequest (ClientException ):
164- """BadRequest is raised if you have made a request that is disqualified based on the input"""
165- pass
166-
167-
168- class NotFound (ClientException ):
169- """NotFound is raised when you try to access a resource that is not found"""
170- pass
171-
172-
173- class FileFormatException (ClientException ):
174- """A FileFormatException is raised if the file format is not supported by the api."""
175- pass
176-
177-
17842class Client :
17943 """A low level client to invoke api methods from Cradl."""
18044 def __init__ (self , credentials : Optional [Credentials ] = None , profile = None ):
18145 """:param credentials: Credentials to use, instance of :py:class:`~cradl.Credentials`
18246 :type credentials: Credentials"""
18347 self .credentials = credentials or guess_credentials (profile )
18448
185- @on_exception ( expo , TooManyRequestsException , max_tries = 4 )
186- @on_exception ( expo , RequestException , max_tries = 3 , giveup = _fatal_code )
49+ @exponential_backoff ( TooManyRequestsException , max_tries = 4 )
50+ @exponential_backoff ( RequestException , max_tries = 3 , giveup = _fatal_code )
18751 def _make_request (
18852 self ,
18953 requests_fn : Callable ,
@@ -212,10 +76,10 @@ def _make_request(
21276 headers = headers ,
21377 ** kwargs ,
21478 )
215- return _decode_response (response )
79+ return decode_response (response )
21680
217- @on_exception ( expo , TooManyRequestsException , max_tries = 4 )
218- @on_exception ( expo , RequestException , max_tries = 3 , giveup = _fatal_code )
81+ @exponential_backoff ( TooManyRequestsException , max_tries = 4 )
82+ @exponential_backoff ( RequestException , max_tries = 3 , giveup = _fatal_code )
21983 def _make_fileserver_request (
22084 self ,
22185 requests_fn : Callable ,
@@ -237,7 +101,7 @@ def _make_fileserver_request(
237101 headers = headers ,
238102 ** kwargs ,
239103 )
240- return _decode_response (response , return_json = False )
104+ return decode_response (response , return_json = False )
241105
242106 def create_app_client (
243107 self ,
@@ -943,7 +807,7 @@ def get_document(
943807 def update_document (
944808 self ,
945809 document_id : str ,
946- ground_truth : Sequence [Dict [str , Union [Optional [str ], bool ]]] = None , # For backwards compatibility reasons, this is placed before the *
810+ ground_truth : Sequence [Dict [str , Union [Optional [str ], bool ]]] = None , # For backwards compatibility reasons, this is placed before the *
947811 * ,
948812 metadata : Optional [dict ] = None ,
949813 dataset_id : str = None ,
0 commit comments