jentic-openapi-validator 1.0.0a27__tar.gz → 1.0.0a28__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 (17) hide show
  1. {jentic_openapi_validator-1.0.0a27 → jentic_openapi_validator-1.0.0a28}/PKG-INFO +49 -10
  2. {jentic_openapi_validator-1.0.0a27 → jentic_openapi_validator-1.0.0a28}/README.md +45 -6
  3. {jentic_openapi_validator-1.0.0a27 → jentic_openapi_validator-1.0.0a28}/pyproject.toml +4 -4
  4. {jentic_openapi_validator-1.0.0a27 → jentic_openapi_validator-1.0.0a28}/src/jentic/apitools/openapi/validator/core/openapi_validator.py +143 -18
  5. {jentic_openapi_validator-1.0.0a27 → jentic_openapi_validator-1.0.0a28}/LICENSE +0 -0
  6. {jentic_openapi_validator-1.0.0a27 → jentic_openapi_validator-1.0.0a28}/NOTICE +0 -0
  7. {jentic_openapi_validator-1.0.0a27 → jentic_openapi_validator-1.0.0a28}/src/jentic/apitools/openapi/validator/backends/base.py +0 -0
  8. {jentic_openapi_validator-1.0.0a27 → jentic_openapi_validator-1.0.0a28}/src/jentic/apitools/openapi/validator/backends/default/__init__.py +0 -0
  9. {jentic_openapi_validator-1.0.0a27 → jentic_openapi_validator-1.0.0a28}/src/jentic/apitools/openapi/validator/backends/default/rules/__init__.py +0 -0
  10. {jentic_openapi_validator-1.0.0a27 → jentic_openapi_validator-1.0.0a28}/src/jentic/apitools/openapi/validator/backends/default/rules/security.py +0 -0
  11. {jentic_openapi_validator-1.0.0a27 → jentic_openapi_validator-1.0.0a28}/src/jentic/apitools/openapi/validator/backends/default/rules/server.py +0 -0
  12. {jentic_openapi_validator-1.0.0a27 → jentic_openapi_validator-1.0.0a28}/src/jentic/apitools/openapi/validator/backends/default/rules/structural.py +0 -0
  13. {jentic_openapi_validator-1.0.0a27 → jentic_openapi_validator-1.0.0a28}/src/jentic/apitools/openapi/validator/backends/openapi_spec.py +0 -0
  14. {jentic_openapi_validator-1.0.0a27 → jentic_openapi_validator-1.0.0a28}/src/jentic/apitools/openapi/validator/backends/py.typed +0 -0
  15. {jentic_openapi_validator-1.0.0a27 → jentic_openapi_validator-1.0.0a28}/src/jentic/apitools/openapi/validator/core/__init__.py +0 -0
  16. {jentic_openapi_validator-1.0.0a27 → jentic_openapi_validator-1.0.0a28}/src/jentic/apitools/openapi/validator/core/diagnostics.py +0 -0
  17. {jentic_openapi_validator-1.0.0a27 → jentic_openapi_validator-1.0.0a28}/src/jentic/apitools/openapi/validator/core/py.typed +0 -0
@@ -1,17 +1,17 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jentic-openapi-validator
3
- Version: 1.0.0a27
3
+ Version: 1.0.0a28
4
4
  Summary: Jentic OpenAPI Validator
5
5
  Author: Jentic
6
6
  Author-email: Jentic <hello@jentic.com>
7
7
  License-Expression: Apache-2.0
8
8
  License-File: LICENSE
9
9
  License-File: NOTICE
10
- Requires-Dist: jentic-openapi-parser~=1.0.0a27
10
+ Requires-Dist: jentic-openapi-parser~=1.0.0a28
11
11
  Requires-Dist: openapi-spec-validator~=0.7.2
12
12
  Requires-Dist: lsprotocol~=2025.0.0
13
- Requires-Dist: jentic-openapi-validator-redocly~=1.0.0a27 ; extra == 'redocly'
14
- Requires-Dist: jentic-openapi-validator-spectral~=1.0.0a27 ; extra == 'spectral'
13
+ Requires-Dist: jentic-openapi-validator-redocly~=1.0.0a28 ; extra == 'redocly'
14
+ Requires-Dist: jentic-openapi-validator-spectral~=1.0.0a28 ; extra == 'spectral'
15
15
  Requires-Python: >=3.11
