langchain-openapi-tools 1.0.0__tar.gz → 1.0.2__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.
Files changed (137) hide show
  1. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/.github/workflows/publish.yml +3 -2
  2. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/PKG-INFO +29 -2
  3. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/README.md +28 -1
  4. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/langchain_openapi/__init__.py +2 -0
  5. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/langchain_openapi/loader.py +5 -0
  6. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/langchain_openapi/parser.py +13 -1
  7. langchain_openapi_tools-1.0.2/langchain_openapi/swagger.py +370 -0
  8. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/langchain_openapi/utils.py +7 -7
  9. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/pyproject.toml +1 -1
  10. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/api/loader/index.html +40 -20
  11. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/api/parser/index.html +446 -434
  12. langchain_openapi_tools-1.0.2/site/search/search_index.json +1 -0
  13. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/tests/test_loader.py +7 -9
  14. langchain_openapi_tools-1.0.2/tests/test_swagger.py +215 -0
  15. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/uv.lock +2 -2
  16. langchain_openapi_tools-1.0.0/site/search/search_index.json +0 -1
  17. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  18. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/.github/ISSUE_TEMPLATE/documentation.md +0 -0
  19. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  20. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  21. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/.github/dependabot.yml +0 -0
  22. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/.github/workflows/ci.yml +0 -0
  23. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/.github/workflows/pages.yml +0 -0
  24. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/.github/workflows/release.yml +0 -0
  25. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/.gitignore +0 -0
  26. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/.pre-commit-config.yaml +0 -0
  27. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/ARCHITECTURE.md +0 -0
  28. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/CHANGELOG.md +0 -0
  29. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/CODE_OF_CONDUCT.md +0 -0
  30. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/CONTRIBUTING.md +0 -0
  31. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/LICENSE +0 -0
  32. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/Makefile +0 -0
  33. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/benchmarks/benchmark.py +0 -0
  34. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/docs/api/executor.md +0 -0
  35. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/docs/api/loader.md +0 -0
  36. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/docs/api/middleware.md +0 -0
  37. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/docs/api/parser.md +0 -0
  38. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/docs/api/toolkit.md +0 -0
  39. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/docs/architecture.md +0 -0
  40. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/docs/authentication.md +0 -0
  41. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/docs/contributing.md +0 -0
  42. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/docs/examples/crossref.md +0 -0
  43. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/docs/examples/petstore.md +0 -0
  44. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/docs/index.md +0 -0
  45. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/docs/installation.md +0 -0
  46. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/docs/middleware.md +0 -0
  47. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/docs/quickstart.md +0 -0
  48. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/docs/toolkit.md +0 -0
  49. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/examples/README.md +0 -0
  50. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/examples/crossref/README.md +0 -0
  51. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/examples/crossref/crossref.json +0 -0
  52. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/examples/crossref/main.py +0 -0
  53. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/examples/crossref/requirements.txt +0 -0
  54. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/examples/petstore/README.md +0 -0
  55. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/examples/petstore/main.py +0 -0
  56. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/examples/petstore/petstore.json +0 -0
  57. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/examples/petstore/requirements.txt +0 -0
  58. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/langchain_openapi/enums.py +0 -0
  59. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/langchain_openapi/exceptions.py +0 -0
  60. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/langchain_openapi/executor.py +0 -0
  61. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/langchain_openapi/middleware.py +0 -0
  62. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/langchain_openapi/models.py +0 -0
  63. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/langchain_openapi/providers.py +0 -0
  64. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/langchain_openapi/py.typed +0 -0
  65. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/langchain_openapi/schema_converter.py +0 -0
  66. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/langchain_openapi/toolkit.py +0 -0
  67. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/mkdocs.yml +0 -0
  68. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/404.html +0 -0
  69. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/api/executor/index.html +0 -0
  70. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/api/middleware/index.html +0 -0
  71. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/api/toolkit/index.html +0 -0
  72. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/architecture/index.html +0 -0
  73. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/_mkdocstrings.css +0 -0
  74. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/images/favicon.png +0 -0
  75. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/javascripts/bundle.d7400e89.min.js +0 -0
  76. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/javascripts/bundle.d7400e89.min.js.map +0 -0
  77. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/javascripts/lunr/min/lunr.ar.min.js +0 -0
  78. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/javascripts/lunr/min/lunr.da.min.js +0 -0
  79. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/javascripts/lunr/min/lunr.de.min.js +0 -0
  80. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/javascripts/lunr/min/lunr.du.min.js +0 -0
  81. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/javascripts/lunr/min/lunr.el.min.js +0 -0
  82. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/javascripts/lunr/min/lunr.es.min.js +0 -0
  83. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/javascripts/lunr/min/lunr.fi.min.js +0 -0
  84. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/javascripts/lunr/min/lunr.fr.min.js +0 -0
  85. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/javascripts/lunr/min/lunr.he.min.js +0 -0
  86. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/javascripts/lunr/min/lunr.hi.min.js +0 -0
  87. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/javascripts/lunr/min/lunr.hu.min.js +0 -0
  88. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/javascripts/lunr/min/lunr.hy.min.js +0 -0
  89. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/javascripts/lunr/min/lunr.it.min.js +0 -0
  90. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/javascripts/lunr/min/lunr.ja.min.js +0 -0
  91. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/javascripts/lunr/min/lunr.jp.min.js +0 -0
  92. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/javascripts/lunr/min/lunr.kn.min.js +0 -0
  93. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/javascripts/lunr/min/lunr.ko.min.js +0 -0
  94. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/javascripts/lunr/min/lunr.multi.min.js +0 -0
  95. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/javascripts/lunr/min/lunr.nl.min.js +0 -0
  96. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/javascripts/lunr/min/lunr.no.min.js +0 -0
  97. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/javascripts/lunr/min/lunr.pt.min.js +0 -0
  98. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/javascripts/lunr/min/lunr.ro.min.js +0 -0
  99. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/javascripts/lunr/min/lunr.ru.min.js +0 -0
  100. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/javascripts/lunr/min/lunr.sa.min.js +0 -0
  101. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +0 -0
  102. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/javascripts/lunr/min/lunr.sv.min.js +0 -0
  103. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/javascripts/lunr/min/lunr.ta.min.js +0 -0
  104. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/javascripts/lunr/min/lunr.te.min.js +0 -0
  105. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/javascripts/lunr/min/lunr.th.min.js +0 -0
  106. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/javascripts/lunr/min/lunr.tr.min.js +0 -0
  107. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/javascripts/lunr/min/lunr.vi.min.js +0 -0
  108. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/javascripts/lunr/min/lunr.zh.min.js +0 -0
  109. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/javascripts/lunr/tinyseg.js +0 -0
  110. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/javascripts/lunr/wordcut.js +0 -0
  111. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/javascripts/workers/search.2c215733.min.js +0 -0
  112. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/javascripts/workers/search.2c215733.min.js.map +0 -0
  113. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/stylesheets/main.ec1eaa64.min.css +0 -0
  114. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/stylesheets/main.ec1eaa64.min.css.map +0 -0
  115. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/stylesheets/palette.ab4e12ef.min.css +0 -0
  116. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/assets/stylesheets/palette.ab4e12ef.min.css.map +0 -0
  117. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/authentication/index.html +0 -0
  118. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/contributing/index.html +0 -0
  119. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/examples/crossref/index.html +0 -0
  120. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/examples/petstore/index.html +0 -0
  121. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/index.html +0 -0
  122. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/installation/index.html +0 -0
  123. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/middleware/index.html +0 -0
  124. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/objects.inv +0 -0
  125. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/quickstart/index.html +0 -0
  126. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/sitemap.xml +0 -0
  127. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/sitemap.xml.gz +0 -0
  128. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/site/toolkit/index.html +0 -0
  129. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/tests/test_e2e.py +0 -0
  130. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/tests/test_executor.py +0 -0
  131. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/tests/test_import.py +0 -0
  132. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/tests/test_middleware.py +0 -0
  133. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/tests/test_parser.py +0 -0
  134. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/tests/test_providers.py +0 -0
  135. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/tests/test_resolver.py +0 -0
  136. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/tests/test_schema_converter.py +0 -0
  137. {langchain_openapi_tools-1.0.0 → langchain_openapi_tools-1.0.2}/tests/test_toolkit.py +0 -0
