openapi-httpx-client 0.4.1__tar.gz → 0.4.3__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Lloyd Zhou
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1,17 +1,28 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: openapi-httpx-client
3
- Version: 0.4.1
3
+ Version: 0.4.3
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
10
8
  Classifier: Programming Language :: Python :: 3
11
9
  Classifier: License :: OSI Approved :: MIT License
12
10
  Classifier: Operating System :: OS Independent
13
11
  Requires-Python: >=3.7
14
12
  Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: httpx>=0.23.0
15
+ Requires-Dist: pyyaml>=6.0
16
+ Dynamic: author
17
+ Dynamic: author-email
18
+ Dynamic: classifier
19
+ Dynamic: description
20
+ Dynamic: description-content-type
21
+ Dynamic: home-page
22
+ Dynamic: license-file
23
+ Dynamic: requires-dist
24
+ Dynamic: requires-python
25
+ Dynamic: summary
15
26
 
16
27
  # OpenAPI Client for Python
17
28
 
@@ -47,6 +58,11 @@ async def main():
47
58
  pet = await client.getPetById(petId=1)
48
59
  print(f"Status: {pet['status']}")
49
60
  print(f"Pet data: {pet['data']}")
61
+
62
+ # Call operations directly as methods, using positional arguments, can using in path and query
63
+ pet = await client.getPetById(1)
64
+ print(f"Status: {pet['status']}")
65
+ print(f"Pet data: {pet['data']}")
50
66
 
51
67
  # Alternative way to call methods
52
68
  pet = await client("getPetById", petId=2)
@@ -127,5 +143,3 @@ All API responses are returned in a dictionary format with the following keys:
127
143
  lloydzhou
128
144
 
129
145
 
130
-
131
-
@@ -32,6 +32,11 @@ async def main():
32
32
  pet = await client.getPetById(petId=1)
33
33
  print(f"Status: {pet['status']}")
34
34
  print(f"Pet data: {pet['data']}")
35
+
36
+ # Call operations directly as methods, using positional arguments, can using in path and query
37
+ pet = await client.getPetById(1)
38
+ print(f"Status: {pet['status']}")
39
+ print(f"Pet data: {pet['data']}")
35
40
 
36
41
  # Alternative way to call methods
37
42
  pet = await client("getPetById", petId=2)
@@ -1,17 +1,28 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: openapi-httpx-client
3
- Version: 0.4.1
3
+ Version: 0.4.3
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
10
8
  Classifier: Programming Language :: Python :: 3
11
9
  Classifier: License :: OSI Approved :: MIT License
12
10
  Classifier: Operating System :: OS Independent
13
11
  Requires-Python: >=3.7
14
12
  Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: httpx>=0.23.0
15
+ Requires-Dist: pyyaml>=6.0
16
+ Dynamic: author
17
+ Dynamic: author-email
18
+ Dynamic: classifier
19
+ Dynamic: description
20
+ Dynamic: description-content-type
21
+ Dynamic: home-page
22
+ Dynamic: license-file
23
+ Dynamic: requires-dist
24
+ Dynamic: requires-python
25
+ Dynamic: summary
15
26
 
16
27
  # OpenAPI Client for Python
17
28
 
@@ -47,6 +58,11 @@ async def main():
47
58
  pet = await client.getPetById(petId=1)
48
59
  print(f"Status: {pet['status']}")
49
60
  print(f"Pet data: {pet['data']}")
61
+
62
+ # Call operations directly as methods, using positional arguments, can using in path and query
63
+ pet = await client.getPetById(1)
64
+ print(f"Status: {pet['status']}")
65
+ print(f"Pet data: {pet['data']}")
50
66
 
51
67
  # Alternative way to call methods
52
68
  pet = await client("getPetById", petId=2)
@@ -127,5 +143,3 @@ All API responses are returned in a dictionary format with the following keys:
127
143
  lloydzhou
128
144
 
129
145
 
130
-
131
-
@@ -1,3 +1,4 @@
1
+ LICENSE
1
2
  README.md
2
3
  setup.py
3
4
  openapi_httpx_client.egg-info/PKG-INFO