16
16
  Project-URL: Homepage, https://github.com/jentic/jentic-openapi-tools
17
17
  Provides-Extra: redocly
@@ -26,6 +26,7 @@ A Python library for validating OpenAPI documents using pluggable validator back
26
26
 
27
27
  - **Pluggable Backend Architecture**: Support for multiple validation strategies via entry points
28
28
  - **Multiple Input Formats**: Validate OpenAPI documents from file URIs, JSON/YAML strings, or Python dictionaries
29
+ - **Parallel Execution**: Run multiple backends concurrently using `ProcessPoolExecutor` for improved performance
29
30
  - **Aggregated Results**: Collect diagnostics from all configured backends into a single result
30
31
  - **Type Safety**: Full type hints with comprehensive docstrings
31
32
  - **Extensible Design**: Easy integration of third-party validator backends
@@ -47,6 +48,12 @@ For advanced validation with Spectral:
47
48
  pip install jentic-openapi-validator-spectral
48
49
  ```
49
50
 
51
+ For validation with Redocly:
52
+
53
+ ```bash
54
+ pip install jentic-openapi-validator-redocly
55
+ ```
56
+
50
57
  ## Quick Start
51
58
 
52
59
  ### Basic Validation
@@ -132,6 +139,26 @@ parser = OpenAPIParser()
132
139
  validator = OpenAPIValidator(parser=parser)
133
140
  ```
134
141
 
142
+ ### Parallel Execution
143
+
144
+ When using multiple backends, you can run them in parallel for improved performance:
145
+
146
+ ```python
147
+ # Run backends in parallel using ProcessPoolExecutor
148
+ validator = OpenAPIValidator(backends=["default", "openapi-spec", "spectral"])
149
+ result = validator.validate(document, parallel=True)
150
+
151
+ # Limit the number of worker processes
152
+ result = validator.validate(document, parallel=True, max_workers=2)
153
+ ```
154
+
155
+ Parallel execution uses `asyncio` with `ProcessPoolExecutor` via `run_in_executor()`, enabling true parallelism that bypasses Python's GIL. This is particularly beneficial when using multiple backends, especially I/O-bound backends like Spectral and Redocly that spawn subprocesses.
156
+
157
+ **Notes:**
158
+ - `parallel=False` by default (opt-in)
159
+ - With a single backend, `parallel=True` has no effect (runs sequentially)
160
+ - Diagnostics from all backends are aggregated regardless of execution mode
161
+
135
162
  ## Working with ValidationResult
136
163
 
137
164
  The `ValidationResult` class provides convenient methods for working with validation diagnostics:
@@ -175,24 +202,28 @@ The package includes integration tests for backend discovery and validation. Tes
175
202
  class OpenAPIValidator:
176
203
  def __init__(
177
204
  self,
178
- backends: list[str | BaseValidatorBackend | Type[BaseValidatorBackend]] | None = None,
205
+ backends: Sequence[str | BaseValidatorBackend | Type[BaseValidatorBackend]] | None = None,
179
206
  parser: OpenAPIParser | None = None,
180
207
  ) -> None