@@ -1,8 +1,9 @@
1
1
  name: Publish to PyPI
2
2
 
3
3
  on:
4
- release:
5
- types: [published]
4
+ push:
5
+ tags:
6
+ - "v*"
6
7
 
7
8
  permissions:
8
9
  contents: read
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langchain-openapi-tools
3
- Version: 1.0.0
3
+ Version: 1.0.2
4
4
  Summary: Convert OpenAPI specifications into native, production-grade LangChain tools.
5
5
  Project-URL: Homepage, https://github.com/abhaywani114/langchain-openapi
6
6
  Project-URL: Documentation, https://abhaywani114.github.io/langchain-openapi/
@@ -79,12 +79,39 @@ No python code generation is required—tools are generated dynamically at runti
79
79
 
80
80
  ## Supported Specifications
81
81
 
82
+ - ✅ **Swagger 2.0** (Automatically normalized to OpenAPI 3.0)
82
83
  - ✅ **OpenAPI 3.0.x** (JSON and YAML)
83
84
  - ✅ **OpenAPI 3.1.x** (JSON and YAML)
84
- - ❌ **Swagger 2.0** (Explicitly rejected with descriptive error)
85
85
 
86
86
  ---
87
87
 
88
+ ## Architecture Pipeline
89
+
90
+ ```text
91
+ Swagger 2.0 / OpenAPI 3.x
92
+
93
+
94
+ Swagger Normalizer
95
+
96
+
97
+ Normalized OpenAPI Model
98
+
99
+
100
+ Existing Parser
101
+
102
+
103
+ Internal Operation Models
104
+
105
+
106
+ Schema Converter
107
+
108
+
109
+ HTTP Executor
110
+
111
+
112
+ LangChain StructuredTools
113
+ ```
114
+
88
115
  ## Quick Start