@@ -3,6 +3,8 @@ import json
3
3
  import os.path
4
4
  from urllib.parse import urljoin, urlparse
5
5
  import yaml
6
+ import re
7
+
6
8
 
7
9
  # 合并DynamicClientBase和BaseClient为一个基类
8
10
  class BaseClient:
@@ -67,13 +69,13 @@ class Client(BaseClient):
67
69
  def __init__(self, api, **kwargs):
68
70
  """Initialize the sync client"""
69
71
  super().__init__(api)
70
- self.session = httpx.Client(**kwargs)
72
+ self.session = api.httpx_client or httpx.Client(**kwargs)
71
73
 
72
74
  def __enter__(self):
73
75
  """Enter context manager and initialize the client"""
74
76
  if not self.api.definition:
75
77
  self.api._load_definition_sync()
76
-
78
+
77
79
  self.setup_base_url()
78
80
  # Generate methods directly on this instance
79
81
  self.api._generate_client_methods(self, is_async=False)
@@ -94,7 +96,7 @@ class AsyncClient(BaseClient):
94
96
  def __init__(self, api, **kwargs):
95
97
  """Initialize the async client"""
96
98
  super().__init__(api)
97
- self.session = httpx.AsyncClient(**kwargs)
99
+ self.session = api.httpx_async_client or httpx.AsyncClient(**kwargs)
98
100
 
99
101
  async def __aenter__(self):
100
102
  """Enter async context manager and initialize the client"""
@@ -112,6 +114,50 @@ class AsyncClient(BaseClient):
112
114
  await self.session.aclose()
113
115
 
114
116
 
117
+ def sanitize_openapi_path(path: str) -> str:
118
+ """
119
+ Convert OpenAPI path (e.g. "/v3/symbols/{symbol}/session") into a valid Python identifier.
120
+ """
121
+
122
+ # replace path parameters {param} with sanitized param name prefixed with "by_"
123
+ def repl_param(m):
124
+ name = m.group(1) or ""
125
+ # sanitize param name: replace non-alnum/_ with underscore and collapse
126
+ name = re.sub(r"[^0-9A-Za-z_]", "_", name)
127
+ name = re.sub(r"_+", "_", name).strip("_")
128
+ return "by_" + name
129
+
130
+ s = re.sub(r"\{([^}]*)\}", repl_param, path)
131
+ # replace slashes/backslashes with underscore
132
+ s = re.sub(r"[\\/]+", "_", s)
133
+ # replace any remaining non-alnum/_ with underscore
134
+ s = re.sub(r"[^0-9A-Za-z_]", "_", s)
135
+ # collapse underscores and strip edges
136
+ s = re.sub(r"_+", "_", s).strip("_")
137
+
138
+ return s
139
+
140
+
141
+ def resolve_open_api_reference(dct, definition):
142
+ if "$ref" not in dct:
143
+ return dct
144
+ else:
145
+ ref_path = dct["$ref"]
146
+ # Only handling local references for simplicity
147
+ if not ref_path.startswith("#/"):
148
+ raise NotImplementedError(
149
+ "Only local references are supported in this implementation."
150
+ )
151
+
152
+ parts = ref_path.lstrip("#/").split("/")
153
+ ref = definition
154
+ for part in parts:
155
+ ref = ref.get(part)
156
+ if ref is None:
157
+ raise ValueError(f"Reference {ref_path} could not be resolved.")
158
+ return ref
159
+
160
+
115
161
  # Create the main OpenAPIClient class as a factory
116
162
  class OpenAPIClient:
117
163
  """
@@ -130,7 +176,7 @@ class OpenAPIClient:
130
176
  result = await client.operation_name(param1=value)
131
177
  """
132
178
 
133
- def __init__(self, definition=None):
179
+ def __init__(self, definition=None, httpx_client=None, httpx_async_client=None):
134
180
  """
135
181
  Initialize the OpenAPI client.
136
182
 
@@ -139,8 +185,10 @@ class OpenAPIClient:
139
185
  """
140
186
  self.definition_source = definition
141
187
  self.definition = {}
142
- self.base_url = ''
188
+ self.base_url = ""
143
189
  self.source_url = None # Store the source URL if loaded from a URL
