jentic-openapi-common 1.0.0a10__tar.gz → 1.0.0a11__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jentic-openapi-common
3
- Version: 1.0.0a10
3
+ Version: 1.0.0a11
4
4
  Summary: Jentic OpenAPI Common
5
5
  Author: Jentic
6
6
  Author-email: Jentic <hello@jentic.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "jentic-openapi-common"
3
- version = "1.0.0-alpha.10"
3
+ version = "1.0.0-alpha.11"
4
4
  description = "Jentic OpenAPI Common"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "Jentic", email = "hello@jentic.com" }]
@@ -1,8 +1,8 @@
1
1
  import os
2
2
  import re
3
- import urllib.request
4
3
  from pathlib import Path
5
4
  from urllib.parse import urljoin, urlparse, urlsplit, urlunsplit
5
+ from urllib.request import url2pathname
6
6
 
7
7
 
8
8
  __all__ = [
@@ -10,8 +10,12 @@ __all__ = [
10
10
  "is_uri_like",
11
11
  "is_http_https_url",
12
12
  "is_file_uri",
13
+ "is_scheme_relative_uri",
14
+ "is_absolute_uri",
15
+ "is_fragment_only_uri",
13
16
  "is_path",
14
17
  "resolve_to_absolute",
18
+ "file_uri_to_path",
15
19
  ]
16
20
 
17
21
 
@@ -50,7 +54,7 @@ class URIResolutionError(ValueError):
50
54
  pass
51
55
 
52
56
 
53
- def is_uri_like(s: str | None) -> bool:
57
+ def is_uri_like(uri: str | None) -> bool:
54
58
  r"""
55
59
  Heuristic check: is `s` a URI-like reference or absolute/relative path?
56
60
  - Accepts http(s)://, file://
@@ -59,13 +63,13 @@ def is_uri_like(s: str | None) -> bool:
59
63
  - Must be a single line (no '\\n' or '\\r').
60
64
  Leading/trailing whitespace is ignored.
61
65
  """
62
- if not s:
66
+ if not uri:
63
67
  return False
64
- s = s.strip()
68
+ uri = uri.strip()
65
69
  # Enforce single line
66
- if "\n" in s or "\r" in s:
70
+ if "\n" in uri or "\r" in uri:
67
71
  return False
68
- return bool(_URI_LIKE_RE.match(s))
72
+ return bool(_URI_LIKE_RE.match(uri))
69
73
 
70
74
 
71
75
  def is_path(s: str | None) -> bool:
@@ -109,6 +113,107 @@ def is_path(s: str | None) -> bool:
109
113
  return True
110
114
 
111
115
 
116
+ def is_http_https_url(url: str) -> bool:
117
+ p = urlparse(url)
118
+ return p.scheme in ("http", "https") and bool(p.netloc)
119
+
120
+
121
+ def is_file_uri(uri: str) -> bool:
122
+ return urlparse(uri).scheme == "file"
123
+
124
+
125
+ def is_scheme_relative_uri(uri: str) -> bool:
126
+ """
127
+ Check if `uri` is a scheme-relative URI (also called protocol-relative URI).
128
+
129
+ A scheme-relative URI starts with "//" followed by an authority component (netloc),
130
+ inheriting the scheme from the context (e.g., "//cdn.example.com/path").
131
+
132
+ This is defined in RFC 3986 section 4.2 as a network-path reference.
133
+ Per RFC 3986, a valid network-path reference must have an authority component.
134
+
135
+ Examples:
136
+ - "//cdn.example.com/x.yaml" -> True
137
+ - "//example.com/api" -> True
138
+ - "http://example.com" -> False (has scheme)
139
+ - "/path/to/file" -> False (single slash)
140
+ - "./relative" -> False (relative path)
141
+ - "//" -> False (no authority component)
142
+ - "///path" -> False (no authority component)
143
+
144
+ Args:
145
+ uri: The string to check
146
+
147
+ Returns:
148
+ True if the string is a valid scheme-relative URI with authority, False otherwise
149
+ """
150
+ if not uri.startswith("//"):
151
+ return False
152
+ p = urlparse(uri)
153
+ return bool(p.netloc)
154
+
155
+
156
+ def is_absolute_uri(uri: str) -> bool:
157
+ """
158
+ Check if `uri` is an absolute URI according to RFC 3986.
159
+
160
+ An absolute URI is defined as having a scheme (e.g., "http:", "https:", "ftp:", "file:").
161
+
162
+ Note: Scheme-relative URIs (starting with "//") are NOT considered absolute URIs.
163
+ According to RFC 3986 section 4.2, scheme-relative URIs are classified as
164
+ "relative references" (specifically, "network-path references").
165
+ Use `is_scheme_relative_uri()` to check for those separately.
166
+
167
+ Examples:
168
+ - "http://example.com" -> True
169
+ - "https://example.com/path" -> True
170
+ - "ftp://ftp.example.com" -> True
171
+ - "file:///path/to/file" -> True
172
+ - "//cdn.example.com/x.yaml" -> False (scheme-relative, use is_scheme_relative_uri)
173
+ - "/path/to/file" -> False (absolute path, not URI)
174
+ - "./relative" -> False
175
+ - "#fragment" -> False
176
+
177
+ Args:
178
+ uri: The string to check
179
+
180
+ Returns:
181
+ True if the string is an absolute URI (has a scheme), False otherwise
182
+ """
183
+ p = urlparse(uri)
184
+ return bool(p.scheme)
185
+
186
+
187
+ def is_fragment_only_uri(uri: str) -> bool:
188
+ """
189
+ Check if `uri` is a fragment-only reference.
190
+
191
+ A fragment-only reference consists solely of a fragment identifier (starts with "#").
192
+ These are used in JSON References and OpenAPI to refer to parts within the same document.
193
+
194
+ Note: This checks if the ENTIRE string is a fragment reference, not whether
195
+ a URI contains a fragment. For example, "http://example.com#section" would
196
+ return False because it's a full URI with a fragment, not fragment-only.
197
+
198
+ Examples:
199
+ - "#/definitions/User" -> True
200
+ - "#fragment" -> True
201
+ - "#" -> True (empty fragment identifier)
202
+ - "##" -> True (fragment identifier is "#")
203
+ - "http://example.com#section" -> False (full URI with fragment)
204
+ - "/path/to/file" -> False
205
+ - "./relative" -> False
206
+ - "" -> False
207
+
208
+ Args:
209
+ uri: The string to check
210
+
211
+ Returns:
212
+ True if the string is a fragment-only reference, False otherwise
213
+ """
214
+ return uri.startswith("#")
215
+
216
+
112
217
  def resolve_to_absolute(value: str, base_uri: str | None = None) -> str:
113
218
  """
114
219
  Resolve `value` to either:
@@ -132,7 +237,7 @@ def resolve_to_absolute(value: str, base_uri: str | None = None) -> str:
132
237
  return _normalize_url(value)
133
238
 
134
239
  if is_file_uri(value):
135
- return _file_uri_to_path(value)
240
+ return file_uri_to_path(value)
136
241
 
137
242
  if _looks_like_windows_path(value):
138
243
  return _resolve_path_like(value, base_uri)
@@ -167,6 +272,36 @@ def resolve_to_absolute(value: str, base_uri: str | None = None) -> str:
167
272
  return _resolve_path_like(value, None)
168
273
 
169
274
 
275
+ def file_uri_to_path(file_uri: str) -> str:
276
+ """
277
+ Convert a file:// URI to an absolute filesystem path.
278
+
279
+ Args:
280
+ file_uri: A file:// URI string (e.g., "file:///path/to/file" or "file://server/share/path")
281
+
282
+ Returns:
283
+ Absolute filesystem path as a string
284
+
285
+ Raises:
286
+ URIResolutionError: If the input is not a valid file:// URI
287
+
288
+ Examples:
289
+ >>> file_uri_to_path("file:///home/user/doc.yaml")
290
+ '/home/user/doc.yaml'
291
+ >>> file_uri_to_path("file://localhost/etc/config.yaml")
292
+ '/etc/config.yaml'
293
+ """
294
+ parsed_uri = urlparse(file_uri)
295
+ if parsed_uri.scheme != "file":
296
+ raise URIResolutionError(f"Not a file URI: {file_uri!r}")
297
+ if parsed_uri.netloc and parsed_uri.netloc not in ("", "localhost"):
298
+ # UNC: \\server\share\path
299
+ unc = f"//{parsed_uri.netloc}{parsed_uri.path}"
300
+ return str(Path(url2pathname(unc)).resolve())
301
+ path = url2pathname(parsed_uri.path)
302
+ return str(Path(path).resolve())
303
+
304
+
170
305
  def _guard_single_line(s: str) -> None:
171
306
  if not isinstance(s, str) or ("\n" in s or "\r" in s):
172
307
  raise URIResolutionError("Input must be a single-line string.")
@@ -176,15 +311,6 @@ def _looks_like_windows_path(s: str) -> bool:
176
311
  return bool(_WINDOWS_DRIVE_RE.match(s) or _WINDOWS_UNC_RE.match(s))
177
312
 
178
313
 
179
- def is_http_https_url(s: str) -> bool:
180
- p = urlparse(s)
181
- return p.scheme in ("http", "https") and bool(p.netloc)
182
-
183
-
184
- def is_file_uri(s: str) -> bool:
185
- return urlparse(s).scheme == "file"
186
-
187
-
188
314
  def _normalize_url(s: str) -> str:
189
315
  import posixpath
190
316
 
@@ -197,24 +323,12 @@ def _normalize_url(s: str) -> str:
197
323
  return urlunsplit((parts.scheme, parts.netloc, normalized_path, parts.query, parts.fragment))
198
324
 
199
325
 
200
- def _file_uri_to_path(file_uri: str) -> str:
201
- p = urlparse(file_uri)
202
- if p.scheme != "file":
203
- raise URIResolutionError(f"Not a file URI: {file_uri!r}")
204
- if p.netloc and p.netloc not in ("", "localhost"):
205
- # UNC: \\server\share\path
206
- unc = f"//{p.netloc}{p.path}"
207
- return str(Path(urllib.request.url2pathname(unc)).resolve())
208
- path = urllib.request.url2pathname(p.path)
209
- return str(Path(path).resolve())
210
-
211
-
212
326
  def _resolve_path_like(value: str, base_uri: str | None) -> str:
213
327
  value = os.path.expandvars(os.path.expanduser(value))
214
328
 
215
329
  if base_uri:
216
330
  if is_file_uri(base_uri):
217
- base_path = Path(urllib.request.url2pathname(urlparse(base_uri).path))
331
+ base_path = Path(url2pathname(urlparse(base_uri).path))
218
332
  elif is_http_https_url(base_uri):
219
333
  # Don't silently combine a local path with a URL base
220
334
  raise URIResolutionError("Cannot resolve a local path against an HTTP(S) base_uri.")