89
116
 
90
117
  ### Installation
@@ -39,12 +39,39 @@ No python code generation is required—tools are generated dynamically at runti
39
39
 
40
40
  ## Supported Specifications
41
41
 
42
+ - ✅ **Swagger 2.0** (Automatically normalized to OpenAPI 3.0)
42
43
  - ✅ **OpenAPI 3.0.x** (JSON and YAML)
43
44
  - ✅ **OpenAPI 3.1.x** (JSON and YAML)
44
- - ❌ **Swagger 2.0** (Explicitly rejected with descriptive error)
45
45
 
46
46
  ---
47
47
 
48
+ ## Architecture Pipeline
49
+
50
+ ```text
51
+ Swagger 2.0 / OpenAPI 3.x
52
+
53
+
54
+ Swagger Normalizer
55
+
56
+
57
+ Normalized OpenAPI Model
58
+
59
+
60
+ Existing Parser
61
+
62
+
63
+ Internal Operation Models
64
+
65
+
66
+ Schema Converter
67
+
68
+
69
+ HTTP Executor
70
+
71
+
72
+ LangChain StructuredTools
73
+ ```
74
+
48
75
  ## Quick Start
49
76
 
50
77
  ### Installation
@@ -69,6 +69,7 @@ from langchain_openapi.schema_converter import (
69
69
  SchemaConverter,
70
70
  map_schema_type_to_python,
71
71
  )
