permitstack 1.0.0__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.
Files changed (68) hide show
  1. permitstack/__init__.py +17 -0
  2. permitstack/_hooks/__init__.py +4 -0
  3. permitstack/_hooks/sdkhooks.py +74 -0
  4. permitstack/_hooks/types.py +112 -0
  5. permitstack/_version.py +15 -0
  6. permitstack/basesdk.py +396 -0
  7. permitstack/bulk_export.py +241 -0
  8. permitstack/contractors.py +625 -0
  9. permitstack/errors/__init__.py +39 -0
  10. permitstack/errors/httpvalidationerror.py +28 -0
  11. permitstack/errors/no_response_error.py +17 -0
  12. permitstack/errors/permitstackdefaulterror.py +40 -0
  13. permitstack/errors/permitstackerror.py +30 -0
  14. permitstack/errors/responsevalidationerror.py +27 -0
  15. permitstack/health.py +171 -0
  16. permitstack/httpclient.py +125 -0
  17. permitstack/models/__init__.py +158 -0
  18. permitstack/models/contractorprofile.py +108 -0
  19. permitstack/models/contractorsearchresponse.py +24 -0
  20. permitstack/models/contractorsummary.py +57 -0
  21. permitstack/models/delete_webhookop.py +16 -0
  22. permitstack/models/export_permits_csvop.py +98 -0
  23. permitstack/models/get_contractor_permitsop.py +46 -0
  24. permitstack/models/get_contractorop.py +16 -0
  25. permitstack/models/get_permitop.py +16 -0
  26. permitstack/models/get_permits_by_addressop.py +46 -0
  27. permitstack/models/get_property_historyop.py +18 -0
  28. permitstack/models/permitcategory.py +27 -0
  29. permitstack/models/permitdetail.py +164 -0
  30. permitstack/models/permitsearchresponse.py +24 -0
  31. permitstack/models/permitstatus.py +16 -0
  32. permitstack/models/permitsummary.py +121 -0
  33. permitstack/models/propertytype.py +14 -0
  34. permitstack/models/search_contractorsop.py +98 -0
  35. permitstack/models/search_permitsop.py +247 -0
  36. permitstack/models/security.py +42 -0
  37. permitstack/models/validationerror.py +57 -0
  38. permitstack/models/webhookcreate.py +60 -0
  39. permitstack/permits.py +866 -0
  40. permitstack/property_history.py +207 -0
  41. permitstack/py.typed +1 -0
  42. permitstack/sdk.py +218 -0
  43. permitstack/sdkconfiguration.py +49 -0
  44. permitstack/types/__init__.py +21 -0
  45. permitstack/types/basemodel.py +77 -0
  46. permitstack/utils/__init__.py +178 -0
  47. permitstack/utils/annotations.py +79 -0
  48. permitstack/utils/datetimes.py +23 -0
  49. permitstack/utils/dynamic_imports.py +54 -0
  50. permitstack/utils/enums.py +134 -0
  51. permitstack/utils/eventstreaming.py +309 -0
  52. permitstack/utils/forms.py +234 -0
  53. permitstack/utils/headers.py +136 -0
  54. permitstack/utils/logger.py +27 -0
  55. permitstack/utils/metadata.py +119 -0
  56. permitstack/utils/queryparams.py +217 -0
  57. permitstack/utils/requestbodies.py +66 -0
  58. permitstack/utils/retries.py +271 -0
  59. permitstack/utils/security.py +215 -0
  60. permitstack/utils/serializers.py +225 -0
  61. permitstack/utils/unmarshal_json_response.py +38 -0
  62. permitstack/utils/url.py +155 -0
  63. permitstack/utils/values.py +137 -0
  64. permitstack/webhooks.py +593 -0
  65. permitstack-1.0.0.dist-info/METADATA +541 -0
  66. permitstack-1.0.0.dist-info/RECORD +68 -0
  67. permitstack-1.0.0.dist-info/WHEEL +5 -0
  68. permitstack-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,119 @@
