jentic-openapi-common 1.0.0a30__py3-none-any.whl

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,223 @@
1
+ """OpenAPI version detection utilities.
2
+
3
+ This module provides functions to detect OpenAPI specification versions
4
+ from both text (YAML/JSON strings) and Mapping objects.
5
+ """
6
+
7
+ import re
8
+ from collections.abc import Mapping
9
+ from typing import Any
10
+
11
+
12
+ __all__ = [
13
+ "get_version",
14
+ "is_openapi_20",
15
+ "is_openapi_30",
16
+ "is_openapi_31",
17
+ "is_openapi_32",
18
+ ]
19
+
20
+
21
+ # Regex patterns for detecting OpenAPI versions in text
22
+ # Matches both YAML and JSON formats
23
+ # YAML: swagger: 2.0 (with optional quotes)
24
+ # JSON: "swagger": "2.0"
25
+ _OPENAPI_20_PATTERN = re.compile(
26
+ r'(?P<YAML>^(["\']?)swagger\2\s*:\s*(["\']?)(?P<version_yaml>2\.0)\3(?:\s+|$))'
27
+ r'|(?P<JSON>"swagger"\s*:\s*"(?P<version_json>2\.0)")',
28
+ re.MULTILINE,
29
+ )
30
+
31
+ # YAML: openapi: 3.0.x (with optional quotes)
32
+ # JSON: "openapi": "3.0.x"
33
+ _OPENAPI_30_PATTERN = re.compile(
34
+ r'(?P<YAML>^(["\']?)openapi\2\s*:\s*(["\']?)(?P<version_yaml>3\.0\.(?:[1-9]\d*|0))\3(?:\s+|$))'
35
+ r'|(?P<JSON>"openapi"\s*:\s*"(?P<version_json>3\.0\.(?:[1-9]\d*|0))")',
36
+ re.MULTILINE,
37
+ )
38
+
39
+ _OPENAPI_31_PATTERN = re.compile(
40
+ r'(?P<YAML>^(["\']?)openapi\2\s*:\s*(["\']?)(?P<version_yaml>3\.1\.(?:[1-9]\d*|0))\3(?:\s+|$))'
41
+ r'|(?P<JSON>"openapi"\s*:\s*"(?P<version_json>3\.1\.(?:[1-9]\d*|0))")',
42
+ re.MULTILINE,
43
+ )
44
+
45
+ _OPENAPI_32_PATTERN = re.compile(
46
+ r'(?P<YAML>^(["\']?)openapi\2\s*:\s*(["\']?)(?P<version_yaml>3\.2\.(?:[1-9]\d*|0))\3(?:\s+|$))'
47
+ r'|(?P<JSON>"openapi"\s*:\s*"(?P<version_json>3\.2\.(?:[1-9]\d*|0))")',
48
+ re.MULTILINE,
49
+ )
50
+
51
+ # All patterns to try for version extraction
52
+ _ALL_PATTERNS = [
53
+ _OPENAPI_20_PATTERN,
54
+ _OPENAPI_30_PATTERN,
55
+ _OPENAPI_31_PATTERN,
56
+ _OPENAPI_32_PATTERN,
57
+ ]
58
+
59
+
60
+ def get_version(document: str | Mapping[str, Any]) -> str | None:
61
+ """Extract the OpenAPI/Swagger version from a document.
62
+
63
+ Args:
64
+ document: Either a text string (YAML/JSON) or a Mapping object
65
+
66
+ Returns:
67
+ The version string if found, None otherwise
68
+
69
+ Examples:
70
+ >>> get_version("swagger: 2.0\\ninfo:\\n title: API")
71
+ '2.0'
72
+ >>> get_version("openapi: 3.0.4\\ninfo:\\n title: API")
73
+ '3.0.4'
74
+ >>> get_version('{"openapi": "3.1.2"}')
75
+ '3.1.2'
76
+ >>> get_version({"openapi": "3.2.0"})
77
+ '3.2.0'
78
+ >>> get_version({"swagger": "2.0"})
79
+ '2.0'
80
+ >>> get_version("no version here")
81
+ None
82
+ """
83
+ if isinstance(document, str):
84
+ # Try all patterns and extract version from named groups
85
+ for pattern in _ALL_PATTERNS:
86
+ match = pattern.search(document)
87
+ if match:
88
+ # Try to get version from either YAML or JSON group
89
+ version = (
90
+ match.group("version_yaml")
91
+ if match.group("version_yaml")
92
+ else match.group("version_json")
93
+ )
94
+ return version
95
+ return None
96
+ elif isinstance(document, Mapping):
97
+ # Return whatever version string is present, without validation
98
+ # Validation can be done separately with is_openapi_*() predicates
99
+ version = document.get("openapi") or document.get("swagger")
100
+ if isinstance(version, str):
101
+ return version
102
+ return None
103
+ return None
104
+
105
+
106
+ def is_openapi_20(document: str | Mapping[str, Any]) -> bool:
107
+ """Check if document is OpenAPI 2.0 (Swagger 2.0) specification.
108
+
109
+ Args:
110
+ document: Either a text string (YAML/JSON) or a Mapping object
111
+
112
+ Returns:
113
+ True if document is OpenAPI 2.0, False otherwise
114
+
115
+ Examples:
116
+ >>> is_openapi_20("swagger: 2.0\\ninfo:\\n title: API")
117
+ True
118
+ >>> is_openapi_20('{"swagger": "2.0"}')
119
+ True
120
+ >>> is_openapi_20({"swagger": "2.0"})
121
+ True
122
+ >>> is_openapi_20({"openapi": "3.0.4"})
123
+ False
124
+ """
125
+ if isinstance(document, str):
126
+ return bool(_OPENAPI_20_PATTERN.search(document))
127
+ elif isinstance(document, Mapping):
128
+ version = document.get("swagger")
129
+ if isinstance(version, str):
130
+ # Construct YAML-like string and reuse text pattern
131
+ test_string = f"swagger: {version}"
132
+ return bool(_OPENAPI_20_PATTERN.search(test_string))
133
+ return False
134
+
135
+
136
+ def is_openapi_30(document: str | Mapping[str, Any]) -> bool:
137
+ """Check if document is OpenAPI 3.0.x specification.
138
+
139
+ Args:
140
+ document: Either a text string (YAML/JSON) or a Mapping object
141
+
142
+ Returns:
143
+ True if document is OpenAPI 3.0.x, False otherwise
144
+
145
+ Examples:
146
+ >>> is_openapi_30("openapi: 3.0.4\\ninfo:\\n title: API")
147
+ True
148
+ >>> is_openapi_30('{"openapi": "3.0.4"}')
149
+ True
150
+ >>> is_openapi_30({"openapi": "3.0.4"})
151
+ True
152
+ >>> is_openapi_30({"openapi": "3.1.0"})
153
+ False
154
+ """
155
+ if isinstance(document, str):
156
+ return bool(_OPENAPI_30_PATTERN.search(document))
157
+ elif isinstance(document, Mapping):
158
+ version = document.get("openapi")
159
+ if isinstance(version, str):
160
+ # Construct YAML-like string and reuse text pattern
161
+ test_string = f"openapi: {version}"
162
+ return bool(_OPENAPI_30_PATTERN.search(test_string))
163
+ return False
164
+
165
+
166
+ def is_openapi_31(document: str | Mapping[str, Any]) -> bool:
167
+ """Check if document is OpenAPI 3.1.x specification.
168
+
169
+ Args:
170
+ document: Either a text string (YAML/JSON) or a Mapping object
171
+
172
+ Returns:
173
+ True if document is OpenAPI 3.1.x, False otherwise
174
+
175
+ Examples:
176
+ >>> is_openapi_31("openapi: 3.1.2\\ninfo:\\n title: API")
177
+ True
178
+ >>> is_openapi_31('{"openapi": "3.1.2"}')
179
+ True
180
+ >>> is_openapi_31({"openapi": "3.1.2"})
181
+ True
182
+ >>> is_openapi_31({"openapi": "3.0.4"})
183
+ False
184
+ """
185
+ if isinstance(document, str):
186
+ return bool(_OPENAPI_31_PATTERN.search(document))
187
+ elif isinstance(document, Mapping):
188
+ version = document.get("openapi")
189
+ if isinstance(version, str):
190
+ # Construct YAML-like string and reuse text pattern
191
+ test_string = f"openapi: {version}"
192
+ return bool(_OPENAPI_31_PATTERN.search(test_string))
193
+ return False
194
+
195
+
196
+ def is_openapi_32(document: str | Mapping[str, Any]) -> bool:
197
+ """Check if document is OpenAPI 3.2.x specification.
198
+
199
+ Args:
200
+ document: Either a text string (YAML/JSON) or a Mapping object
201
+
202
+ Returns:
203
+ True if document is OpenAPI 3.2.x, False otherwise
204
+
205
+ Examples:
206
+ >>> is_openapi_32("openapi: 3.2.0\\ninfo:\\n title: API")
207
+ True
208
+ >>> is_openapi_32('{"openapi": "3.2.0"}')
209
+ True
210
+ >>> is_openapi_32({"openapi": "3.2.0"})
211
+ True
212
+ >>> is_openapi_32({"openapi": "3.1.0"})
213
+ False
214
+ """
215
+ if isinstance(document, str):
216
+ return bool(_OPENAPI_32_PATTERN.search(document))
217
+ elif isinstance(document, Mapping):
218
+ version = document.get("openapi")
219
+ if isinstance(version, str):
220
+ # Construct YAML-like string and reuse text pattern
221
+ test_string = f"openapi: {version}"
222
+ return bool(_OPENAPI_32_PATTERN.search(test_string))
223
+ return False
@@ -0,0 +1,322 @@
1
+ Metadata-Version: 2.4
2
+ Name: jentic-openapi-common
3
+ Version: 1.0.0a30
4
+ Summary: Jentic OpenAPI Common
5
+ Author: Jentic
6
+ Author-email: Jentic <hello@jentic.com>
7
+ License-Expression: Apache-2.0
8
+ License-File: LICENSE
9
+ License-File: NOTICE
10
+ Requires-Python: >=3.11
11
+ Project-URL: Homepage, https://github.com/jentic/jentic-openapi-tools
12
+ Description-Content-Type: text/markdown
13
+
14
+ # jentic-openapi-common
15
+
16
+ Common utilities for OpenAPI tools packages. This package provides shared functionality using PEP 420 namespace packages as contribution points.
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ uv add jentic-openapi-common
22
+ ```
23
+
24
+ ## Modules
25
+
26
+ ### uri
27
+
28
+ URI/URL/path utilities for working with OpenAPI document references.
29
+
30
+ **Available functions:**
31
+
32
+ - `is_uri_like(s: str | None) -> bool` - Check if a string looks like a URI/URL/path
33
+ - `is_http_https_url(s: str | None) -> bool` - Check if string is an HTTP(S) URL
34
+ - `is_file_uri(s: str | None) -> bool` - Check if string is a file:// URI
35
+ - `is_path(s: str | None) -> bool` - Check if string is a filesystem path (not a URL)
36
+ - `resolve_to_absolute(uri: str, base_uri: str | None = None) -> str` - Resolve relative URIs to absolute
37
+
38
+ **Exceptions:**
39
+
40
+ - `URIResolutionError` - Raised when URI resolution fails
41
+
42
+ ### path_security
43
+
44
+ Path security utilities for validating and securing filesystem access. Provides defense-in-depth protection against path traversal attacks, directory escapes, and unauthorized file access.
45
+
46
+ **Available functions:**
47
+
48
+ - `validate_path(path, *, allowed_base=None, allowed_extensions=None, resolve_symlinks=True, as_string=True) -> str | Path` - Validate and canonicalize a filesystem path with security checks. Returns `str` by default, or `Path` when `as_string=False`
49
+
50
+ **Exceptions:**
51
+
52
+ - `PathSecurityError` - Base exception for path security violations
53
+ - `PathTraversalError` - Path attempts to escape allowed base directory
54
+ - `InvalidExtensionError` - Path has disallowed file extension
55
+ - `SymlinkSecurityError` - Path contains symlinks when not allowed or symlink escapes boundary
56
+
57
+ ### subproc
58
+
59
+ Subprocess execution utilities with enhanced error handling and cross-platform support.
60
+
61
+ ### version_detection
62
+
63
+ OpenAPI/Swagger version detection utilities. Provides functions to detect and extract version information from OpenAPI documents in text (YAML/JSON) or Mapping formats.
64
+
65
+ **Available functions:**
66
+
67
+ - `get_version(document: str | Mapping[str, Any]) -> str | None` - Extract version string from document (e.g., "3.0.4", "2.0")
68
+ - `is_openapi_20(document: str | Mapping[str, Any]) -> bool` - Check if document is OpenAPI 2.0 (Swagger 2.0)
69
+ - `is_openapi_30(document: str | Mapping[str, Any]) -> bool` - Check if document is OpenAPI 3.0.x
70
+ - `is_openapi_31(document: str | Mapping[str, Any]) -> bool` - Check if document is OpenAPI 3.1.x
71
+ - `is_openapi_32(document: str | Mapping[str, Any]) -> bool` - Check if document is OpenAPI 3.2.x
72
+
73
+ **Version Detection Behavior:**
74
+
75
+ - **Text input**: Validates against regex patterns, only returns/matches valid versions per specification
76
+ - **Mapping input**:
77
+ - `get_version()` returns whatever version string is present (for extraction/inspection)
78
+ - `is_openapi_*()` validates against patterns (for version checking)
79
+
80
+ ## Usage Examples
81
+
82
+ ### URI Utilities
83
+
84
+ ```python
85
+ from jentic.apitools.openapi.common.uri import (
86
+ is_uri_like,
87
+ is_http_https_url,
88
+ is_file_uri,
89
+ is_path,
90
+ resolve_to_absolute,
91
+ URIResolutionError,
92
+ )
93
+
94
+ # Check URI types
95
+ is_uri_like("https://example.com/spec.yaml") # True
96
+ is_http_https_url("https://example.com/spec.yaml") # True
97
+ is_file_uri("file:///home/user/spec.yaml") # True
98
+ is_path("/home/user/spec.yaml") # True
99
+ is_path("https://example.com/spec.yaml") # False
100
+
101
+ # Resolve relative URIs
102
+ absolute = resolve_to_absolute("../spec.yaml", "/home/user/project/docs/")
103
+ # Returns: "/home/user/project/spec.yaml"
104
+
105
+ absolute = resolve_to_absolute("spec.yaml") # Resolves against current working directory
106
+ ```
107
+
108
+ ### Path Security
109
+
110
+ ```python
111
+ from pathlib import Path
112
+ from jentic.apitools.openapi.common.path_security import (
113
+ validate_path,
114
+ PathSecurityError,
115
+ PathTraversalError,
116
+ InvalidExtensionError,
117
+ SymlinkSecurityError,
118
+ )
119
+
120
+ # Basic validation - converts to absolute path (returns string by default)
121
+ safe_path = validate_path("./specs/openapi.yaml")
122
+ print(safe_path) # '/current/working/dir/specs/openapi.yaml'
123
+ print(type(safe_path)) # <class 'str'>
124
+
125
+ # Request Path object with as_string=False
126
+ safe_path_obj = validate_path("./specs/openapi.yaml", as_string=False)
127
+ print(safe_path_obj) # Path('/current/working/dir/specs/openapi.yaml')
128
+ print(type(safe_path_obj)) # <class 'pathlib.Path'>
129
+
130
+ # Return type control with as_string parameter
131
+ # - as_string=True (default): Returns str - best for subprocess commands
132
+ # - as_string=False: Returns Path - best for file operations with pathlib
133
+
134
+ # Example: Using with subprocess commands (default string return)
135
+ import subprocess
136
+ doc_path = validate_path("./specs/openapi.yaml")
137
+ subprocess.run(["cat", doc_path]) # Works directly, no str() conversion needed
138
+
139
+ # Example: Using with pathlib operations (Path return)
140
+ from pathlib import Path
141
+ doc_path = validate_path("./specs/openapi.yaml", as_string=False)
142
+ if doc_path.exists():
143
+ content = doc_path.read_text() # Path methods available
144
+
145
+ # Boundary enforcement - restrict access to specific directory
146
+ try:
147
+ safe_path = validate_path(
148
+ "/var/app/data/spec.yaml",
149
+ allowed_base="/var/app",
150
+ )
151
+ print(f"Access granted: {safe_path}")
152
+ except PathTraversalError as e:
153
+ print(f"Access denied: {e}")
154
+
155
+ # Block directory traversal attacks
156
+ try:
157
+ safe_path = validate_path(
158
+ "/var/app/../../../etc/passwd",
159
+ allowed_base="/var/app",
160
+ )
161
+ except PathTraversalError:
162
+ print("Path traversal attack blocked!")
163
+
164
+ # Extension validation - whitelist approach
165
+ try:
166
+ safe_path = validate_path(
167
+ "spec.yaml",
168
+ allowed_extensions=(".yaml", ".yml", ".json"),
169
+ )
170
+ print(f"Valid extension: {safe_path}")
171
+ except InvalidExtensionError:
172
+ print("Invalid file extension")
173
+
174
+ # Combined security checks (recommended for web services)
175
+ try:
176
+ safe_path = validate_path(
177
+ user_provided_path,
178
+ allowed_base="/var/app/uploads",
179
+ allowed_extensions=(".yaml", ".yml", ".json"),
180
+ resolve_symlinks=True, # Default: resolve and check symlinks
181
+ )
182
+ # Safe to use safe_path for file operations
183
+ with open(safe_path) as f:
184
+ content = f.read()
185
+ except PathSecurityError as e:
186
+ print(f"Security validation failed: {e}")
187
+ ```
188
+
189
+ ### Subprocess Execution
190
+
191
+ #### Basic Command Execution
192
+
193
+ ```python
194
+ from jentic.apitools.openapi.common.subproc import run_subprocess
195
+
196
+ # Simple command
197
+ result = run_subprocess(["echo", "hello"])
198
+ print(result.stdout) # "hello\n"
199
+ print(result.returncode) # 0
200
+
201
+ # Command with working directory
202
+ result = run_subprocess(["pwd"], cwd="/tmp")
203
+ print(result.stdout.strip()) # "/tmp"
204
+ ```
205
+
206
+ ### Error Handling
207
+
208
+ ```python
209
+ from jentic.apitools.openapi.common.subproc import (
210
+ run_subprocess,
211
+ SubprocessExecutionError
212
+ )
213
+
214
+ # Handle errors manually
215
+ result = run_subprocess(["false"]) # Command that exits with code 1
216
+ if result.returncode != 0:
217
+ print(f"Command failed with code {result.returncode}")
218
+
219
+ # Automatic error handling
220
+ try:
221
+ result = run_subprocess(["false"], fail_on_error=True)
222
+ except SubprocessExecutionError as e:
223
+ print(f"Command {e.cmd} failed: {e}")
224
+ ```
225
+
226
+ ### Advanced Usage
227
+
228
+ ```python
229
+ from jentic.apitools.openapi.common.subproc import (
230
+ run_subprocess,
231
+ SubprocessExecutionError
232
+ )
233
+
234
+ # Timeout handling
235
+ try:
236
+ result = run_subprocess(["sleep", "10"], timeout=1)
237
+ except SubprocessExecutionError as e:
238
+ print("Command timed out")
239
+
240
+ # Custom encoding
241
+ result = run_subprocess(["python", "-c", "print('ñ')"], encoding="utf-8")
242
+ print(result.stdout) # "ñ\n"
243
+ ```
244
+
245
+ ### Version Detection
246
+
247
+ ```python
248
+ from jentic.apitools.openapi.common.version_detection import (
249
+ get_version,
250
+ is_openapi_20,
251
+ is_openapi_30,
252
+ is_openapi_31,
253
+ is_openapi_32,
254
+ )
255
+
256
+ # Extract version from text (YAML/JSON)
257
+ yaml_doc = """
258
+ openapi: 3.0.4
259
+ info:
260
+ title: Pet Store API
261
+ version: 1.0.0
262
+ """
263
+ version = get_version(yaml_doc)
264
+ print(version) # "3.0.4"
265
+
266
+ json_doc = '{"openapi": "3.1.2", "info": {"title": "API", "version": "1.0.0"}}'
267
+ version = get_version(json_doc)
268
+ print(version) # "3.1.2"
269
+
270
+ # Extract version from Mapping (returns any version string, even if unsupported)
271
+ doc = {"openapi": "3.0.4"}
272
+ version = get_version(doc)
273
+ print(version) # "3.0.4"
274
+
275
+ # Even unsupported versions are returned from Mapping
276
+ doc = {"openapi": "3.0.4-rc1"}
277
+ version = get_version(doc)
278
+ print(version) # "3.0.4-rc1" (suffix returned as-is)
279
+
280
+ # But text input validates with regex
281
+ version = get_version("openapi: 3.0.4-rc1")
282
+ print(version) # None (suffix doesn't match pattern)
283
+
284
+ # Version checking with predicates (validates for both text and Mapping)
285
+ doc_20 = {"swagger": "2.0"}
286
+ print(is_openapi_20(doc_20)) # True
287
+ print(is_openapi_30(doc_20)) # False
288
+
289
+ doc_30 = {"openapi": "3.0.4"}
290
+ print(is_openapi_30(doc_30)) # True
291
+ print(is_openapi_31(doc_30)) # False
292
+
293
+ doc_31 = {"openapi": "3.1.2"}
294
+ print(is_openapi_31(doc_31)) # True
295
+ print(is_openapi_32(doc_31)) # False
296
+
297
+ doc_32 = {"openapi": "3.2.0"}
298
+ print(is_openapi_32(doc_32)) # True
299
+
300
+ # Predicates validate strictly
301
+ doc_suffix = {"openapi": "3.0.4-rc1"}
302
+ print(is_openapi_30(doc_suffix)) # False (suffix rejected)
303
+
304
+ doc_unsupported = {"openapi": "3.3.0"}
305
+ print(is_openapi_32(doc_unsupported)) # False (unsupported version)
306
+
307
+ # Works with YAML text too
308
+ yaml_text = "openapi: 3.0.4\ninfo:\n title: API"
309
+ print(is_openapi_30(yaml_text)) # True
310
+
311
+ # Works with JSON text
312
+ json_text = '{"openapi": "3.1.2"}'
313
+ print(is_openapi_31(json_text)) # True
314
+ ```
315
+
316
+ ## Testing
317
+
318
+ Run the test suite:
319
+
320
+ ```bash
321
+ uv run --package jentic-openapi-common pytest packages/jentic-openapi-common -v
322
+ ```
@@ -0,0 +1,10 @@
1
+ jentic/apitools/openapi/common/path_security.py,sha256=TN32F7LiKQQJAIsq-T8rhdRJcXjR7Hs6Q7SBYOP7xeI,6374
2
+ jentic/apitools/openapi/common/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ jentic/apitools/openapi/common/subproc.py,sha256=KowpIfK1tZxjXKGpUJmX4Eiu8CP3rNMwpA6a6yo2Lfk,4983
4
+ jentic/apitools/openapi/common/uri.py,sha256=jZ-US7GanWsqwYPTSjALxcBtrnq3r9fKaHfVjBhxRDw,11193
5
+ jentic/apitools/openapi/common/version_detection.py,sha256=dxs9x6_pOfNfhMyBFWMCI24phVYHsnkanTDaLJtXw1Y,7114
6
+ jentic_openapi_common-1.0.0a30.dist-info/licenses/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
7
+ jentic_openapi_common-1.0.0a30.dist-info/licenses/NOTICE,sha256=pAOGW-rGw9KNc2cuuLWZkfx0GSTV4TicbgBKZSLPMIs,168
8
+ jentic_openapi_common-1.0.0a30.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
9
+ jentic_openapi_common-1.0.0a30.dist-info/METADATA,sha256=JssI8E98ohz-pxOJzHavdjsT2pE1eobG_xcRzELgkag,9830
10
+ jentic_openapi_common-1.0.0a30.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.8.24
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any