72
+ from langchain_openapi.swagger import SwaggerNormalizer
72
73
  from langchain_openapi.toolkit import (
73
74
  LangChainToolFactory,
74
75
  OpenAPIToolCallbackHandler,
@@ -132,6 +133,7 @@ __all__ = [
132
133
  "SchemaConverter",
133
134
  "SpecLoadError",
134
135
  "StaticHeadersProvider",
136
+ "SwaggerNormalizer",
135
137
  "UnsupportedVersionError",
136
138
  "__version__",
137
139
  "build_tool_description",
@@ -137,6 +137,11 @@ class OpenAPILoader:
137
137
  """
138
138
  raw_data = self._load_fn()
139
139
  validate_raw_spec(raw_data)
140
+ swagger_ver = str(raw_data.get("swagger", ""))
141
+ if raw_data.get("swagger") == "2.0" or swagger_ver.startswith("2."):
142
+ from langchain_openapi.swagger import SwaggerNormalizer
143
+
144
+ raw_data = SwaggerNormalizer(raw_data).normalize()
140
145
  spec = OpenAPISpec.from_dict(raw_data)
141
146
 
142
147
  logger.info(
@@ -40,7 +40,13 @@ class OpenAPISpec:
40
40
  @classmethod
41
41
  def from_dict(cls, spec_dict: dict[str, Any]) -> "OpenAPISpec":
42
42
  """Construct an OpenAPISpec instance from a raw dictionary."""
43
- version = str(spec_dict.get("openapi", ""))
43
+ swagger_ver = str(spec_dict.get("swagger", ""))
44
+ if spec_dict.get("swagger") == "2.0" or swagger_ver.startswith("2."):
45
+ from langchain_openapi.swagger import SwaggerNormalizer
46
+
47
+ spec_dict = SwaggerNormalizer(spec_dict).normalize()
48
+
49
+ version = str(spec_dict.get("openapi", "3.0.0"))
44
50
  info = spec_dict.get("info")
45
51
  if not isinstance(info, dict):
46
52
  info = {}
@@ -170,6 +176,12 @@ class OpenAPIParser:
170
176
  """Parser for converting an OpenAPISpec into normalized internal Python models."""
171
177
 
172
178
  def __init__(self, spec: OpenAPISpec) -> None:
179
+ swagger_ver = str(spec.raw.get("swagger", ""))
180
+ if spec.raw.get("swagger") == "2.0" or swagger_ver.startswith("2."):
181
+ from langchain_openapi.swagger import SwaggerNormalizer
182
+
183
+ normalized_raw = SwaggerNormalizer(spec.raw).normalize()
184
+ spec = OpenAPISpec.from_dict(normalized_raw)
173
185
  self._spec = spec
174
186
  self._resolver = ReferenceResolver(spec.raw)
175
187
 
@@ -0,0 +1,370 @@
1
+ """Swagger 2.0 to OpenAPI 3.0 specification normalizer."""
2
+
3
+ import copy
4
+ from typing import Any
5
+
6
+
7
+ class SwaggerNormalizer:
8
+ """Normalizes raw Swagger 2.0 specification dictionary into OpenAPI 3.0 format."""
9
+
10
+ def __init__(self, raw_spec: dict[str, Any]) -> None:
11
+ self.raw = copy.deepcopy(raw_spec)
12
+
13
+ def normalize(self) -> dict[str, Any]:
14
+ """Convert Swagger 2.0 spec dictionary into an OpenAPI 3.0 compliant dictionary.
15
+
16
+ Returns:
17
+ Normalized OpenAPI 3.0 specification dictionary.
18
+ """
19
+ spec = self.raw
20
+ version = str(spec.get("swagger", ""))
21
+ if not (spec.get("swagger") == "2.0" or version.startswith("2.")):
22
+ return spec
23
+
24
+ normalized: dict[str, Any] = {
25
+ "openapi": "3.0.3",
26
+ "info": spec.get("info", {}),
27
+ }
28
+
29
+ if "tags" in spec:
30
+ normalized["tags"] = spec["tags"]
31
+
32
+ # 1. Servers
33
+ servers = self._convert_servers(spec)
34
+ if servers:
35
+ normalized["servers"] = servers
36
+
37
+ # 2. Components
38
+ components = self._convert_components(spec)
39
+ if components:
40
+ normalized["components"] = components
41
+
42
+ # 3. Security Requirements
43
+ if "security" in spec:
44
+ normalized["security"] = spec["security"]
45
+
46
+ # 4. Paths
47
+ global_consumes = spec.get("consumes", ["application/json"])
48
+ global_produces = spec.get("produces", ["application/json"])
49
+ paths = spec.get("paths", {})
50
+ normalized["paths"] = self._convert_paths(
51
+ paths, global_consumes, global_produces
52
+ )
53
+
54
+ # 5. Reference Rewriting ($ref)
55
+ normalized = self._rewrite_references(normalized)
56
+
57
+ return normalized
58
+
59
+ def _convert_servers(self, spec: dict[str, Any]) -> list[dict[str, str]]:
60
+ host = spec.get("host", "").strip()
61
+ base_path = spec.get("basePath", "").strip()
62
+ schemes = spec.get("schemes", [])
63
+
64
+ if not isinstance(schemes, list) or not schemes:
65
+ schemes = ["https"]
66
+
67
+ if not base_path.startswith("/"):
68
+ base_path = f"/{base_path}" if base_path else ""
69
+ if base_path == "/":
70
+ base_path = ""
71
+
72
+ servers: list[dict[str, str]] = []
73
+ if host:
74
+ for scheme in schemes:
75
+ url = f"{scheme}://{host}{base_path}"
76
+ servers.append({"url": url})
77
+ elif base_path:
78
+ servers.append({"url": base_path})
79
+
80
+ return servers
81
+
82
+ def _convert_components(self, spec: dict[str, Any]) -> dict[str, Any]:
83
+ components: dict[str, Any] = {}
84
+
85
+ if "definitions" in spec and isinstance(spec["definitions"], dict):
86
+ components["schemas"] = spec["definitions"]
87
+
88
+ if "parameters" in spec and isinstance(spec["parameters"], dict):
89
+ converted_params: dict[str, Any] = {}
90
+ for param_name, param_obj in spec["parameters"].items():
91
+ if isinstance(param_obj, dict):
92
+ converted_params[param_name] = self._convert_parameter(param_obj)
93
+ components["parameters"] = converted_params
94
+
95
+ if "responses" in spec and isinstance(spec["responses"], dict):
96
+ converted_responses: dict[str, Any] = {}
97
+ global_produces = spec.get("produces", ["application/json"])
98
+ for resp_name, resp_obj in spec["responses"].items():
99
+ if isinstance(resp_obj, dict):
100
+ converted_responses[resp_name] = self._convert_response(
101
+ resp_obj, global_produces
102
+ )
103
+ components["responses"] = converted_responses
104
+
105
+ if "securityDefinitions" in spec and isinstance(
106
+ spec["securityDefinitions"], dict
107
+ ):
108
+ components["securitySchemes"] = self._convert_security_definitions(
109
+ spec["securityDefinitions"]
110
+ )
111
+
112
+ return components
113
+
114
+ def _convert_security_definitions(self, sec_defs: dict[str, Any]) -> dict[str, Any]:
115
+ schemes: dict[str, Any] = {}
116
+ for name, sec in sec_defs.items():
117
+ if not isinstance(sec, dict):
118
+ continue
119
+ sec_type = sec.get("type")
120
+ if sec_type == "basic":
121
+ schemes[name] = {"type": "http", "scheme": "basic"}
122
+ elif sec_type == "apiKey":
123
+ schemes[name] = {
124
+ "type": "apiKey",
125
+ "name": sec.get("name", name),
126
+ "in": sec.get("in", "header"),
127
+ }
128
+ elif sec_type == "oauth2":
129
+ flow = sec.get("flow")
130
+ flow_key = flow
131
+ if flow == "accessCode":
132
+ flow_key = "authorizationCode"
133
+ elif flow == "application":
134
+ flow_key = "clientCredentials"
135
+
136
+ flow_obj: dict[str, Any] = {
137
+ "scopes": sec.get("scopes", {}),
138
+ }
139
+ if sec.get("authorizationUrl"):
140
+ flow_obj["authorizationUrl"] = sec["authorizationUrl"]
141
+ if sec.get("tokenUrl"):
142
+ flow_obj["tokenUrl"] = sec["tokenUrl"]
143
+
144
+ schemes[name] = {
145
+ "type": "oauth2",
146
+ "flows": {flow_key: flow_obj} if flow_key else {},
147
+ }
148
+ else:
149
+ schemes[name] = sec
150
+ if "description" in sec:
151
+ schemes[name]["description"] = sec["description"]
152
+ return schemes
153
+
154
+ def _convert_paths(
155
+ self,
156
+ paths: dict[str, Any],
157
+ global_consumes: list[str],
158
+ global_produces: list[str],
159
+ ) -> dict[str, Any]:
160
+ converted_paths: dict[str, Any] = {}
161
+
162
+ for path_str, path_item in paths.items():
163
+ if not isinstance(path_item, dict):
164
+ continue
165
+
166
+ new_path_item: dict[str, Any] = {}
167
+ path_parameters = path_item.get("parameters", [])
168
+
169
+ for key, val in path_item.items():
170
+ if key in (
171
+ "get",
172
+ "post",
173
+ "put",
174
+ "delete",
175
+ "patch",
176
+ "options",
177
+ "head",
178
+ "trace",
179
+ ):
180
+ if isinstance(val, dict):
181
+ new_path_item[key] = self._convert_operation(
182
+ val, global_consumes, global_produces, path_parameters
183
+ )
184
+ elif key != "parameters":
185
+ new_path_item[key] = val
186
+
187
+ converted_paths[path_str] = new_path_item
188
+
189
+ return converted_paths
190
+
191
+ def _convert_operation(
192
+ self,
193
+ operation: dict[str, Any],
194
+ global_consumes: list[str],
195
+ global_produces: list[str],
196
+ path_parameters: list[Any],
197
+ ) -> dict[str, Any]:
198
+ op = copy.deepcopy(operation)
199
+ op_consumes = op.pop("consumes", global_consumes)
200
+ op_produces = op.pop("produces", global_produces)
201
+
202
+ # Merge path parameters and operation parameters
203
+ all_params = list(path_parameters) + op.get("parameters", [])
204
+ normal_params: list[dict[str, Any]] = []
205
+ body_param: dict[str, Any] | None = None
206
+ form_params: list[dict[str, Any]] = []
207
+
208
+ for p in all_params:
209
+ if not isinstance(p, dict):
210
+ continue
211
+ p_in = p.get("in")
212
+ if p_in == "body":
213
+ body_param = p
214
+ elif p_in == "formData":
215
+ form_params.append(p)
216
+ else:
217
+ normal_params.append(self._convert_parameter(p))
218
+
219
+ op["parameters"] = normal_params
220
+
221
+ # Convert Request Body (from in: body or in: formData)
222
+ if body_param:
223
+ op["requestBody"] = self._convert_body_parameter(body_param, op_consumes)
224
+ elif form_params:
225
+ op["requestBody"] = self._convert_form_parameters(form_params, op_consumes)
226
+
227
+ # Convert Responses
228
+ if "responses" in op and isinstance(op["responses"], dict):
229
+ converted_resps: dict[str, Any] = {}
230
+ for code, resp in op["responses"].items():
231
+ if isinstance(resp, dict):
232
+ converted_resps[str(code)] = self._convert_response(
233
+ resp, op_produces
234
+ )
235
+ op["responses"] = converted_resps
236
+
237
+ return op
238
+
239
+ def _convert_parameter(self, param: dict[str, Any]) -> dict[str, Any]:
240
+ p = copy.deepcopy(param)
241
+ if "schema" in p:
242
+ return p
243
+
244
+ # Move top-level schema attributes into schema dict
245
+ schema: dict[str, Any] = {}
246
+ for attr in (
247
+ "type",
248
+ "format",
249
+ "items",
250
+ "enum",
251
+ "default",
252
+ "minimum",
253
+ "maximum",
254
+ "minLength",
255
+ "maxLength",
256
+ "pattern",
257
+ "minItems",
258
+ "maxItems",
259
+ "uniqueItems",
260
+ "multipleOf",
261
+ ):
262
+ if attr in p:
263
+ schema[attr] = p.pop(attr)
264
+
265
+ if schema:
266
+ p["schema"] = schema
267
+ return p
268
+
269
+ def _convert_body_parameter(
270
+ self, body_param: dict[str, Any], consumes: list[str]
271
+ ) -> dict[str, Any]:
272
+ schema = body_param.get("schema", {})
273
+ content: dict[str, Any] = {}
274
+
275
+ media_types = consumes if consumes else ["application/json"]
276
+ for media_type in media_types:
277
+ content[media_type] = {"schema": schema}
278
+
279
+ req_body: dict[str, Any] = {"content": content}
280
+ if "description" in body_param:
281
+ req_body["description"] = body_param["description"]
282
+ if "required" in body_param:
283
+ req_body["required"] = body_param["required"]
284
+
285
+ return req_body
286
+
287
+ def _convert_form_parameters(
288
+ self, form_params: list[dict[str, Any]], consumes: list[str]
289
+ ) -> dict[str, Any]:
290
+ properties: dict[str, Any] = {}
291
+ required: list[str] = []
292
+ has_file = False
293
+
294
+ for p in form_params:
295
+ name = p.get("name")
296
+ if not name:
297
+ continue
298
+ if p.get("required"):
299
+ required.append(name)
300
+
301
+ p_type = p.get("type", "string")
302
+ if p_type == "file":
303
+ has_file = True
304
+ prop_schema: dict[str, Any] = {"type": "string", "format": "binary"}
305
+ else:
306
+ prop_schema = {}
307
+ for attr in (
308
+ "type",
309
+ "format",
310
+ "items",
311
+ "enum",
312
+ "default",
313
+ "description",
314
+ ):
315
+ if attr in p:
316
+ prop_schema[attr] = p[attr]
317
+ properties[name] = prop_schema
318
+
319
+ schema: dict[str, Any] = {
320
+ "type": "object",
321
+ "properties": properties,
322
+ }
323
+ if required:
324
+ schema["required"] = required
325
+
326
+ media_type = (
327
+ "multipart/form-data"
328
+ if has_file
329
+ else (consumes[0] if consumes else "application/x-www-form-urlencoded")
330
+ )
331
+
332
+ return {
333
+ "required": bool(required),
334
+ "content": {media_type: {"schema": schema}},
335
+ }
336
+
337
+ def _convert_response(
338
+ self, resp: dict[str, Any], produces: list[str]
339
+ ) -> dict[str, Any]:
340
+ r = copy.deepcopy(resp)
341
+ if "schema" in r:
342
+ schema = r.pop("schema")
343
+ content: dict[str, Any] = {}
344
+ media_types = produces if produces else ["application/json"]
345
+ for media_type in media_types:
346
+ content[media_type] = {"schema": schema}
347
+ r["content"] = content
348
+ return r
349
+
350
+ def _rewrite_references(self, obj: Any) -> Any:
351
+ if isinstance(obj, dict):
352
+ new_dict: dict[str, Any] = {}
353
+ for k, v in obj.items():
354
+ if k == "$ref" and isinstance(v, str):
355
+ ref_str = v
356
+ ref_str = ref_str.replace("#/definitions/", "#/components/schemas/")
357
+ ref_str = ref_str.replace(
358
+ "#/parameters/", "#/components/parameters/"
359
+ )
360
+ ref_str = ref_str.replace("#/responses/", "#/components/responses/")
361
+ ref_str = ref_str.replace(
362
+ "#/securityDefinitions/", "#/components/securitySchemes/"
363
+ )
364
+ new_dict[k] = ref_str
365
+ else:
366
+ new_dict[k] = self._rewrite_references(v)
367
+ return new_dict
368
+ elif isinstance(obj, list):
369
+ return [self._rewrite_references(item) for item in obj]
370
+ return obj
@@ -72,14 +72,14 @@ def validate_raw_spec(spec_dict: dict[str, Any]) -> None:
72
72
  )
73
73
 
74
74
  version_str = str(version)
75
- if version_str.startswith("2."):
75
+ if not (
76
+ version_str.startswith("2.0")
77
+ or version_str.startswith("2.")
78
+ or version_str.startswith("3.0")
79
+ or version_str.startswith("3.1")
80
+ ):
76
81
  raise UnsupportedVersionError(
77
- f"Swagger version '{version_str}' is not supported."
78
- )
79
-
80
- if not (version_str.startswith("3.0") or version_str.startswith("3.1")):
81
- raise UnsupportedVersionError(
82
- f"OpenAPI version '{version_str}' is not supported."
82
+ f"Specification version '{version_str}' is not supported."
83
83
  )
84
84
 
85
85
  if "paths" not in spec_dict:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "langchain-openapi-tools"
7
- version = "1.0.0"
7
+ version = "1.0.2"
8
8
  description = "Convert OpenAPI specifications into native, production-grade LangChain tools."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"