1
+ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT."""
2
+
3
+ from typing import Optional, Type, TypeVar, Union
4
+ from dataclasses import dataclass
5
+ from pydantic.fields import FieldInfo
6
+
7
+
8
+ T = TypeVar("T")
9
+
10
+
11
+ @dataclass
12
+ class SecurityMetadata:
13
+ option: bool = False
14
+ scheme: bool = False
15
+ scheme_type: Optional[str] = None
16
+ sub_type: Optional[str] = None
17
+ field_name: Optional[str] = None
18
+ composite: bool = False
19
+
20
+ def get_field_name(self, default: str) -> str:
21
+ return self.field_name or default
22
+
23
+
24
+ @dataclass
25
+ class ParamMetadata:
26
+ serialization: Optional[str] = None
27
+ style: str = "simple"
28
+ explode: bool = False
29
+
30
+
31
+ @dataclass
32
+ class PathParamMetadata(ParamMetadata):
33
+ pass
34
+
35
+
36
+ @dataclass
37
+ class QueryParamMetadata(ParamMetadata):
38
+ style: str = "form"
39
+ explode: bool = True
40
+
41
+
42
+ @dataclass
43
+ class HeaderMetadata(ParamMetadata):
44
+ pass
45
+
46
+
47
+ @dataclass
48
+ class RequestMetadata:
49
+ media_type: str = "application/octet-stream"
50
+
51
+
52
+ @dataclass
53
+ class MultipartFormMetadata:
54
+ file: bool = False
55
+ content: bool = False
56
+ json: bool = False
57
+
58
+
59
+ @dataclass
60
+ class FormMetadata:
61
+ json: bool = False
62
+ style: str = "form"
63
+ explode: bool = True
64
+
65
+
66
+ class FieldMetadata:
67
+ security: Optional[SecurityMetadata] = None
68
+ path: Optional[PathParamMetadata] = None
69
+ query: Optional[QueryParamMetadata] = None
70
+ header: Optional[HeaderMetadata] = None
71
+ request: Optional[RequestMetadata] = None
72
+ form: Optional[FormMetadata] = None
73
+ multipart: Optional[MultipartFormMetadata] = None
74
+
75
+ def __init__(
76
+ self,
77
+ security: Optional[SecurityMetadata] = None,
78
+ path: Optional[Union[PathParamMetadata, bool]] = None,
79
+ query: Optional[Union[QueryParamMetadata, bool]] = None,
80
+ header: Optional[Union[HeaderMetadata, bool]] = None,
81
+ request: Optional[Union[RequestMetadata, bool]] = None,
82
+ form: Optional[Union[FormMetadata, bool]] = None,
83
+ multipart: Optional[Union[MultipartFormMetadata, bool]] = None,
84
+ ):
85
+ self.security = security
86
+ self.path = PathParamMetadata() if isinstance(path, bool) else path
87
+ self.query = QueryParamMetadata() if isinstance(query, bool) else query
88
+ self.header = HeaderMetadata() if isinstance(header, bool) else header
89
+ self.request = RequestMetadata() if isinstance(request, bool) else request
90
+ self.form = FormMetadata() if isinstance(form, bool) else form
91
+ self.multipart = (
92
+ MultipartFormMetadata() if isinstance(multipart, bool) else multipart
93
+ )
94
+
95
+
96
+ def find_field_metadata(field_info: FieldInfo, metadata_type: Type[T]) -> Optional[T]:
97
+ metadata = find_metadata(field_info, FieldMetadata)
98
+ if not metadata:
99
+ return None
100
+
101
+ fields = metadata.__dict__
102
+
103
+ for field in fields:
104
+ if isinstance(fields[field], metadata_type):
105
+ return fields[field]
106
+
107
+ return None
108
+
109
+
110
+ def find_metadata(field_info: FieldInfo, metadata_type: Type[T]) -> Optional[T]:
111
+ metadata = field_info.metadata
112
+ if not metadata:
113
+ return None
114
+
115
+ for md in metadata:
116
+ if isinstance(md, metadata_type):
117
+ return md
118
+
119
+ return None
@@ -0,0 +1,217 @@
1
+ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT."""
2
+
3
+ from typing import (
4
+ Any,
5
+ Dict,
6
+ get_type_hints,
7
+ List,
8
+ Optional,
9
+ )
10
+
11
+ from pydantic import BaseModel
12
+ from pydantic.fields import FieldInfo
13
+
14
+ from .metadata import (
15
+ QueryParamMetadata,
16
+ find_field_metadata,
17
+ )
18
+ from .values import (
19
+ _get_serialized_params,
20
+ _is_set,
21
+ _populate_from_globals,
22
+ _val_to_string,
23
+ )
24
+ from .forms import _populate_form
25
+
26
+
27
+ def get_query_params(
28
+ query_params: Any,
29
+ gbls: Optional[Any] = None,
30
+ allow_empty_value: Optional[List[str]] = None,
31
+ ) -> Dict[str, List[str]]:
32
+ params: Dict[str, List[str]] = {}
33
+
34
+ globals_already_populated = _populate_query_params(query_params, gbls, params, [], allow_empty_value)
35
+ if _is_set(gbls):
36
+ _populate_query_params(gbls, None, params, globals_already_populated, allow_empty_value)
37
+
38
+ return params
39
+
40
+
41
+ def _populate_query_params(
42
+ query_params: Any,
43
+ gbls: Any,
44
+ query_param_values: Dict[str, List[str]],
45
+ skip_fields: List[str],
46
+ allow_empty_value: Optional[List[str]] = None,
47
+ ) -> List[str]:
48
+ globals_already_populated: List[str] = []
49
+
50
+ if not isinstance(query_params, BaseModel):
51
+ return globals_already_populated
52
+
53
+ param_fields: Dict[str, FieldInfo] = query_params.__class__.model_fields
54
+ param_field_types = get_type_hints(query_params.__class__)
55
+ for name in param_fields:
56
+ if name in skip_fields:
57
+ continue
58
+
59
+ field = param_fields[name]
60
+
61
+ metadata = find_field_metadata(field, QueryParamMetadata)
62
+ if not metadata:
63
+ continue
64
+
65
+ value = getattr(query_params, name) if _is_set(query_params) else None
66
+
67
+ value, global_found = _populate_from_globals(
68
+ name, value, QueryParamMetadata, gbls
69
+ )
70
+ if global_found:
71
+ globals_already_populated.append(name)
72
+
73
+ f_name = field.alias if field.alias is not None else name
74
+
75
+ allow_empty_set = set(allow_empty_value or [])
76
+ should_include_empty = f_name in allow_empty_set and (
77
+ value is None or value == [] or value == ""
78
+ )
79
+
80
+ if should_include_empty:
81
+ query_param_values[f_name] = [""]
82
+ continue
83
+
84
+ serialization = metadata.serialization
85
+ if serialization is not None:
86
+ serialized_parms = _get_serialized_params(
87
+ metadata, f_name, value, param_field_types[name]
88
+ )
89
+ for key, value in serialized_parms.items():
90
+ if key in query_param_values:
91
+ query_param_values[key].extend(value)
92
+ else:
93
+ query_param_values[key] = [value]
94
+ else:
95
+ style = metadata.style
96
+ if style == "deepObject":
97
+ _populate_deep_object_query_params(f_name, value, query_param_values)
98
+ elif style == "form":
99
+ _populate_delimited_query_params(
100
+ metadata, f_name, value, ",", query_param_values
101
+ )
102
+ elif style == "pipeDelimited":
103
+ _populate_delimited_query_params(
104
+ metadata, f_name, value, "|", query_param_values
105
+ )
106
+ else:
107
+ raise NotImplementedError(
108
+ f"query param style {style} not yet supported"
109
+ )
110
+
111
+ return globals_already_populated
112
+
113
+
114
+ def _populate_deep_object_query_params(
115
+ field_name: str,
116
+ obj: Any,
117
+ params: Dict[str, List[str]],
118
+ ):
119
+ if not _is_set(obj):
120
+ return
121
+
122
+ if isinstance(obj, BaseModel):
123
+ _populate_deep_object_query_params_basemodel(field_name, obj, params)
124
+ elif isinstance(obj, Dict):
125
+ _populate_deep_object_query_params_dict(field_name, obj, params)
126
+
127
+
128
+ def _populate_deep_object_query_params_basemodel(
129
+ prior_params_key: str,
130
+ obj: Any,
131
+ params: Dict[str, List[str]],
132
+ ):
133
+ if not _is_set(obj) or not isinstance(obj, BaseModel):
134
+ return
135
+
136
+ obj_fields: Dict[str, FieldInfo] = obj.__class__.model_fields
137
+ for name in obj_fields:
138
+ obj_field = obj_fields[name]
139
+
140
+ f_name = obj_field.alias if obj_field.alias is not None else name
141
+
142
+ params_key = f"{prior_params_key}[{f_name}]"
143
+
144
+ obj_param_metadata = find_field_metadata(obj_field, QueryParamMetadata)
145
+ if not _is_set(obj_param_metadata):
146
+ continue
147
+
148
+ obj_val = getattr(obj, name)
149
+ if not _is_set(obj_val):
150
+ continue
151
+
152
+ if isinstance(obj_val, BaseModel):
153
+ _populate_deep_object_query_params_basemodel(params_key, obj_val, params)
154
+ elif isinstance(obj_val, Dict):
155
+ _populate_deep_object_query_params_dict(params_key, obj_val, params)
156
+ elif isinstance(obj_val, List):
157
+ _populate_deep_object_query_params_list(params_key, obj_val, params)
158
+ else:
159
+ params[params_key] = [_val_to_string(obj_val)]
160
+
161
+
162
+ def _populate_deep_object_query_params_dict(
163
+ prior_params_key: str,
164
+ value: Dict,
165
+ params: Dict[str, List[str]],
166
+ ):
167
+ if not _is_set(value):
168
+ return
169
+
170
+ for key, val in value.items():
171
+ if not _is_set(val):
172
+ continue
173
+
174
+ params_key = f"{prior_params_key}[{key}]"
175
+
176
+ if isinstance(val, BaseModel):
177
+ _populate_deep_object_query_params_basemodel(params_key, val, params)
178
+ elif isinstance(val, Dict):
179
+ _populate_deep_object_query_params_dict(params_key, val, params)
180
+ elif isinstance(val, List):
181
+ _populate_deep_object_query_params_list(params_key, val, params)
182
+ else:
183
+ params[params_key] = [_val_to_string(val)]
184
+
185
+
186
+ def _populate_deep_object_query_params_list(
187
+ params_key: str,
188
+ value: List,
189
+ params: Dict[str, List[str]],
190
+ ):
191
+ if not _is_set(value):
192
+ return
193
+
194
+ for val in value:
195
+ if not _is_set(val):
196
+ continue
197
+
198
+ if params.get(params_key) is None:
199
+ params[params_key] = []
200
+
201
+ params[params_key].append(_val_to_string(val))
202
+
203
+
204
+ def _populate_delimited_query_params(
205
+ metadata: QueryParamMetadata,
206
+ field_name: str,
207
+ obj: Any,
208
+ delimiter: str,
209
+ query_param_values: Dict[str, List[str]],
210
+ ):
211
+ _populate_form(
212
+ field_name,
213
+ metadata.explode,
214
+ obj,
215
+ delimiter,
216
+ query_param_values,
217
+ )
@@ -0,0 +1,66 @@
1
+ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT."""
2
+
3
+ import io
4
+ from dataclasses import dataclass
5
+ import re
6
+ from typing import (
7
+ Any,
8
+ Optional,
9
+ )
10
+
11
+ from .forms import serialize_form_data, serialize_multipart_form
12
+
13
+ from .serializers import marshal_json
14
+
15
+ SERIALIZATION_METHOD_TO_CONTENT_TYPE = {
16
+ "json": "application/json",
17
+ "form": "application/x-www-form-urlencoded",
18
+ "multipart": "multipart/form-data",
19
+ "raw": "application/octet-stream",
20
+ "string": "text/plain",
21
+ }
22
+
23
+
24
+ @dataclass
25
+ class SerializedRequestBody:
26
+ media_type: Optional[str] = None
27
+ content: Optional[Any] = None
28
+ data: Optional[Any] = None
29
+ files: Optional[Any] = None
30
+
31
+
32
+ def serialize_request_body(
33
+ request_body: Any,
34
+ nullable: bool,
35
+ optional: bool,
36
+ serialization_method: str,
37
+ request_body_type,
38
+ ) -> Optional[SerializedRequestBody]:
39
+ if request_body is None:
40
+ if not nullable and optional:
41
+ return None
42
+
43
+ media_type = SERIALIZATION_METHOD_TO_CONTENT_TYPE[serialization_method]
44
+
45
+ serialized_request_body = SerializedRequestBody(media_type)
46
+
47
+ if re.match(r"^(application|text)\/([^+]+\+)*json.*", media_type) is not None:
48
+ serialized_request_body.content = marshal_json(request_body, request_body_type)
49
+ elif re.match(r"^multipart\/.*", media_type) is not None:
50
+ (
51
+ serialized_request_body.media_type,
52
+ serialized_request_body.data,
53
+ serialized_request_body.files,
54
+ ) = serialize_multipart_form(media_type, request_body)
55
+ elif re.match(r"^application\/x-www-form-urlencoded.*", media_type) is not None:
56
+ serialized_request_body.data = serialize_form_data(request_body)
57
+ elif isinstance(request_body, (bytes, bytearray, io.BytesIO, io.BufferedReader)):
58
+ serialized_request_body.content = request_body
59
+ elif isinstance(request_body, str):
60
+ serialized_request_body.content = request_body
61
+ else:
62
+ raise TypeError(
63
+ f"invalid request body type {type(request_body)} for mediaType {media_type}"
64
+ )
65
+
66
+ return serialized_request_body
@@ -0,0 +1,271 @@
1
+ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT."""
2
+
3
+ import asyncio
4
+ import random
5
+ import time
6
+ from datetime import datetime
7
+ from email.utils import parsedate_to_datetime
8
+ from typing import List, Optional
9
+
10
+ import httpx
11
+
12
+
13
+ class BackoffStrategy:
14
+ initial_interval: int
15
+ max_interval: int
16
+ exponent: float
17
+ max_elapsed_time: int
18
+
19
+ def __init__(
20
+ self,
21
+ initial_interval: int,
22
+ max_interval: int,
23
+ exponent: float,
24
+ max_elapsed_time: int,
25
+ ):
26
+ self.initial_interval = initial_interval
27
+ self.max_interval = max_interval
28
+ self.exponent = exponent
29
+ self.max_elapsed_time = max_elapsed_time
30
+
31
+
32
+ class RetryConfig:
33
+ strategy: str
34
+ backoff: BackoffStrategy
35
+ retry_connection_errors: bool
36
+
37
+ def __init__(
38
+ self, strategy: str, backoff: BackoffStrategy, retry_connection_errors: bool
39
+ ):
40
+ self.strategy = strategy
41
+ self.backoff = backoff
42
+ self.retry_connection_errors = retry_connection_errors
43
+
44
+
45
+ class Retries:
46
+ config: RetryConfig
47
+ status_codes: List[str]
48
+
49
+ def __init__(self, config: RetryConfig, status_codes: List[str]):
50
+ self.config = config
51
+ self.status_codes = status_codes
52
+
53
+
54
+ class TemporaryError(Exception):
55
+ response: httpx.Response
56
+ retry_after: Optional[int]
57
+
58
+ def __init__(self, response: httpx.Response):
59
+ self.response = response
60
+ self.retry_after = _parse_retry_after_header(response)
61
+
62
+
63
+ class PermanentError(Exception):
64
+ inner: Exception
65
+
66
+ def __init__(self, inner: Exception):
67
+ self.inner = inner
68
+
69
+
70
+ def _parse_retry_after_header(response: httpx.Response) -> Optional[int]:
71
+ """Parse Retry-After header from response.
72
+
73
+ Returns:
74
+ Retry interval in milliseconds, or None if header is missing or invalid.
75
+ """
76
+ retry_after_header = response.headers.get("retry-after")
77
+ if not retry_after_header:
78
+ return None
79
+
80
+ try:
81
+ seconds = float(retry_after_header)
82
+ return round(seconds * 1000)
83
+ except ValueError:
84
+ pass
85
+
86
+ try:
87
+ retry_date = parsedate_to_datetime(retry_after_header)
88
+ delta = (retry_date - datetime.now(retry_date.tzinfo)).total_seconds()
89
+ return round(max(0, delta) * 1000)
90
+ except (ValueError, TypeError):
91
+ pass
92
+
93
+ return None
94
+
95
+
96
+ def _get_sleep_interval(
97
+ exception: Exception,
98
+ initial_interval: int,
99
+ max_interval: int,
100
+ exponent: float,
101
+ retries: int,
102
+ ) -> float:
103
+ """Get sleep interval for retry with exponential backoff.
104
+
105
+ Args:
106
+ exception: The exception that triggered the retry.
107
+ initial_interval: Initial retry interval in milliseconds.
108
+ max_interval: Maximum retry interval in milliseconds.
109
+ exponent: Base for exponential backoff calculation.
110
+ retries: Current retry attempt count.
111
+
112
+ Returns:
113
+ Sleep interval in seconds.
114
+ """
115
+ if (
116
+ isinstance(exception, TemporaryError)
117
+ and exception.retry_after is not None
118
+ and exception.retry_after > 0
119
+ ):
120
+ return exception.retry_after / 1000
121
+
122
+ sleep = (initial_interval / 1000) * exponent**retries + random.uniform(0, 1)
123
+ return min(sleep, max_interval / 1000)
124
+
125
+
126
+ def retry(func, retries: Retries):
127
+ if retries.config.strategy == "backoff":
128
+
129
+ def do_request() -> httpx.Response:
130
+ res: httpx.Response
131
+ try:
132
+ res = func()
133
+
134
+ for code in retries.status_codes:
135
+ if "X" in code.upper():
136
+ code_range = int(code[0])
137
+
138
+ status_major = res.status_code / 100
139
+
140
+ if code_range <= status_major < code_range + 1:
141
+ raise TemporaryError(res)
142
+ else:
143
+ parsed_code = int(code)
144
+
145
+ if res.status_code == parsed_code:
146
+ raise TemporaryError(res)
147
+ except (httpx.NetworkError, httpx.TimeoutException) as exception:
148
+ if retries.config.retry_connection_errors:
149
+ raise
150
+
151
+ raise PermanentError(exception) from exception
152
+ except TemporaryError:
153
+ raise
154
+ except Exception as exception:
155
+ raise PermanentError(exception) from exception
156
+
157
+ return res
158
+
159
+ return retry_with_backoff(
160
+ do_request,
161
+ retries.config.backoff.initial_interval,
162
+ retries.config.backoff.max_interval,
163
+ retries.config.backoff.exponent,
164
+ retries.config.backoff.max_elapsed_time,
165
+ )
166
+
167
+ return func()
168
+
169
+
170
+ async def retry_async(func, retries: Retries):
171
+ if retries.config.strategy == "backoff":
172
+
173
+ async def do_request() -> httpx.Response:
174
+ res: httpx.Response
175
+ try:
176
+ res = await func()
177
+
178
+ for code in retries.status_codes:
179
+ if "X" in code.upper():
180
+ code_range = int(code[0])
181
+
182
+ status_major = res.status_code / 100
183
+
184
+ if code_range <= status_major < code_range + 1:
185
+ raise TemporaryError(res)
186
+ else:
187
+ parsed_code = int(code)
188
+
189
+ if res.status_code == parsed_code:
190
+ raise TemporaryError(res)
191
+ except (httpx.NetworkError, httpx.TimeoutException) as exception:
192
+ if retries.config.retry_connection_errors:
193
+ raise
194
+
195
+ raise PermanentError(exception) from exception
196
+ except TemporaryError:
197
+ raise
198
+ except Exception as exception:
199
+ raise PermanentError(exception) from exception
200
+
201
+ return res
202
+
203
+ return await retry_with_backoff_async(
204
+ do_request,
205
+ retries.config.backoff.initial_interval,
206
+ retries.config.backoff.max_interval,
207
+ retries.config.backoff.exponent,
208
+ retries.config.backoff.max_elapsed_time,
209
+ )
210
+
211
+ return await func()
212
+
213
+
214
+ def retry_with_backoff(
215
+ func,
216
+ initial_interval=500,
217
+ max_interval=60000,
218
+ exponent=1.5,
219
+ max_elapsed_time=3600000,
220
+ ):
221
+ start = round(time.time() * 1000)
222
+ retries = 0
223
+
224
+ while True:
225
+ try:
226
+ return func()
227
+ except PermanentError as exception:
228
+ raise exception.inner
229
+ except Exception as exception: # pylint: disable=broad-exception-caught
230
+ now = round(time.time() * 1000)
231
+ if now - start > max_elapsed_time:
232
+ if isinstance(exception, TemporaryError):
233
+ return exception.response
234
+
235
+ raise
236
+
237
+ sleep = _get_sleep_interval(
238
+ exception, initial_interval, max_interval, exponent, retries
239
+ )
240
+ time.sleep(sleep)
241
+ retries += 1
242
+
243
+
244
+ async def retry_with_backoff_async(
245
+ func,
246
+ initial_interval=500,
247
+ max_interval=60000,
248
+ exponent=1.5,
249
+ max_elapsed_time=3600000,
250
+ ):
251
+ start = round(time.time() * 1000)
252
+ retries = 0
253
+
254
+ while True:
255
+ try:
256
+ return await func()
257
+ except PermanentError as exception:
258
+ raise exception.inner
259
+ except Exception as exception: # pylint: disable=broad-exception-caught
260
+ now = round(time.time() * 1000)
261
+ if now - start > max_elapsed_time:
262
+ if isinstance(exception, TemporaryError):
263
+ return exception.response
264
+
265
+ raise
266
+
267
+ sleep = _get_sleep_interval(
268
+ exception, initial_interval, max_interval, exponent, retries
269
+ )
270
+ await asyncio.sleep(sleep)
271
+ retries += 1