181
208
  ```
182
209
 
183
210
  **Parameters:**
184
- - `backends`: List of validator backends to use. Each item can be:
185
- - `str`: Name of a backend registered via entry points (e.g., "openapi-spec", "spectral")
211
+ - `backends`: Sequence of validator backends to use. Each item can be:
212
+ - `str`: Name of a backend registered via entry points (e.g., "default", "openapi-spec", "redocly", "spectral")
186
213
  - `BaseValidatorBackend`: Instance of a validator backend
187
214
  - `Type[BaseValidatorBackend]`: Class of a validator backend (will be instantiated)
188
- - Defaults to `["openapi-spec"]` if `None`
215
+ - Defaults to `["default"]` if `None`
189
216
  - `parser`: Custom OpenAPIParser instance (optional)
190
217
 
191
218
  **Methods:**
192
219
 
193
- - `validate(document: str | dict) -> ValidationResult`
220
+ - `validate(document: str | dict, *, base_url: str | None = None, target: str | None = None, parallel: bool = False, max_workers: int | None = None) -> ValidationResult`
194
221
  - Validates an OpenAPI document using all configured backends
195
222
  - `document`: File URI, JSON/YAML string, or dictionary
223
+ - `base_url`: Optional base URL for resolving relative references
224
+ - `target`: Optional target identifier for validation context
225
+ - `parallel`: If `True` and multiple backends are configured, run validation in parallel using `ProcessPoolExecutor`. Defaults to `False`.
226
+ - `max_workers`: Maximum number of worker processes for parallel execution. If `None`, defaults to the number of processors on the machine. Only used when `parallel=True`.
196
227
  - Returns: `ValidationResult` with aggregated diagnostics
197
228
 
198
229
  ### ValidationResult
@@ -218,7 +249,15 @@ class ValidationResult:
218
249
  ### default
219
250
  Basic validation backend that checks for required OpenAPI fields and structure. Suitable for basic document validation.
220
251
 
252
+ ### openapi-spec
253
+ Validation backend using the `openapi-spec-validator` library for JSON Schema-based validation of OpenAPI documents. CPU-bound validation.
254
+
255
+ ### redocly (Optional)
256
+ Validation backend using Redocly CLI for comprehensive OpenAPI linting and validation. I/O-bound (spawns Node.js subprocess).
257
+
258
+ Install: `pip install jentic-openapi-validator-redocly`
259
+
221
260
  ### spectral (Optional)
222
- Advanced validation backend using Spectral CLI with comprehensive rule checking.
261
+ Advanced validation backend using Spectral CLI with comprehensive rule checking. I/O-bound (spawns Node.js subprocess).
223
262
 
224
263
  Install: `pip install jentic-openapi-validator-spectral`
@@ -6,6 +6,7 @@ A Python library for validating OpenAPI documents using pluggable validator back
6
6
 
7
7
  - **Pluggable Backend Architecture**: Support for multiple validation strategies via entry points
8
8
  - **Multiple Input Formats**: Validate OpenAPI documents from file URIs, JSON/YAML strings, or Python dictionaries
9
+ - **Parallel Execution**: Run multiple backends concurrently using `ProcessPoolExecutor` for improved performance
9
10
  - **Aggregated Results**: Collect diagnostics from all configured backends into a single result
10
11
  - **Type Safety**: Full type hints with comprehensive docstrings
11
12
  - **Extensible Design**: Easy integration of third-party validator backends
@@ -27,6 +28,12 @@ For advanced validation with Spectral:
27
28
  pip install jentic-openapi-validator-spectral
28
29
  ```
29
30
 
31
+ For validation with Redocly:
32
+
33
+ ```bash
34
+ pip install jentic-openapi-validator-redocly
35
+ ```
36
+
30
37
  ## Quick Start
31
38
 
32
39
  ### Basic Validation
@@ -112,6 +119,26 @@ parser = OpenAPIParser()
112
119
  validator = OpenAPIValidator(parser=parser)
113
120
  ```
114
121
 
122
+ ### Parallel Execution
123
+
124
+ When using multiple backends, you can run them in parallel for improved performance:
125
+
126
+ ```python
127
+ # Run backends in parallel using ProcessPoolExecutor
128
+ validator = OpenAPIValidator(backends=["default", "openapi-spec", "spectral"])
129
+ result = validator.validate(document, parallel=True)
130
+
131
+ # Limit the number of worker processes
132
+ result = validator.validate(document, parallel=True, max_workers=2)
133
+ ```
134
+
135
+ Parallel execution uses `asyncio` with `ProcessPoolExecutor` via `run_in_executor()`, enabling true parallelism that bypasses Python's GIL. This is particularly beneficial when using multiple backends, especially I/O-bound backends like Spectral and Redocly that spawn subprocesses.
136
+
137
+ **Notes:**
138
+ - `parallel=False` by default (opt-in)
139
+ - With a single backend, `parallel=True` has no effect (runs sequentially)
140
+ - Diagnostics from all backends are aggregated regardless of execution mode
141
+
115
142
  ## Working with ValidationResult
116
143
 
117
144
  The `ValidationResult` class provides convenient methods for working with validation diagnostics:
@@ -155,24 +182,28 @@ The package includes integration tests for backend discovery and validation. Tes
155
182
  class OpenAPIValidator:
156
183
  def __init__(
157
184
  self,
158
- backends: list[str | BaseValidatorBackend | Type[BaseValidatorBackend]] | None = None,
185
+ backends: Sequence[str | BaseValidatorBackend | Type[BaseValidatorBackend]] | None = None,
159
186
  parser: OpenAPIParser | None = None,
160
187
  ) -> None
161
188
  ```