190
+ self.httpx_client = httpx_client
191
+ self.httpx_async_client = httpx_async_client
144
192
 
145
193
  def Client(self, **kwargs):
146
194
  """
@@ -153,7 +201,7 @@ class OpenAPIClient:
153
201
  Client: A synchronous client
154
202
  """
155
203
  return Client(self, **kwargs)
156
-
204
+
157
205
  def AsyncClient(self, **kwargs):
158
206
  """
159
207
  Create an asynchronous client instance that can be used as a context manager.
@@ -289,38 +337,43 @@ class OpenAPIClient:
289
337
 
290
338
  def create_tool(self, operation_id, operation, all_references):
291
339
  """Create an AI tool description from operation data"""
292
- # Get parameters from the request body schema only for json content
340
+ # Get parameters from the request body schema
293
341
  body = operation.get('requestBody', {})
294
- schema = body.get('content', {}).get('application/json', {}).get('schema', {})
295
- parameters = {
296
- "type": "object",
297
- "required": ['body'] if body.get("required", False) else [],
298
- "description": body.get('description', ''),
299
- "properties": {
300
- "body": self.resolve_schema_ref(schema, all_references) if schema else {},
301
- }
302
- }
342
+ content = body.get('content', {})
343
+ schema = content.get('application/json', {}).get('schema', {}) or content.get('application/xml', {}).get('schema', {}) or content.get('application/x-www-form-urlencoded', {}).get('schema', {})
344
+ json_schema = self.resolve_schema_ref(schema, all_references) if schema else { "type": "object", "properties": {} }
345
+
346
+ if not json_schema.get('description'):
347
+ json_schema['description'] = body.get('description', '')
348
+
303
349
  # add parameters from path and query
304
- for parameter in operation.get('parameters', []):
305
- name = parameter.get('name')
306
- if parameter.get('required', False):
307
- parameters["required"].append(name)
308
- item = {
309
- "type": parameter.get('schema', {}).get('type', 'string'),
310
- "description": parameter.get('description', ''),
311
- }
312
- # Add format, enum, and example if available
313
- for key in ['format', 'enum', 'example']:
314
- if parameter.get('schema', {}).get(key):
315
- item[key] = parameter.get('schema', {}).get(key)
316
- parameters["properties"][name] = item
350
+ parameters = operation.get('parameters', [])
351
+ if len(parameters) > 0:
352
+ if not json_schema.get('required'):
353
+ json_schema['required'] = []
354
+ if not json_schema.get('properties'):
355
+ json_schema['properties'] = {}
356
+ for parameter in parameters:
357
+ name = parameter.get('name')
358
+ if parameter.get('required', False):
359
+ json_schema["required"].append(name)
360
+
361
+ parameter_schema = {
362
+ "type": parameter.get('schema', {}).get('type', 'string'),
363
+ "description": parameter.get('description', ''),
364
+ }
365
+ # Add format, enum, and example if available
366
+ for key in ['format', 'enum', 'example']:
367
+ if parameter.get('schema', {}).get(key):
368
+ parameter_schema[key] = parameter.get('schema', {}).get(key)
369
+ json_schema["properties"][name] = parameter_schema
317
370
 
318
371
  return {
319
372
  "type": "function",
320
373
  "function": {
321
374
  "name": operation_id,
322
375
  "description": operation.get('summary', '') or operation.get('description', ''),
323
- "parameters": parameters,
376
+ "parameters": json_schema,
324
377
  }
325
378
  }
326
379
 
@@ -348,7 +401,9 @@ class OpenAPIClient:
348
401
  for operation in self.get_operations():
349
402
  operation_id = operation.get('operationId')
350
403
  if not operation_id:
351
- continue
404
+ operation_id = (
405
+ operation["method"] + "_" + sanitize_openapi_path(operation["path"])
406
+ )
352
407
 
353
408
  path = operation.get('path')
354
409
  paths.append(path)
@@ -374,13 +429,14 @@ class OpenAPIClient:
374
429
  client_instance.paths = paths
375
430
  client_instance.tools = tools
376
431
 
377
- def _prepare_request_params(self, path, operation, kwargs):
432
+ def _prepare_request_params(self, path, operation, args, kwargs):
378
433
  """
