openapi-httpx-client 0.1.0__tar.gz → 0.2.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,26 +1,17 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.1
2
2
  Name: openapi-httpx-client
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: A Python client for OpenAPI specifications using httpx
5
5
  Home-page: https://github.com/lloydzhou/openapiclient
6
6
  Author: lloydzhou
7
7
  Author-email: lloydzhou@qq.com
8
+ License: UNKNOWN
9
+ Platform: UNKNOWN
8
10
  Classifier: Programming Language :: Python :: 3
9
11
  Classifier: License :: OSI Approved :: MIT License
10
12
  Classifier: Operating System :: OS Independent
11
13
  Requires-Python: >=3.7
12
14
  Description-Content-Type: text/markdown
13
- Requires-Dist: httpx>=0.23.0
14
- Requires-Dist: pyyaml>=6.0
15
- Dynamic: author
16
- Dynamic: author-email
17
- Dynamic: classifier
18
- Dynamic: description
19
- Dynamic: description-content-type
20
- Dynamic: home-page
21
- Dynamic: requires-dist
22
- Dynamic: requires-python
23
- Dynamic: summary
24
15
 
25
16
  # OpenAPI Client for Python
26
17
 
@@ -29,7 +20,7 @@ A Python implementation inspired by [openapi-client-axios](https://github.com/op
29
20
  ## Installation
30
21
 
31
22
  ```bash
32
- pip install -e .
23
+ pip install openapi-httpx-client
33
24
  ```
34
25
 
35
26
  ## Usage
@@ -73,3 +64,5 @@ if __name__ == "__main__":
73
64
  lloydzhou (2025-02-27)
74
65
 
75
66
 
67
+
68
+
@@ -5,7 +5,7 @@ A Python implementation inspired by [openapi-client-axios](https://github.com/op
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
- pip install -e .
8
+ pip install openapi-httpx-client
9
9
  ```
10
10
 
11
11
  ## Usage
@@ -1,26 +1,17 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.1
2
2
  Name: openapi-httpx-client
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: A Python client for OpenAPI specifications using httpx
5
5
  Home-page: https://github.com/lloydzhou/openapiclient
6
6
  Author: lloydzhou
7
7
  Author-email: lloydzhou@qq.com
8
+ License: UNKNOWN
9
+ Platform: UNKNOWN
8
10
  Classifier: Programming Language :: Python :: 3
9
11
  Classifier: License :: OSI Approved :: MIT License
10
12
  Classifier: Operating System :: OS Independent
11
13
  Requires-Python: >=3.7
12
14
  Description-Content-Type: text/markdown
13
- Requires-Dist: httpx>=0.23.0
14
- Requires-Dist: pyyaml>=6.0
15
- Dynamic: author
16
- Dynamic: author-email
17
- Dynamic: classifier
18
- Dynamic: description
19
- Dynamic: description-content-type
20
- Dynamic: home-page
21
- Dynamic: requires-dist
22
- Dynamic: requires-python
23
- Dynamic: summary
24
15
 
25
16
  # OpenAPI Client for Python
26
17
 
@@ -29,7 +20,7 @@ A Python implementation inspired by [openapi-client-axios](https://github.com/op
29
20
  ## Installation
30
21
 
31
22
  ```bash
32
- pip install -e .
23
+ pip install openapi-httpx-client
33
24
  ```
34
25
 
35
26
  ## Usage
@@ -73,3 +64,5 @@ if __name__ == "__main__":
73
64
  lloydzhou (2025-02-27)
74
65
 
75
66
 
67
+
68
+
@@ -1,2 +1,3 @@
1
1
  httpx>=0.23.0
2
2
  pyyaml>=6.0
3
+ nanoid>=0.2.1
@@ -0,0 +1,355 @@
1
+ import httpx
2
+ import json
3
+ import os.path
4
+ from urllib.parse import urljoin, urlparse
5
+ import yaml
6
+ import types
7
+ import functools
8
+ from nanoid import generate as nanoid_generate
9
+
10
+ # Create a base class for DynamicClient
11
+ class DynamicClientBase:
12
+
13
+ @property
14
+ def functions(self):
15
+ """Return all operation methods available in this client"""
16
+ return {name: getattr(self, name) for name in self.operations if hasattr(self, name)}
17
+
18
+ def __getitem__(self, name):
19
+ """Allow dictionary-like access to operations by name"""
20
+ if name in self.operations and hasattr(self, name):
21
+ return getattr(self, name)
22
+ raise KeyError(f"Operation '{name}' not found")
23
+
24
+ def __iter__(self):
25
+ """Allow iteration over all operation names"""
26
+ return iter(self.functions)
27
+
28
+ def __call__(self, method_name, *args, **kwargs):
29
+ """Allow calling methods by name with partial application"""
30
+ if method_name not in self.operations:
31
+ raise AttributeError(f"'{self.__class__.__name__}' has no operation '{method_name}'")
32
+
33
+ method = getattr(self, method_name, None)
34
+ if not method or not callable(method):
35
+ raise AttributeError(f"'{self.__class__.__name__}' has no callable method '{method_name}'")
36
+
37
+ return functools.partial(method, *args, **kwargs)
38
+
39
+
40
+ # Create the main OpenAPIClient class
41
+ class OpenAPIClient:
42
+ """
43
+ A Python client for OpenAPI specifications, inspired by openapi-client-axios.
44
+ Uses httpx for HTTP requests.
45
+ """
46
+
47
+ def __init__(self, definition=None):
48
+ """
49
+ Initialize the OpenAPI client.
50
+
51
+ Args:
52
+ definition: URL or file path to the OpenAPI definition, or a dictionary containing the definition
53
+ """
54
+ self.definition_source = definition
55
+ self.definition = {}
56
+ self.client = None
57
+ self.base_url = ''
58
+ self.session = None
59
+ self.source_url = None # Store the source URL if loaded from a URL
60
+
61
+ async def init(self):
62
+ """
63
+ Initialize the client by loading and parsing the OpenAPI definition.
64
+
65
+ Returns:
66
+ DynamicClient: A client with methods generated from the OpenAPI definition
67
+ """
68
+ # Load the OpenAPI definition
69
+ await self.load_definition()
70
+
71
+ # Create HTTP session
72
+ self.session = httpx.AsyncClient()
73
+
74
+ # Set base URL from the servers list if available
75
+ self.setup_base_url()
76
+
77
+ # Create a dynamic client with methods based on the operations defined in the spec
78
+ return await self.create_dynamic_client()
79
+
80
+ async def load_definition(self):
81
+ """
82
+ Load the OpenAPI definition from a URL, file, or dictionary.
83
+ """
84
+ if isinstance(self.definition_source, dict):
85
+ self.definition = self.definition_source
86
+ return
87
+
88
+ if os.path.isfile(str(self.definition_source)):
89
+ # Load from file
90
+ with open(self.definition_source, 'r') as f:
91
+ content = f.read()
92
+ if self.definition_source.endswith('.yaml') or self.definition_source.endswith('.yml'):
93
+ self.definition = yaml.safe_load(content)
94
+ else:
95
+ self.definition = json.loads(content)
96
+ return
97
+
98
+ # Assume it's a URL
99
+ self.source_url = self.definition_source # Store the source URL
100
+ async with httpx.AsyncClient() as client:
101
+ response = await client.get(self.definition_source)
102
+ if response.status_code == 200:
103
+ content_type = response.headers.get('Content-Type', '')
104
+ if 'yaml' in content_type or 'yml' in content_type:
105
+ self.definition = yaml.safe_load(response.text)
106
+ else:
107
+ self.definition = response.json()
108
+ else:
109
+ raise Exception(f"Failed to load OpenAPI definition: {response.status_code}")
110
+
111
+ def setup_base_url(self):
112
+ """
113
+ Set up the base URL for API requests, handling various server URL formats.
114
+ """
115
+ if 'servers' in self.definition and self.definition['servers']:
116
+ server_url = self.definition['servers'][0]['url']
117
+
118
+ # Check if this is a full URL or just a path
119
+ parsed_url = urlparse(server_url)
120
+
121
+ # If it's a full URL (has scheme), use it directly
122
+ if parsed_url.scheme:
123
+ self.base_url = server_url
124
+ # If it's not a full URL and we loaded from a URL, combine them
125
+ elif self.source_url:
126
+ # Parse the source URL to get scheme, hostname, and port
127
+ source_parsed = urlparse(self.source_url)
128
+ base = f"{source_parsed.scheme}://{source_parsed.netloc}"
129
+
130
+ # Combine the base with the server path
131
+ self.base_url = urljoin(base, server_url)
132
+ else:
133
+ # Just use what we have
134
+ self.base_url = server_url
135
+
136
+ def get_operations(self):
137
+ """
138
+ Extract all operations from the OpenAPI definition.
139
+ # https://github.com/openapistack/openapi-client-axios/blob/main/packages/openapi-client-axios/src/client.ts#L581
140
+
141
+ Returns:
142
+ list: A list of operation objects with normalized properties.
143
+ """
144
+ # Get all paths from the definition or empty dict if not available
145
+ paths = self.definition.get('paths', {})
146
+ # List of standard HTTP methods in OpenAPI
147
+ http_methods = ['get', 'post', 'put', 'delete', 'patch', 'options', 'head']
148
+ operations = []
149
+
150
+ # Iterate through each path
151
+ for path, path_object in paths.items():
152
+ # For each HTTP method in the path
153
+ for method in http_methods:
154
+ operation = path_object.get(method)
155
+ # Skip if this method doesn't exist for this path
156
+ if not operation:
157
+ continue
158
+
159
+ # Create operation object with basic properties
160
+ op = operation.copy() if isinstance(operation, dict) else {}
161
+ op['path'] = path
162
+ op['method'] = method
163
+
164
+ # Add path-level parameters if they exist
165
+ if 'parameters' in path_object:
166
+ op['parameters'] = (op.get('parameters', []) + path_object['parameters'])
167
+
168
+ # Add path-level servers if they exist
169
+ if 'servers' in path_object:
170
+ op['servers'] = (op.get('servers', []) + path_object['servers'])
171
+
172
+ # Set security from definition if not specified in operation
173
+ if 'security' not in op and 'security' in self.definition:
174
+ op['security'] = self.definition['security']
175
+
176
+ operations.append(op)
177
+
178
+ return operations
179
+
180
+ async def create_dynamic_client(self):
181
+ """
182
+ Create a client with methods dynamically generated from the OpenAPI spec using metaprogramming.
183
+
184
+ Returns:
185
+ DynamicClient: A client with methods for each operation in the spec
186
+ """
187
+
188
+ def resolve_schema_ref(schema):
189
+ if '$ref' in schema:
190
+ schema = all_references.get(schema['$ref'], {})
191
+ elif schema.get('type') == 'object':
192
+ for key, value in schema.get('properties', {}).items():
193
+ schema['properties'][key] = resolve_schema_ref(value)
194
+ elif schema.get('type') == 'array':
195
+ schema['items'] = resolve_schema_ref(schema.get('items', {}))
196
+ return schema
197
+
198
+ all_references = {f'#/components/schemas/{name}': schema for name, schema in self.definition.get('components', {}).get('schemas', {}).items()}
199
+ # try to resolve sub `$ref` in all references
200
+ for name, schema in all_references.items():
201
+ schema = resolve_schema_ref(schema)
202
+ all_references[name] = schema
203
+
204
+ def create_tool(operation_id, operation):
205
+ # Get parameters from the request body schema only for json content
206
+ body = operation.get('requestBody', {})
207
+ schema = body.get('content', {}).get('application/json', {}).get('schema', {})
208
+ parameters = {
209
+ "type": "object",
210
+ "required": ['body'] if body.get("required", False) else [],
211
+ "description": body.get('description', ''),
212
+ "properties": {
213
+ "body": resolve_schema_ref(schema) if schema else {},
214
+ }
215
+ }
216
+ # add parameters from path and query
217
+ for parameter in operation.get('parameters', []):
218
+ name = parameter.get('name')
219
+ if parameter.get('required', False):
220
+ parameters["required"].append(name)
221
+ item = {
222
+ "type": parameter.get('schema', {}).get('type', 'string'),
223
+ "description": parameter.get('description', ''),
224
+ }
225
+ # Add format, enum, and example if available
226
+ for key in ['format', 'enum', 'example']:
227
+ if parameter.get('schema', {}).get(key):
228
+ item[key] = parameter.get('schema', {}).get(key)
229
+ parameters["properties"][name] = item
230
+
231
+ return {
232
+ "type": "function",
233
+ "function": {
234
+ "name": operation_id,
235
+ "description": operation.get('summary', '') or operation.get('description', ''),
236
+ "parameters": parameters,
237
+ }
238
+ }
239
+
240
+ # Create a new class dynamically using type
241
+ paths, tools, methods_dict = [], {}, {}
242
+ for opration in self.get_operations():
243
+ operation_id, path = opration.get('operationId'), opration.get('path')
244
+ paths.append(path)
245
+ methods_dict[operation_id] = self.create_operation_method(path, opration.get('method'), opration)
246
+ tools[operation_id] = create_tool(operation_id, opration)
247
+
248
+ # Generate a unique class name based on the API info or a random suffix
249
+ api_title = self.definition.get('info', {}).get('title', '')
250
+ # Replace spaces, hyphens, dots and other special characters
251
+ api_version = self.definition.get('info', {}).get('version', '')
252
+
253
+ if api_title:
254
+ class_name = f"{api_title}Client_{api_version}" if api_version else f"{api_title}Client"
255
+ else:
256
+ # Fallback to a random suffix
257
+ class_name = f"DynamicClient_{nanoid_generate(size=8)}"
258
+
259
+ # Replace spaces, hyphens, dots and other special characters
260
+ class_name = ''.join(c for c in class_name if c.isalnum())
261
+
262
+ attribute_dict = {
263
+ **methods_dict,
264
+ 'operations': list(methods_dict.keys()),
265
+ 'paths': paths,
266
+ 'tools': list(tools.values()),
267
+ '_api': self, # Store reference to the api
268
+ }
269
+ # Create the dynamic client class with the methods and the base class
270
+ DynamicClientClass = type(class_name, (DynamicClientBase,), attribute_dict)
271
+
272
+ # Create an instance of this class
273
+ client = DynamicClientClass()
274
+
275
+ return client
276
+
277
+ def create_operation_method(self, path, method, operation):
278
+ """
279
+ Create a method for an operation defined in the OpenAPI spec.
280
+
281
+ Args:
282
+ path: The path template (e.g., "/pets/{petId}")
283
+ method: The HTTP method (e.g., "get", "post")
284
+ operation: The operation object from the OpenAPI spec
285
+
286
+ Returns:
287
+ function: A method that performs the API request
288
+ """
289
+ async def operation_method(*args, **kwargs):
290
+ # Process path parameters
291
+ url = path
292
+ path_params = {}
293
+
294
+ # Extract parameters from operation definition
295
+ parameters = operation.get('parameters', [])
296
+ for param in parameters:
297
+ if param.get('in') == 'path':
298
+ name = param.get('name')
299
+ if name in kwargs:
300
+ path_params[name] = kwargs.pop(name)
301
+
302
+ # Replace path parameters in the URL
303
+ for name, value in path_params.items():
304
+ url = url.replace(f"{{{name}}}", str(value))
305
+
306
+ # Build the full URL
307
+ full_url = urljoin(self.base_url, url)
308
+
309
+ # Handle query parameters
310
+ query_params = {}
311
+ for param in parameters:
312
+ if param.get('in') == 'query':
313
+ name = param.get('name')
314
+ if name in kwargs:
315
+ query_params[name] = kwargs.pop(name)
316
+
317
+ # Make the request
318
+ headers = kwargs.pop('headers', {})
319
+
320
+ # Handle request body
321
+ body = kwargs.pop('data', None) or kwargs.pop('body', None)
322
+ # josn body
323
+ if not body and len(kwargs) > 0 and operation.get('requestBody', {}).get('content', {}).get('application/json'):
324
+ body = json.dumps(kwargs)
325
+
326
+ response = await self.session.request(
327
+ method,
328
+ full_url,
329
+ params=query_params,
330
+ json=body,
331
+ headers=headers,
332
+ **kwargs
333
+ )
334
+
335
+ if 'application/json' in response.headers.get('Content-Type', ''):
336
+ result = response.json()
337
+ else:
338
+ result = response.text
339
+
340
+ # Create response object similar to axios
341
+ return {
342
+ 'data': result,
343
+ 'status': response.status_code,
344
+ 'headers': dict(response.headers),
345
+ 'config': kwargs
346
+ }
347
+
348
+ operation_method.__name__ = operation.get('operationId', '')
349
+ operation_method.__doc__ = operation.get('summary', '') + "\n\n" + operation.get('description', '')
350
+ return operation_method
351
+
352
+ async def close(self):
353
+ """Close the HTTP session."""
354
+ if self.session:
355
+ await self.session.aclose()
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
5
5
 
6
6
  setup(
7
7
  name="openapi-httpx-client",
8
- version="0.1.0",
8
+ version="0.2.0",
9
9
  author="lloydzhou",
10
10
  author_email="lloydzhou@qq.com",
11
11
  description="A Python client for OpenAPI specifications using httpx",
@@ -16,6 +16,7 @@ setup(
16
16
  install_requires=[
17
17
  "httpx>=0.23.0",
18
18
  "pyyaml>=6.0",
19
+ "nanoid>=0.2.1",
19
20
  ],
20
21
  classifiers=[
21
22
  "Programming Language :: Python :: 3",
@@ -1,219 +0,0 @@
1
- import httpx
2
- import json
3
- import os.path
4
- from urllib.parse import urljoin, urlparse
5
- import yaml
6
- import types
7
-
8
- class OpenAPIClient:
9
- """
10
- A Python client for OpenAPI specifications, inspired by openapi-client-axios.
11
- Uses httpx for HTTP requests.
12
- """
13
-
14
- def __init__(self, definition=None):
15
- """
16
- Initialize the OpenAPI client.
17
-
18
- Args:
19
- definition: URL or file path to the OpenAPI definition, or a dictionary containing the definition
20
- """
21
- self.definition_source = definition
22
- self.definition = None
23
- self.client = None
24
- self.base_url = ''
25
- self.session = None
26
- self.source_url = None # Store the source URL if loaded from a URL
27
-
28
- async def init(self):
29
- """
30
- Initialize the client by loading and parsing the OpenAPI definition.
31
-
32
- Returns:
33
- DynamicClient: A client with methods generated from the OpenAPI definition
34
- """
35
- # Load the OpenAPI definition
36
- await self.load_definition()
37
-
38
- # Create HTTP session
39
- self.session = httpx.AsyncClient()
40
-
41
- # Set base URL from the servers list if available
42
- self.setup_base_url()
43
-
44
- # Create a dynamic client with methods based on the operations defined in the spec
45
- return await self.create_dynamic_client()
46
-
47
- async def load_definition(self):
48
- """
49
- Load the OpenAPI definition from a URL, file, or dictionary.
50
- """
51
- if isinstance(self.definition_source, dict):
52
- self.definition = self.definition_source
53
- return
54
-
55
- if os.path.isfile(str(self.definition_source)):
56
- # Load from file
57
- with open(self.definition_source, 'r') as f:
58
- content = f.read()
59
- if self.definition_source.endswith('.yaml') or self.definition_source.endswith('.yml'):
60
- self.definition = yaml.safe_load(content)
61
- else:
62
- self.definition = json.loads(content)
63
- return
64
-
65
- # Assume it's a URL
66
- self.source_url = self.definition_source # Store the source URL
67
- async with httpx.AsyncClient() as client:
68
- response = await client.get(self.definition_source)
69
- if response.status_code == 200:
70
- content_type = response.headers.get('Content-Type', '')
71
- if 'yaml' in content_type or 'yml' in content_type:
72
- self.definition = yaml.safe_load(response.text)
73
- else:
74
- self.definition = response.json()
75
- else:
76
- raise Exception(f"Failed to load OpenAPI definition: {response.status_code}")
77
-
78
- def setup_base_url(self):
79
- """
80
- Set up the base URL for API requests, handling various server URL formats.
81
- """
82
- if 'servers' in self.definition and self.definition['servers']:
83
- server_url = self.definition['servers'][0]['url']
84
-
85
- # Check if this is a full URL or just a path
86
- parsed_url = urlparse(server_url)
87
-
88
- # If it's a full URL (has scheme), use it directly
89
- if parsed_url.scheme:
90
- self.base_url = server_url
91
- # If it's not a full URL and we loaded from a URL, combine them
92
- elif self.source_url:
93
- # Parse the source URL to get scheme, hostname, and port
94
- source_parsed = urlparse(self.source_url)
95
- base = f"{source_parsed.scheme}://{source_parsed.netloc}"
96
-
97
- # Combine the base with the server path
98
- self.base_url = urljoin(base, server_url)
99
- else:
100
- # Just use what we have
101
- self.base_url = server_url
102
-
103
- async def create_dynamic_client(self):
104
- """
105
- Create a client with methods dynamically generated from the OpenAPI spec using metaprogramming.
106
-
107
- Returns:
108
- DynamicClient: A client with methods for each operation in the spec
109
- """
110
- # Create a new class dynamically using type
111
- methods_dict = {}
112
-
113
- # Generate methods for each path and operation
114
- paths = self.definition.get('paths', {})
115
- for path, path_item in paths.items():
116
- for method, operation in path_item.items():
117
- if method in ['get', 'post', 'put', 'delete', 'patch', 'options', 'head']:
118
- operation_id = operation.get('operationId')
119
- if operation_id:
120
- # Create a method for this operation and capture it in the closure
121
- method_func = self.create_operation_method(path, method, operation)
122
-
123
- # Create a function with proper binding
124
- def create_bound_method(func=method_func):
125
- async def bound_method(*args, **kwargs):
126
- return await func(*args, **kwargs)
127
- # Set the name and docstring
128
- bound_method.__name__ = operation_id
129
- bound_method.__doc__ = operation.get('summary', '') + "\n\n" + operation.get('description', '')
130
- return bound_method
131
-
132
- methods_dict[operation_id] = create_bound_method()
133
-
134
- # Create the dynamic client class with the methods
135
- DynamicClientClass = type('DynamicClient', (object,), methods_dict)
136
-
137
- # Create an instance of this class
138
- client = DynamicClientClass()
139
-
140
- # Store reference to the api
141
- client._api = self
142
-
143
- return client
144
-
145
- def create_operation_method(self, path, method, operation):
146
- """
147
- Create a method for an operation defined in the OpenAPI spec.
148
-
149
- Args:
150
- path: The path template (e.g., "/pets/{petId}")
151
- method: The HTTP method (e.g., "get", "post")
152
- operation: The operation object from the OpenAPI spec
153
-
154
- Returns:
155
- function: A method that performs the API request
156
- """
157
- async def operation_method(*args, **kwargs):
158
- # Process path parameters
159
- url = path
160
- path_params = {}
161
-
162
- # Extract parameters from operation definition
163
- parameters = operation.get('parameters', [])
164
- for param in parameters:
165
- if param.get('in') == 'path':
166
- name = param.get('name')
167
- if name in kwargs:
168
- path_params[name] = kwargs.pop(name)
169
-
170
- # Replace path parameters in the URL
171
- for name, value in path_params.items():
172
- url = url.replace(f"{{{name}}}", str(value))
173
-
174
- # Build the full URL
175
- full_url = urljoin(self.base_url, url)
176
-
177
- # Handle query parameters
178
- query_params = {}
179
- for param in parameters:
180
- if param.get('in') == 'query':
181
- name = param.get('name')
182
- if name in kwargs:
183
- query_params[name] = kwargs.pop(name)
184
-
185
- # Handle request body
186
- body = kwargs.pop('data', None)
187
-
188
- # Make the request
189
- headers = kwargs.pop('headers', {})
190
-
191
- response = await self.session.request(
192
- method,
193
- full_url,
194
- params=query_params,
195
- json=body,
196
- headers=headers,
197
- **kwargs
198
- )
199
-
200
- if 'application/json' in response.headers.get('Content-Type', ''):
201
- result = response.json()
202
- else:
203
- result = response.text
204
-
205
- # Create response object similar to axios
206
- return {
207
- 'data': result,
208
- 'status': response.status_code,
209
- 'headers': dict(response.headers),
210
- 'config': kwargs
211
- }
212
-
213
- return operation_method
214
-
215
- async def close(self):
216
- """Close the HTTP session."""
217
- if self.session:
218
- await self.session.aclose()
219
-