162
189
 
163
190
  **Parameters:**
164
- - `backends`: List of validator backends to use. Each item can be:
165
- - `str`: Name of a backend registered via entry points (e.g., "openapi-spec", "spectral")
191
+ - `backends`: Sequence of validator backends to use. Each item can be:
192
+ - `str`: Name of a backend registered via entry points (e.g., "default", "openapi-spec", "redocly", "spectral")
166
193
  - `BaseValidatorBackend`: Instance of a validator backend
167
194
  - `Type[BaseValidatorBackend]`: Class of a validator backend (will be instantiated)
168
- - Defaults to `["openapi-spec"]` if `None`
195
+ - Defaults to `["default"]` if `None`
169
196
  - `parser`: Custom OpenAPIParser instance (optional)
170
197
 
171
198
  **Methods:**
172
199
 
173
- - `validate(document: str | dict) -> ValidationResult`
200
+ - `validate(document: str | dict, *, base_url: str | None = None, target: str | None = None, parallel: bool = False, max_workers: int | None = None) -> ValidationResult`
174
201
  - Validates an OpenAPI document using all configured backends
175
202
  - `document`: File URI, JSON/YAML string, or dictionary
203
+ - `base_url`: Optional base URL for resolving relative references
204
+ - `target`: Optional target identifier for validation context
205
+ - `parallel`: If `True` and multiple backends are configured, run validation in parallel using `ProcessPoolExecutor`. Defaults to `False`.
206
+ - `max_workers`: Maximum number of worker processes for parallel execution. If `None`, defaults to the number of processors on the machine. Only used when `parallel=True`.
176
207
  - Returns: `ValidationResult` with aggregated diagnostics
177
208
 
178
209
  ### ValidationResult
@@ -198,7 +229,15 @@ class ValidationResult:
198
229
  ### default
199
230
  Basic validation backend that checks for required OpenAPI fields and structure. Suitable for basic document validation.
200
231
 
232
+ ### openapi-spec
233
+ Validation backend using the `openapi-spec-validator` library for JSON Schema-based validation of OpenAPI documents. CPU-bound validation.
234
+
235
+ ### redocly (Optional)
236
+ Validation backend using Redocly CLI for comprehensive OpenAPI linting and validation. I/O-bound (spawns Node.js subprocess).
237
+
238
+ Install: `pip install jentic-openapi-validator-redocly`
239
+
201
240
  ### spectral (Optional)
202
- Advanced validation backend using Spectral CLI with comprehensive rule checking.
241
+ Advanced validation backend using Spectral CLI with comprehensive rule checking. I/O-bound (spawns Node.js subprocess).
203
242
 
204
243
  Install: `pip install jentic-openapi-validator-spectral`
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "jentic-openapi-validator"
3
- version = "1.0.0-alpha.27"
3
+ version = "1.0.0-alpha.28"
4
4
  description = "Jentic OpenAPI Validator"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "Jentic", email = "hello@jentic.com" }]
@@ -8,14 +8,14 @@ license = "Apache-2.0"
8
8
  license-files = ["LICENSE", "NOTICE"]
9
9
  requires-python = ">=3.11"
10
10
  dependencies = [
11
- "jentic-openapi-parser~=1.0.0-alpha.27",
11
+ "jentic-openapi-parser~=1.0.0-alpha.28",
12
12
  "openapi-spec-validator~=0.7.2",
13
13
  "lsprotocol~=2025.0.0"
14
14
  ]