379
434
  Prepare request parameters for an API operation.
380
435
 
381
436
  Args:
382
437
  path: The path template
383
438
  operation: Operation object
439
+ args: Positional arguments passed to the operation
384
440
  kwargs: Keyword arguments passed to the operation
385
441
 
386
442
  Returns:
@@ -393,10 +449,13 @@ class OpenAPIClient:
393
449
  # Extract parameters from operation definition
394
450
  parameters = operation.get('parameters', [])
395
451
  for param in parameters:
452
+ param = resolve_open_api_reference(param, self.definition)
396
453
  if param.get('in') == 'path':
397
454
  name = param.get('name')
398
455
  if name in kwargs:
399
456
  path_params[name] = kwargs.pop(name)
457
+ elif len(args) > 0:
458
+ path_params[name] = args.pop(0) # Pop the first positional argument
400
459
 
401
460
  # Replace path parameters in the URL
402
461
  for name, value in path_params.items():
@@ -408,10 +467,13 @@ class OpenAPIClient:
408
467
  # Handle query parameters
409
468
  query_params = {}
410
469
  for param in parameters:
470
+ param = resolve_open_api_reference(param, self.definition)
411
471
  if param.get('in') == 'query':
412
472
  name = param.get('name')
413
473
  if name in kwargs:
414
474
  query_params[name] = kwargs.pop(name)
475
+ elif len(args) > 0:
476
+ query_params[name] = args.pop(0) # Pop the first positional argument
415
477
 
416
478
  # Handle headers
417
479
  headers = kwargs.pop('headers', {})
@@ -419,7 +481,7 @@ class OpenAPIClient:
419
481
  # Handle request body
420
482
  body = kwargs.pop('data', None) or kwargs.pop('body', None)
421
483
  # json body
422
- if not body and len(kwargs) > 0 and operation.get('requestBody', {}).get('content', {}).get('application/json'):
484
+ if not body and len(kwargs) > 0 and operation.get('requestBody', {}).get('content', {}):
423
485
  body = kwargs.copy()
424
486
  kwargs.clear() # Clear the kwargs after using them as body
425
487
 
@@ -466,17 +528,17 @@ class OpenAPIClient:
466
528
  async def operation_method(*args, **kwargs):
467
529
  # Prepare request parameters
468
530
  full_url, query_params, body, headers, remaining_kwargs = self._prepare_request_params(
469
- path, operation, kwargs.copy()
531
+ path, operation, list(args), kwargs.copy()
470
532
  )
471
533
 
472
534
  # Make the async request
473
535
  response = await client_instance.session.request(
474
536
  method,
475
537
  full_url,
476
- params=query_params,
477
- json=body,
478
- headers=headers,
479
- **remaining_kwargs
538
+ params=query_params,
539
+ json=body,
540
+ headers=dict(client_instance.session.headers) | headers,
541
+ **remaining_kwargs,
480
542
  )
481
543
 
482
544
  # Process the response
@@ -485,17 +547,17 @@ class OpenAPIClient:
485
547
  def operation_method(*args, **kwargs):
486
548
  # Prepare request parameters
487
549
  full_url, query_params, body, headers, remaining_kwargs = self._prepare_request_params(
488
- path, operation, kwargs.copy()
550
+ path, operation, list(args), kwargs.copy()
489
551
  )
490
552
 
491
553
  # Make the sync request
492
554
  response = client_instance.session.request(
493
555
  method,
494
556
  full_url,
495
- params=query_params,
496
- json=body,
497
- headers=headers,
498
- **remaining_kwargs
557
+ params=query_params,
558
+ json=body,
559
+ headers=dict(client_instance.session.headers) | headers,
560
+ **remaining_kwargs,
499
561
  )
500
562
 
501
563
  # Process the response
@@ -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.4.1",
8
+ version="0.4.3",
9
9
  author="lloydzhou",
10
10
  author_email="lloydzhou@qq.com",
11
11
  description="A Python client for OpenAPI specifications using httpx",