openapi-httpx-client 0.1.1__tar.gz → 0.2.1__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.
- {openapi_httpx_client-0.1.1 → openapi-httpx-client-0.2.1}/PKG-INFO +12 -13
- {openapi_httpx_client-0.1.1 → openapi-httpx-client-0.2.1}/README.md +6 -0
- {openapi_httpx_client-0.1.1 → openapi-httpx-client-0.2.1}/openapi_httpx_client.egg-info/PKG-INFO +12 -13
- {openapi_httpx_client-0.1.1 → openapi-httpx-client-0.2.1}/openapi_httpx_client.egg-info/requires.txt +1 -0
- openapi-httpx-client-0.2.1/openapiclient/client.py +355 -0
- {openapi_httpx_client-0.1.1 → openapi-httpx-client-0.2.1}/setup.py +2 -1
- openapi_httpx_client-0.1.1/openapiclient/client.py +0 -219
- {openapi_httpx_client-0.1.1 → openapi-httpx-client-0.2.1}/openapi_httpx_client.egg-info/SOURCES.txt +0 -0
- {openapi_httpx_client-0.1.1 → openapi-httpx-client-0.2.1}/openapi_httpx_client.egg-info/dependency_links.txt +0 -0
- {openapi_httpx_client-0.1.1 → openapi-httpx-client-0.2.1}/openapi_httpx_client.egg-info/top_level.txt +0 -0
- {openapi_httpx_client-0.1.1 → openapi-httpx-client-0.2.1}/openapiclient/__init__.py +0 -0
- {openapi_httpx_client-0.1.1 → openapi-httpx-client-0.2.1}/setup.cfg +0 -0
|
@@ -1,26 +1,17 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: openapi-httpx-client
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.1
|
|
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
|
|
|
@@ -45,6 +36,12 @@ async def main():
|
|
|
45
36
|
try:
|
|
46
37
|
# Initialize and get the dynamic client
|
|
47
38
|
client = await api.init()
|
|
39
|
+
print("client", client, dir(client))
|
|
40
|
+
|
|
41
|
+
print("client.operations", client.operations)
|
|
42
|
+
print("client.paths", client.paths)
|
|
43
|
+
print("client.functions", client.functions)
|
|
44
|
+
print("client.tools", client.tools) # get function call tools
|
|
48
45
|
|
|
49
46
|
# Call an operation using the generated method
|
|
50
47
|
response = await client.getPetById(petId=1)
|
|
@@ -73,3 +70,5 @@ if __name__ == "__main__":
|
|
|
73
70
|
lloydzhou (2025-02-27)
|
|
74
71
|
|
|
75
72
|
|
|
73
|
+
|
|
74
|
+
|
|
@@ -21,6 +21,12 @@ async def main():
|
|
|
21
21
|
try:
|
|
22
22
|
# Initialize and get the dynamic client
|
|
23
23
|
client = await api.init()
|
|
24
|
+
print("client", client, dir(client))
|
|
25
|
+
|
|
26
|
+
print("client.operations", client.operations)
|
|
27
|
+
print("client.paths", client.paths)
|
|
28
|
+
print("client.functions", client.functions)
|
|
29
|
+
print("client.tools", client.tools) # get function call tools
|
|
24
30
|
|
|
25
31
|
# Call an operation using the generated method
|
|
26
32
|
response = await client.getPetById(petId=1)
|
{openapi_httpx_client-0.1.1 → openapi-httpx-client-0.2.1}/openapi_httpx_client.egg-info/PKG-INFO
RENAMED
|
@@ -1,26 +1,17 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: openapi-httpx-client
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.1
|
|
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
|
|
|
@@ -45,6 +36,12 @@ async def main():
|
|
|
45
36
|
try:
|
|
46
37
|
# Initialize and get the dynamic client
|
|
47
38
|
client = await api.init()
|
|
39
|
+
print("client", client, dir(client))
|
|
40
|
+
|
|
41
|
+
print("client.operations", client.operations)
|
|
42
|
+
print("client.paths", client.paths)
|
|
43
|
+
print("client.functions", client.functions)
|
|
44
|
+
print("client.tools", client.tools) # get function call tools
|
|
48
45
|
|
|
49
46
|
# Call an operation using the generated method
|
|
50
47
|
response = await client.getPetById(petId=1)
|
|
@@ -73,3 +70,5 @@ if __name__ == "__main__":
|
|
|
73
70
|
lloydzhou (2025-02-27)
|
|
74
71
|
|
|
75
72
|
|
|
73
|
+
|
|
74
|
+
|
|
@@ -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.
|
|
8
|
+
version="0.2.1",
|
|
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
|
-
|
{openapi_httpx_client-0.1.1 → openapi-httpx-client-0.2.1}/openapi_httpx_client.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|