15
15
 
16
16
  [project.optional-dependencies]
17
- spectral = ["jentic-openapi-validator-spectral~=1.0.0-alpha.27"]
18
- redocly = ["jentic-openapi-validator-redocly~=1.0.0-alpha.27"]
17
+ spectral = ["jentic-openapi-validator-spectral~=1.0.0-alpha.28"]
18
+ redocly = ["jentic-openapi-validator-redocly~=1.0.0-alpha.28"]
19
19
 
20
20
  [project.urls]
21
21
  Homepage = "https://github.com/jentic/jentic-openapi-tools"
@@ -1,6 +1,9 @@
1
+ import asyncio
1
2
  import importlib.metadata
2
3
  import json
3
4
  import warnings
5
+ from collections.abc import Sequence
6
+ from concurrent.futures import ProcessPoolExecutor
4
7
  from typing import Type
5
8
 
6
9
  from jentic.apitools.openapi.parser.core import OpenAPIParser
@@ -40,7 +43,7 @@ class OpenAPIValidator:
40
43
 
41
44
  def __init__(
42
45
  self,
43
- backends: list[str | BaseValidatorBackend | Type[BaseValidatorBackend]] | None = None,
46
+ backends: Sequence[str | BaseValidatorBackend | Type[BaseValidatorBackend]] | None = None,
44
47
  parser: OpenAPIParser | None = None,
45
48
  ):
46
49
  """
@@ -78,7 +81,13 @@ class OpenAPIValidator:
78
81
  raise TypeError("Invalid backend type: must be name or backend class/instance")
79
82
 
80
83
  def validate(
81
- self, document: str | dict, *, base_url: str | None = None, target: str | None = None
84
+ self,
85
+ document: str | dict,
86
+ *,
87
+ base_url: str | None = None,
88
+ target: str | None = None,
89
+ parallel: bool = False,
90
+ max_workers: int | None = None,
82
91
  ) -> ValidationResult:
83
92
  """
84
93
  Validate an OpenAPI document using all configured backends.
@@ -94,6 +103,11 @@ class OpenAPIValidator:
94
103
  - Python dictionary
95
104
  base_url: Optional base URL for resolving relative references in the document
96
105
  target: Optional target identifier for validation context
106
+ parallel: If True and multiple backends are configured, run validation
107
+ in parallel using ProcessPoolExecutor. Defaults to False.
108
+ max_workers: Maximum number of worker processes for parallel execution.
109
+ If None, defaults to the number of processors on the machine.
110
+ Only used when parallel=True.
97
111
 
98
112
  Returns:
99
113
  ValidationResult containing aggregated diagnostics from all backends.
@@ -103,7 +117,6 @@ class OpenAPIValidator:
103
117
  TypeError: If document is not a str or dict
104
118
  """
105
119
 
106
- diagnostics: list[JenticDiagnostic] = []
107
120
  document_is_uri: bool = False
108
121
  document_text: str = ""
109
122
  document_dict: dict | None = None
@@ -135,21 +148,35 @@ class OpenAPIValidator:
135
148
  )
136
149
 
137
150
  # Run validation through all backends
138
- for backend in self.backends:
139
- accepted = backend.accepts()
140
- backend_document = None
141
-
142
- # Determine which format to pass to this backend
143
- if document_is_uri and "uri" in accepted:
144
- backend_document = document
145
- elif "dict" in accepted and document_dict is not None:
146
- backend_document = document_dict
147
- elif "text" in accepted:
148
- backend_document = document_text
149
-
150
- if backend_document is not None:
151
- result = backend.validate(backend_document, base_url=base_url, target=target)
152
- diagnostics.extend(result.diagnostics)
151
+ if parallel and len(self.backends) > 1:
152
+ # Parallel execution using asyncio with ProcessPoolExecutor
153
+ diagnostics = asyncio.run(
154
+ _validate_parallel(
155
+ self.backends,
156
+ document,
157
+ document_dict,
158
+ document_text,
159
+ document_is_uri,
160
+ base_url,
161
+ target,
162
+ max_workers,
163
+ )
164
+ )
165
+ else:
166
+ # Sequential execution (default)
167
+ diagnostics: list[JenticDiagnostic] = []
168
+ for backend in self.backends:
169
+ diagnostics.extend(
170
+ _validate_single_backend(
171
+ backend,
172
+ document,
173
+ document_dict,
174
+ document_text,
175
+ document_is_uri,
176
+ base_url,
177
+ target,
178
+ )
179
+ )
153
180
 
154
181
  return ValidationResult(diagnostics=diagnostics)
155
182
 
@@ -189,3 +216,101 @@ class OpenAPIValidator:
189
216
  ['default', 'spectral']
190
217
  """
