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,309 @@
1
+ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT."""
2
+
3
+ import re
4
+ import json
5
+ from dataclasses import dataclass, asdict
6
+ from typing import (
7
+ Any,
8
+ Callable,
9
+ Generic,
10
+ TypeVar,
11
+ Optional,
12
+ Generator,
13
+ AsyncGenerator,
14
+ Tuple,
15
+ )
16
+ import httpx
17
+
18
+ T = TypeVar("T")
19
+
20
+
21
+ class EventStream(Generic[T]):
22
+ # Holds a reference to the SDK client to avoid it being garbage collected
23
+ # and cause termination of the underlying httpx client.
24
+ client_ref: Optional[object]
25
+ response: httpx.Response
26
+ generator: Generator[T, None, None]
27
+ _closed: bool
28
+
29
+ def __init__(
30
+ self,
31
+ response: httpx.Response,
32
+ decoder: Callable[[str], T],
33
+ sentinel: Optional[str] = None,
34
+ client_ref: Optional[object] = None,
35
+ data_required: bool = True,
36
+ ):
37
+ self.response = response
38
+ self.generator = stream_events(
39
+ response, decoder, sentinel, data_required=data_required
40
+ )
41
+ self.client_ref = client_ref
42
+ self._closed = False
43
+
44
+ def __iter__(self):
45
+ return self
46
+
47
+ def __next__(self):
48
+ if self._closed:
49
+ raise StopIteration
50
+ return next(self.generator)
51
+
52
+ def __enter__(self):
53
+ return self
54
+
55
+ def __exit__(self, exc_type, exc_val, exc_tb):
56
+ self._closed = True
57
+ self.response.close()
58
+
59
+
60
+ class EventStreamAsync(Generic[T]):
61
+ # Holds a reference to the SDK client to avoid it being garbage collected
62
+ # and cause termination of the underlying httpx client.
63
+ client_ref: Optional[object]
64
+ response: httpx.Response
65
+ generator: AsyncGenerator[T, None]
66
+ _closed: bool
67
+
68
+ def __init__(
69
+ self,
70
+ response: httpx.Response,
71
+ decoder: Callable[[str], T],
72
+ sentinel: Optional[str] = None,
73
+ client_ref: Optional[object] = None,
74
+ data_required: bool = True,
75
+ ):
76
+ self.response = response
77
+ self.generator = stream_events_async(
78
+ response, decoder, sentinel, data_required=data_required
79
+ )
80
+ self.client_ref = client_ref
81
+ self._closed = False
82
+
83
+ def __aiter__(self):
84
+ return self
85
+
86
+ async def __anext__(self):
87
+ if self._closed:
88
+ raise StopAsyncIteration
89
+ return await self.generator.__anext__()
90
+
91
+ async def __aenter__(self):
92
+ return self
93
+
94
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
95
+ self._closed = True
96
+ await self.response.aclose()
97
+
98
+
99
+ @dataclass
100
+ class ServerEvent:
101
+ id: Optional[str] = None
102
+ event: Optional[str] = None
103
+ data: Any = None
104
+ retry: Optional[int] = None
105
+
106
+
107
+ MESSAGE_BOUNDARIES = [
108
+ b"\r\n\r\n",
109
+ b"\r\n\r",
110
+ b"\r\n\n",
111
+ b"\r\r\n",
112
+ b"\n\r\n",
113
+ b"\r\r",
114
+ b"\n\r",
115
+ b"\n\n",
116
+ ]
117
+
118
+ UTF8_BOM = b"\xef\xbb\xbf"
119
+
120
+
121
+ async def stream_events_async(
122
+ response: httpx.Response,
123
+ decoder: Callable[[str], T],
124
+ sentinel: Optional[str] = None,
125
+ data_required: bool = True,
126
+ ) -> AsyncGenerator[T, None]:
127
+ buffer = bytearray()
128
+ position = 0
129
+ event_id: Optional[str] = None
130
+ async for chunk in response.aiter_bytes():
131
+ if len(buffer) == 0 and chunk.startswith(UTF8_BOM):
132
+ chunk = chunk[len(UTF8_BOM) :]
133
+ buffer += chunk
134
+ for i in range(position, len(buffer)):
135
+ char = buffer[i : i + 1]
136
+ seq: Optional[bytes] = None
137
+ if char in [b"\r", b"\n"]:
138
+ for boundary in MESSAGE_BOUNDARIES:
139
+ seq = _peek_sequence(i, buffer, boundary)
140
+ if seq is not None:
141
+ break
142
+ if seq is None:
143
+ continue
144
+
145
+ block = buffer[position:i]
146
+ position = i + len(seq)
147
+ event, discard, event_id = _parse_event(
148
+ raw=block,
149
+ decoder=decoder,
150
+ sentinel=sentinel,
151
+ event_id=event_id,
152
+ data_required=data_required,
153
+ )
154
+ if event is not None:
155
+ yield event
156
+ if discard:
157
+ await response.aclose()
158
+ return
159
+
160
+ if position > 0:
161
+ buffer = buffer[position:]
162
+ position = 0
163
+
164
+ event, discard, _ = _parse_event(
165
+ raw=buffer,
166
+ decoder=decoder,
167
+ sentinel=sentinel,
168
+ event_id=event_id,
169
+ data_required=data_required,
170
+ )
171
+ if event is not None:
172
+ yield event
173
+
174
+
175
+ def stream_events(
176
+ response: httpx.Response,
177
+ decoder: Callable[[str], T],
178
+ sentinel: Optional[str] = None,
179
+ data_required: bool = True,
180
+ ) -> Generator[T, None, None]:
181
+ buffer = bytearray()
182
+ position = 0
183
+ event_id: Optional[str] = None
184
+ for chunk in response.iter_bytes():
185
+ if len(buffer) == 0 and chunk.startswith(UTF8_BOM):
186
+ chunk = chunk[len(UTF8_BOM) :]
187
+ buffer += chunk
188
+ for i in range(position, len(buffer)):
189
+ char = buffer[i : i + 1]
190
+ seq: Optional[bytes] = None
191
+ if char in [b"\r", b"\n"]:
192
+ for boundary in MESSAGE_BOUNDARIES:
193
+ seq = _peek_sequence(i, buffer, boundary)
194
+ if seq is not None:
195
+ break
196
+ if seq is None:
197
+ continue
198
+
199
+ block = buffer[position:i]
200
+ position = i + len(seq)
201
+ event, discard, event_id = _parse_event(
202
+ raw=block,
203
+ decoder=decoder,
204
+ sentinel=sentinel,
205
+ event_id=event_id,
206
+ data_required=data_required,
207
+ )
208
+ if event is not None:
209
+ yield event
210
+ if discard:
211
+ response.close()
212
+ return
213
+
214
+ if position > 0:
215
+ buffer = buffer[position:]
216
+ position = 0
217
+
218
+ event, discard, _ = _parse_event(
219
+ raw=buffer,
220
+ decoder=decoder,
221
+ sentinel=sentinel,
222
+ event_id=event_id,
223
+ data_required=data_required,
224
+ )
225
+ if event is not None:
226
+ yield event
227
+
228
+
229
+ def _parse_event(
230
+ *,
231
+ raw: bytearray,
232
+ decoder: Callable[[str], T],
233
+ sentinel: Optional[str] = None,
234
+ event_id: Optional[str] = None,
235
+ data_required: bool = True,
236
+ ) -> Tuple[Optional[T], bool, Optional[str]]:
237
+ block = raw.decode()
238
+ lines = re.split(r"\r?\n|\r", block)
239
+ publish = False
240
+ event = ServerEvent()
241
+ data = ""
242
+ for line in lines:
243
+ if not line:
244
+ continue
245
+
246
+ delim = line.find(":")
247
+ if delim == 0:
248
+ continue
249
+
250
+ field = line
251
+ value = ""
252
+ if delim > 0:
253
+ field = line[0:delim]
254
+ value = line[delim + 1 :] if delim < len(line) - 1 else ""
255
+ if len(value) and value[0] == " ":
256
+ value = value[1:]
257
+
258
+ if field == "event":
259
+ event.event = value
260
+ publish = True
261
+ elif field == "data":
262
+ data += value + "\n"
263
+ publish = True
264
+ elif field == "id":
265
+ publish = True
266
+ if "\x00" not in value:
267
+ event_id = value
268
+ elif field == "retry":
269
+ if value.isdigit():
270
+ event.retry = int(value)
271
+ publish = True
272
+
273
+ event.id = event_id
274
+
275
+ if sentinel and data == f"{sentinel}\n":
276
+ return None, True, event_id
277
+
278
+ # Skip data-less events when data is required
279
+ if not data and publish and data_required:
280
+ return None, False, event_id
281
+
282
+ if data:
283
+ data = data[:-1]
284
+ try:
285
+ event.data = json.loads(data)
286
+ except json.JSONDecodeError:
287
+ event.data = data
288
+
289
+ out = None
290
+ if publish:
291
+ out_dict = {
292
+ k: v
293
+ for k, v in asdict(event).items()
294
+ if v is not None or (k == "data" and data)
295
+ }
296
+ out = decoder(json.dumps(out_dict))
297
+
298
+ return out, False, event_id
299
+
300
+
301
+ def _peek_sequence(position: int, buffer: bytearray, sequence: bytes):
302
+ if len(sequence) > (len(buffer) - position):
303
+ return None
304
+
305
+ for i, seq in enumerate(sequence):
306
+ if buffer[position + i] != seq:
307
+ return None
308
+
309
+ return sequence
@@ -0,0 +1,234 @@
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
+ Tuple,
9
+ )
10
+ from pydantic import BaseModel
11
+ from pydantic.fields import FieldInfo
12
+
13
+ from .serializers import marshal_json
14
+
15
+ from .metadata import (
16
+ FormMetadata,
17
+ MultipartFormMetadata,
18
+ find_field_metadata,
19
+ )
20
+ from .values import _is_set, _val_to_string
21
+
22
+
23
+ def _populate_form(
24
+ field_name: str,
25
+ explode: bool,
26
+ obj: Any,
27
+ delimiter: str,
28
+ form: Dict[str, List[str]],
29
+ ):
30
+ if not _is_set(obj):
31
+ return form
32
+
33
+ if isinstance(obj, BaseModel):
34
+ items = []
35
+
36
+ obj_fields: Dict[str, FieldInfo] = obj.__class__.model_fields
37
+ for name in obj_fields:
38
+ obj_field = obj_fields[name]
39
+ obj_field_name = obj_field.alias if obj_field.alias is not None else name
40
+ if obj_field_name == "":
41
+ continue
42
+
43
+ val = getattr(obj, name)
44
+ if not _is_set(val):
45
+ continue
46
+
47
+ if explode:
48
+ form[obj_field_name] = [_val_to_string(val)]
49
+ else:
50
+ items.append(f"{obj_field_name}{delimiter}{_val_to_string(val)}")
51
+
52
+ if len(items) > 0:
53
+ form[field_name] = [delimiter.join(items)]
54
+ elif isinstance(obj, Dict):
55
+ items = []
56
+ for key, value in obj.items():
57
+ if not _is_set(value):
58
+ continue
59
+
60
+ if explode:
61
+ form[key] = [_val_to_string(value)]
62
+ else:
63
+ items.append(f"{key}{delimiter}{_val_to_string(value)}")
64
+
65
+ if len(items) > 0:
66
+ form[field_name] = [delimiter.join(items)]
67
+ elif isinstance(obj, List):
68
+ items = []
69
+
70
+ for value in obj:
71
+ if not _is_set(value):
72
+ continue
73
+
74
+ if explode:
75
+ if not field_name in form:
76
+ form[field_name] = []
77
+ form[field_name].append(_val_to_string(value))
78
+ else:
79
+ items.append(_val_to_string(value))
80
+
81
+ if len(items) > 0:
82
+ form[field_name] = [delimiter.join([str(item) for item in items])]
83
+ else:
84
+ form[field_name] = [_val_to_string(obj)]
85
+
86
+ return form
87
+
88
+
89
+ def _extract_file_properties(file_obj: Any) -> Tuple[str, Any, Any]:
90
+ """Extract file name, content, and content type from a file object."""
91
+ file_fields: Dict[str, FieldInfo] = file_obj.__class__.model_fields
92
+
93
+ file_name = ""
94
+ content = None
95
+ content_type = None
96
+
97
+ for file_field_name in file_fields:
98
+ file_field = file_fields[file_field_name]
99
+
100
+ file_metadata = find_field_metadata(file_field, MultipartFormMetadata)
101
+ if file_metadata is None:
102
+ continue
103
+
104
+ if file_metadata.content:
105
+ content = getattr(file_obj, file_field_name, None)
106
+ elif file_field_name == "content_type":
107
+ content_type = getattr(file_obj, file_field_name, None)
108
+ else:
109
+ file_name = getattr(file_obj, file_field_name)
110
+
111
+ if file_name == "" or content is None:
112
+ raise ValueError("invalid multipart/form-data file")
113
+
114
+ return file_name, content, content_type
115
+
116
+
117
+ def serialize_multipart_form(
118
+ media_type: str, request: Any
119
+ ) -> Tuple[str, Dict[str, Any], List[Tuple[str, Any]]]:
120
+ form: Dict[str, Any] = {}
121
+ files: List[Tuple[str, Any]] = []
122
+
123
+ if not isinstance(request, BaseModel):
124
+ raise TypeError("invalid request body type")
125
+
126
+ request_fields: Dict[str, FieldInfo] = request.__class__.model_fields
127
+ request_field_types = get_type_hints(request.__class__)
128
+
129
+ for name in request_fields:
130
+ field = request_fields[name]
131
+
132
+ val = getattr(request, name)
133
+ if not _is_set(val):
134
+ continue
135
+
136
+ field_metadata = find_field_metadata(field, MultipartFormMetadata)
137
+ if not field_metadata:
138
+ continue
139
+
140
+ f_name = field.alias if field.alias else name
141
+
142
+ if field_metadata.file:
143
+ if isinstance(val, List):
144
+ # Handle array of files
145
+ array_field_name = f_name
146
+ for file_obj in val:
147
+ if not _is_set(file_obj):
148
+ continue
149
+
150
+ file_name, content, content_type = _extract_file_properties(
151
+ file_obj
152
+ )
153
+
154
+ if content_type is not None:
155
+ files.append(
156
+ (array_field_name, (file_name, content, content_type))
157
+ )
158
+ else:
159
+ files.append((array_field_name, (file_name, content)))
160
+ else:
161
+ # Handle single file
162
+ file_name, content, content_type = _extract_file_properties(val)
163
+
164
+ if content_type is not None:
165
+ files.append((f_name, (file_name, content, content_type)))
166
+ else:
167
+ files.append((f_name, (file_name, content)))
168
+ elif field_metadata.json:
169
+ files.append(
170
+ (
171
+ f_name,
172
+ (
173
+ None,
174
+ marshal_json(val, request_field_types[name]),
175
+ "application/json",
176
+ ),
177
+ )
178
+ )
179
+ else:
180
+ if isinstance(val, List):
181
+ values = []
182
+
183
+ for value in val:
184
+ if not _is_set(value):
185
+ continue
186
+ values.append(_val_to_string(value))
187
+
188
+ array_field_name = f_name
189
+ form[array_field_name] = values
190
+ else:
191
+ form[f_name] = _val_to_string(val)
192
+ return media_type, form, files
193
+
194
+
195
+ def serialize_form_data(data: Any) -> Dict[str, Any]:
196
+ form: Dict[str, List[str]] = {}
197
+
198
+ if isinstance(data, BaseModel):
199
+ data_fields: Dict[str, FieldInfo] = data.__class__.model_fields
200
+ data_field_types = get_type_hints(data.__class__)
201
+ for name in data_fields:
202
+ field = data_fields[name]
203
+
204
+ val = getattr(data, name)
205
+ if not _is_set(val):
206
+ continue
207
+
208
+ metadata = find_field_metadata(field, FormMetadata)
209
+ if metadata is None:
210
+ continue
211
+
212
+ f_name = field.alias if field.alias is not None else name
213
+
214
+ if metadata.json:
215
+ form[f_name] = [marshal_json(val, data_field_types[name])]
216
+ else:
217
+ if metadata.style == "form":
218
+ _populate_form(
219
+ f_name,
220
+ metadata.explode,
221
+ val,
222
+ ",",
223
+ form,
224
+ )
225
+ else:
226
+ raise ValueError(f"Invalid form style for field {name}")
227
+ elif isinstance(data, Dict):
228
+ for key, value in data.items():
229
+ if _is_set(value):
230
+ form[key] = [_val_to_string(value)]
231
+ else:
232
+ raise TypeError(f"Invalid request body type {type(data)} for form data")
233
+
234
+ return form
@@ -0,0 +1,136 @@
1
+ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT."""
2
+
3
+ from typing import (
4
+ Any,
5
+ Dict,
6
+ List,
7
+ Optional,
8
+ )
9
+ from httpx import Headers
10
+ from pydantic import BaseModel
11
+ from pydantic.fields import FieldInfo
12
+
13
+ from .metadata import (
14
+ HeaderMetadata,
15
+ find_field_metadata,
16
+ )
17
+
18
+ from .values import _is_set, _populate_from_globals, _val_to_string
19
+
20
+
21
+ def get_headers(headers_params: Any, gbls: Optional[Any] = None) -> Dict[str, str]:
22
+ headers: Dict[str, str] = {}
23
+
24
+ globals_already_populated = []
25
+ if _is_set(headers_params):
26
+ globals_already_populated = _populate_headers(headers_params, gbls, headers, [])
27
+ if _is_set(gbls):
28
+ _populate_headers(gbls, None, headers, globals_already_populated)
29
+
30
+ return headers
31
+
32
+
33
+ def _populate_headers(
34
+ headers_params: Any,
35
+ gbls: Any,
36
+ header_values: Dict[str, str],
37
+ skip_fields: List[str],
38
+ ) -> List[str]:
39
+ globals_already_populated: List[str] = []
40
+
41
+ if not isinstance(headers_params, BaseModel):
42
+ return globals_already_populated
43
+
44
+ param_fields: Dict[str, FieldInfo] = headers_params.__class__.model_fields
45
+ for name in param_fields:
46
+ if name in skip_fields:
47
+ continue
48
+
49
+ field = param_fields[name]
50
+ f_name = field.alias if field.alias is not None else name
51
+
52
+ metadata = find_field_metadata(field, HeaderMetadata)
53
+ if metadata is None:
54
+ continue
55
+
56
+ value, global_found = _populate_from_globals(
57
+ name, getattr(headers_params, name), HeaderMetadata, gbls
58
+ )
59
+ if global_found:
60
+ globals_already_populated.append(name)
61
+ value = _serialize_header(metadata.explode, value)
62
+
63
+ if value != "":
64
+ header_values[f_name] = value
65
+
66
+ return globals_already_populated
67
+
68
+
69
+ def _serialize_header(explode: bool, obj: Any) -> str:
70
+ if not _is_set(obj):
71
+ return ""
72
+
73
+ if isinstance(obj, BaseModel):
74
+ items = []
75
+ obj_fields: Dict[str, FieldInfo] = obj.__class__.model_fields
76
+ for name in obj_fields:
77
+ obj_field = obj_fields[name]
78
+ obj_param_metadata = find_field_metadata(obj_field, HeaderMetadata)
79
+
80
+ if not obj_param_metadata:
81
+ continue
82
+
83
+ f_name = obj_field.alias if obj_field.alias is not None else name
84
+
85
+ val = getattr(obj, name)
86
+ if not _is_set(val):
87
+ continue
88
+
89
+ if explode:
90
+ items.append(f"{f_name}={_val_to_string(val)}")
91
+ else:
92
+ items.append(f_name)
93
+ items.append(_val_to_string(val))
94
+
95
+ if len(items) > 0:
96
+ return ",".join(items)
97
+ elif isinstance(obj, Dict):
98
+ items = []
99
+
100
+ for key, value in obj.items():
101
+ if not _is_set(value):
102
+ continue
103
+
104
+ if explode:
105
+ items.append(f"{key}={_val_to_string(value)}")
106
+ else:
107
+ items.append(key)
108
+ items.append(_val_to_string(value))
109
+
110
+ if len(items) > 0:
111
+ return ",".join([str(item) for item in items])
112
+ elif isinstance(obj, List):
113
+ items = []
114
+
115
+ for value in obj:
116
+ if not _is_set(value):
117
+ continue
118
+
119
+ items.append(_val_to_string(value))
120
+
121
+ if len(items) > 0:
122
+ return ",".join(items)
123
+ elif _is_set(obj):
124
+ return f"{_val_to_string(obj)}"
125
+
126
+ return ""
127
+
128
+
129
+ def get_response_headers(headers: Headers) -> Dict[str, List[str]]:
130
+ res: Dict[str, List[str]] = {}
131
+ for k, v in headers.items():
132
+ if not k in res:
133
+ res[k] = []
134
+
135
+ res[k].append(v)
136
+ return res
@@ -0,0 +1,27 @@
1
+ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT."""
2
+
3
+ import httpx
4
+ import logging
5
+ import os
6
+ from typing import Any, Protocol
7
+
8
+
9
+ class Logger(Protocol):
10
+ def debug(self, msg: str, *args: Any, **kwargs: Any) -> None:
11
+ pass
12
+
13
+
14
+ class NoOpLogger:
15
+ def debug(self, msg: str, *args: Any, **kwargs: Any) -> None:
16
+ pass
17
+
18
+
19
+ def get_body_content(req: httpx.Request) -> str:
20
+ return "<streaming body>" if not hasattr(req, "_content") else str(req.content)
21
+
22
+
23
+ def get_default_logger() -> Logger:
24
+ if os.getenv("PERMITSTACK_DEBUG"):
25
+ logging.basicConfig(level=logging.DEBUG)
26
+ return logging.getLogger("permitstack")
27
+ return NoOpLogger()