191
218
  return list(_VALIDATOR_BACKENDS.keys())
219
+
220
+
221
+ def _validate_single_backend(
222
+ backend: BaseValidatorBackend,
223
+ document: str | dict,
224
+ document_dict: dict | None,
225
+ document_text: str,
226
+ document_is_uri: bool,
227
+ base_url: str | None,
228
+ target: str | None,
229
+ ) -> list[JenticDiagnostic]:
230
+ """
231
+ Validate document with a single backend.
232
+
233
+ This is a module-level function (not a method) to ensure it's picklable
234
+ for use with ProcessPoolExecutor.
235
+
236
+ Args:
237
+ backend: The validator backend to use
238
+ document: The original document (URI or text)
239
+ document_dict: Parsed document as dict (if available)
240
+ document_text: Document as text string
241
+ document_is_uri: Whether document is a URI
242
+ base_url: Optional base URL for resolving references
243
+ target: Optional target identifier
244
+
245
+ Returns:
246
+ List of diagnostics from the backend
247
+ """
248
+ accepted = backend.accepts()
249
+ backend_document: str | dict | None = None
250
+
251
+ if document_is_uri and "uri" in accepted:
252
+ backend_document = document
253
+ elif "dict" in accepted and document_dict is not None:
254
+ backend_document = document_dict
255
+ elif "text" in accepted:
256
+ backend_document = document_text
257
+
258
+ if backend_document is not None:
259
+ result = backend.validate(backend_document, base_url=base_url, target=target)
260
+ return list(result.diagnostics)
261
+ return []
262
+
263
+
264
+ async def _validate_parallel(
265
+ backends: Sequence[BaseValidatorBackend],
266
+ document: str | dict,
267
+ document_dict: dict | None,
268
+ document_text: str,
269
+ document_is_uri: bool,
270
+ base_url: str | None,
271
+ target: str | None,
272
+ max_workers: int | None,
273
+ ) -> list[JenticDiagnostic]:
274
+ """
275
+ Run validators in parallel using ProcessPoolExecutor.
276
+
277
+ This module-level async function uses asyncio's run_in_executor to dispatch
278
+ each backend validation to a separate process, enabling true parallelism
279
+ for CPU-bound validators.
280
+
281
+ Args:
282
+ backends: List of validator backends to run
283
+ document: The original document (URI or text)
284
+ document_dict: Parsed document as dict (if available)
285
+ document_text: Document as text string
286
+ document_is_uri: Whether document is a URI
287
+ base_url: Optional base URL for resolving references
288
+ target: Optional target identifier
289
+ max_workers: Maximum number of worker processes
290
+
291
+ Returns:
292
+ Aggregated list of diagnostics from all backends
293
+ """
294
+ loop = asyncio.get_running_loop()
295
+
296
+ with ProcessPoolExecutor(max_workers=max_workers) as executor:
297
+ tasks = [
298
+ loop.run_in_executor(
299
+ executor,
300
+ _validate_single_backend,
301
+ backend,
302
+ document,
303
+ document_dict,
304
+ document_text,
305
+ document_is_uri,
306
+ base_url,
307
+ target,
308
+ )
309
+ for backend in backends
310
+ ]
311
+ results = await asyncio.gather(*tasks)
312
+
313
+ diagnostics: list[JenticDiagnostic] = []
314
+ for result in results:
315
+ diagnostics.extend(result)
316
+ return diagnostics