schemathesis 3.15.4__py3-none-any.whl → 4.4.2__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.
Files changed (251) hide show
  1. schemathesis/__init__.py +53 -25
  2. schemathesis/auths.py +507 -0
  3. schemathesis/checks.py +190 -25
  4. schemathesis/cli/__init__.py +27 -1219
  5. schemathesis/cli/__main__.py +4 -0
  6. schemathesis/cli/commands/__init__.py +133 -0
  7. schemathesis/cli/commands/data.py +10 -0
  8. schemathesis/cli/commands/run/__init__.py +602 -0
  9. schemathesis/cli/commands/run/context.py +228 -0
  10. schemathesis/cli/commands/run/events.py +60 -0
  11. schemathesis/cli/commands/run/executor.py +157 -0
  12. schemathesis/cli/commands/run/filters.py +53 -0
  13. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  14. schemathesis/cli/commands/run/handlers/base.py +45 -0
  15. schemathesis/cli/commands/run/handlers/cassettes.py +464 -0
  16. schemathesis/cli/commands/run/handlers/junitxml.py +60 -0
  17. schemathesis/cli/commands/run/handlers/output.py +1750 -0
  18. schemathesis/cli/commands/run/loaders.py +118 -0
  19. schemathesis/cli/commands/run/validation.py +256 -0
  20. schemathesis/cli/constants.py +5 -0
  21. schemathesis/cli/core.py +19 -0
  22. schemathesis/cli/ext/fs.py +16 -0
  23. schemathesis/cli/ext/groups.py +203 -0
  24. schemathesis/cli/ext/options.py +81 -0
  25. schemathesis/config/__init__.py +202 -0
  26. schemathesis/config/_auth.py +51 -0
  27. schemathesis/config/_checks.py +268 -0
  28. schemathesis/config/_diff_base.py +101 -0
  29. schemathesis/config/_env.py +21 -0
  30. schemathesis/config/_error.py +163 -0
  31. schemathesis/config/_generation.py +157 -0
  32. schemathesis/config/_health_check.py +24 -0
  33. schemathesis/config/_operations.py +335 -0
  34. schemathesis/config/_output.py +171 -0
  35. schemathesis/config/_parameters.py +19 -0
  36. schemathesis/config/_phases.py +253 -0
  37. schemathesis/config/_projects.py +543 -0
  38. schemathesis/config/_rate_limit.py +17 -0
  39. schemathesis/config/_report.py +120 -0
  40. schemathesis/config/_validator.py +9 -0
  41. schemathesis/config/_warnings.py +89 -0
  42. schemathesis/config/schema.json +975 -0
  43. schemathesis/core/__init__.py +72 -0
  44. schemathesis/core/adapter.py +34 -0
  45. schemathesis/core/compat.py +32 -0
  46. schemathesis/core/control.py +2 -0
  47. schemathesis/core/curl.py +100 -0
  48. schemathesis/core/deserialization.py +210 -0
  49. schemathesis/core/errors.py +588 -0
  50. schemathesis/core/failures.py +316 -0
  51. schemathesis/core/fs.py +19 -0
  52. schemathesis/core/hooks.py +20 -0
  53. schemathesis/core/jsonschema/__init__.py +13 -0
  54. schemathesis/core/jsonschema/bundler.py +183 -0
  55. schemathesis/core/jsonschema/keywords.py +40 -0
  56. schemathesis/core/jsonschema/references.py +222 -0
  57. schemathesis/core/jsonschema/types.py +41 -0
  58. schemathesis/core/lazy_import.py +15 -0
  59. schemathesis/core/loaders.py +107 -0
  60. schemathesis/core/marks.py +66 -0
  61. schemathesis/core/media_types.py +79 -0
  62. schemathesis/core/output/__init__.py +46 -0
  63. schemathesis/core/output/sanitization.py +54 -0
  64. schemathesis/core/parameters.py +45 -0
  65. schemathesis/core/rate_limit.py +60 -0
  66. schemathesis/core/registries.py +34 -0
  67. schemathesis/core/result.py +27 -0
  68. schemathesis/core/schema_analysis.py +17 -0
  69. schemathesis/core/shell.py +203 -0
  70. schemathesis/core/transforms.py +144 -0
  71. schemathesis/core/transport.py +223 -0
  72. schemathesis/core/validation.py +73 -0
  73. schemathesis/core/version.py +7 -0
  74. schemathesis/engine/__init__.py +28 -0
  75. schemathesis/engine/context.py +152 -0
  76. schemathesis/engine/control.py +44 -0
  77. schemathesis/engine/core.py +201 -0
  78. schemathesis/engine/errors.py +446 -0
  79. schemathesis/engine/events.py +284 -0
  80. schemathesis/engine/observations.py +42 -0
  81. schemathesis/engine/phases/__init__.py +108 -0
  82. schemathesis/engine/phases/analysis.py +28 -0
  83. schemathesis/engine/phases/probes.py +172 -0
  84. schemathesis/engine/phases/stateful/__init__.py +68 -0
  85. schemathesis/engine/phases/stateful/_executor.py +364 -0
  86. schemathesis/engine/phases/stateful/context.py +85 -0
  87. schemathesis/engine/phases/unit/__init__.py +220 -0
  88. schemathesis/engine/phases/unit/_executor.py +459 -0
  89. schemathesis/engine/phases/unit/_pool.py +82 -0
  90. schemathesis/engine/recorder.py +254 -0
  91. schemathesis/errors.py +47 -0
  92. schemathesis/filters.py +395 -0
  93. schemathesis/generation/__init__.py +25 -0
  94. schemathesis/generation/case.py +478 -0
  95. schemathesis/generation/coverage.py +1528 -0
  96. schemathesis/generation/hypothesis/__init__.py +121 -0
  97. schemathesis/generation/hypothesis/builder.py +992 -0
  98. schemathesis/generation/hypothesis/examples.py +56 -0
  99. schemathesis/generation/hypothesis/given.py +66 -0
  100. schemathesis/generation/hypothesis/reporting.py +285 -0
  101. schemathesis/generation/meta.py +227 -0
  102. schemathesis/generation/metrics.py +93 -0
  103. schemathesis/generation/modes.py +20 -0
  104. schemathesis/generation/overrides.py +127 -0
  105. schemathesis/generation/stateful/__init__.py +37 -0
  106. schemathesis/generation/stateful/state_machine.py +294 -0
  107. schemathesis/graphql/__init__.py +15 -0
  108. schemathesis/graphql/checks.py +109 -0
  109. schemathesis/graphql/loaders.py +285 -0
  110. schemathesis/hooks.py +270 -91
  111. schemathesis/openapi/__init__.py +13 -0
  112. schemathesis/openapi/checks.py +467 -0
  113. schemathesis/openapi/generation/__init__.py +0 -0
  114. schemathesis/openapi/generation/filters.py +72 -0
  115. schemathesis/openapi/loaders.py +315 -0
  116. schemathesis/pytest/__init__.py +5 -0
  117. schemathesis/pytest/control_flow.py +7 -0
  118. schemathesis/pytest/lazy.py +341 -0
  119. schemathesis/pytest/loaders.py +36 -0
  120. schemathesis/pytest/plugin.py +357 -0
  121. schemathesis/python/__init__.py +0 -0
  122. schemathesis/python/asgi.py +12 -0
  123. schemathesis/python/wsgi.py +12 -0
  124. schemathesis/schemas.py +682 -257
  125. schemathesis/specs/graphql/__init__.py +0 -1
  126. schemathesis/specs/graphql/nodes.py +26 -2
  127. schemathesis/specs/graphql/scalars.py +77 -12
  128. schemathesis/specs/graphql/schemas.py +367 -148
  129. schemathesis/specs/graphql/validation.py +33 -0
  130. schemathesis/specs/openapi/__init__.py +9 -1
  131. schemathesis/specs/openapi/_hypothesis.py +555 -318
  132. schemathesis/specs/openapi/adapter/__init__.py +10 -0
  133. schemathesis/specs/openapi/adapter/parameters.py +729 -0
  134. schemathesis/specs/openapi/adapter/protocol.py +59 -0
  135. schemathesis/specs/openapi/adapter/references.py +19 -0
  136. schemathesis/specs/openapi/adapter/responses.py +368 -0
  137. schemathesis/specs/openapi/adapter/security.py +144 -0
  138. schemathesis/specs/openapi/adapter/v2.py +30 -0
  139. schemathesis/specs/openapi/adapter/v3_0.py +30 -0
  140. schemathesis/specs/openapi/adapter/v3_1.py +30 -0
  141. schemathesis/specs/openapi/analysis.py +96 -0
  142. schemathesis/specs/openapi/checks.py +748 -82
  143. schemathesis/specs/openapi/converter.py +176 -37
  144. schemathesis/specs/openapi/definitions.py +599 -4
  145. schemathesis/specs/openapi/examples.py +581 -165
  146. schemathesis/specs/openapi/expressions/__init__.py +52 -5
  147. schemathesis/specs/openapi/expressions/extractors.py +25 -0
  148. schemathesis/specs/openapi/expressions/lexer.py +34 -31
  149. schemathesis/specs/openapi/expressions/nodes.py +97 -46
  150. schemathesis/specs/openapi/expressions/parser.py +35 -13
  151. schemathesis/specs/openapi/formats.py +122 -0
  152. schemathesis/specs/openapi/media_types.py +75 -0
  153. schemathesis/specs/openapi/negative/__init__.py +93 -73
  154. schemathesis/specs/openapi/negative/mutations.py +294 -103
  155. schemathesis/specs/openapi/negative/utils.py +0 -9
  156. schemathesis/specs/openapi/patterns.py +458 -0
  157. schemathesis/specs/openapi/references.py +60 -81
  158. schemathesis/specs/openapi/schemas.py +647 -666
  159. schemathesis/specs/openapi/serialization.py +53 -30
  160. schemathesis/specs/openapi/stateful/__init__.py +403 -68
  161. schemathesis/specs/openapi/stateful/control.py +87 -0
  162. schemathesis/specs/openapi/stateful/dependencies/__init__.py +232 -0
  163. schemathesis/specs/openapi/stateful/dependencies/inputs.py +428 -0
  164. schemathesis/specs/openapi/stateful/dependencies/models.py +341 -0
  165. schemathesis/specs/openapi/stateful/dependencies/naming.py +491 -0
  166. schemathesis/specs/openapi/stateful/dependencies/outputs.py +34 -0
  167. schemathesis/specs/openapi/stateful/dependencies/resources.py +339 -0
  168. schemathesis/specs/openapi/stateful/dependencies/schemas.py +447 -0
  169. schemathesis/specs/openapi/stateful/inference.py +254 -0
  170. schemathesis/specs/openapi/stateful/links.py +219 -78
  171. schemathesis/specs/openapi/types/__init__.py +3 -0
  172. schemathesis/specs/openapi/types/common.py +23 -0
  173. schemathesis/specs/openapi/types/v2.py +129 -0
  174. schemathesis/specs/openapi/types/v3.py +134 -0
  175. schemathesis/specs/openapi/utils.py +7 -6
  176. schemathesis/specs/openapi/warnings.py +75 -0
  177. schemathesis/transport/__init__.py +224 -0
  178. schemathesis/transport/asgi.py +26 -0
  179. schemathesis/transport/prepare.py +126 -0
  180. schemathesis/transport/requests.py +278 -0
  181. schemathesis/transport/serialization.py +329 -0
  182. schemathesis/transport/wsgi.py +175 -0
  183. schemathesis-4.4.2.dist-info/METADATA +213 -0
  184. schemathesis-4.4.2.dist-info/RECORD +192 -0
  185. {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
  186. schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
  187. {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
  188. schemathesis/_compat.py +0 -57
  189. schemathesis/_hypothesis.py +0 -123
  190. schemathesis/auth.py +0 -214
  191. schemathesis/cli/callbacks.py +0 -240
  192. schemathesis/cli/cassettes.py +0 -351
  193. schemathesis/cli/context.py +0 -38
  194. schemathesis/cli/debug.py +0 -21
  195. schemathesis/cli/handlers.py +0 -11
  196. schemathesis/cli/junitxml.py +0 -41
  197. schemathesis/cli/options.py +0 -70
  198. schemathesis/cli/output/__init__.py +0 -1
  199. schemathesis/cli/output/default.py +0 -521
  200. schemathesis/cli/output/short.py +0 -40
  201. schemathesis/constants.py +0 -88
  202. schemathesis/exceptions.py +0 -257
  203. schemathesis/extra/_aiohttp.py +0 -27
  204. schemathesis/extra/_flask.py +0 -10
  205. schemathesis/extra/_server.py +0 -16
  206. schemathesis/extra/pytest_plugin.py +0 -251
  207. schemathesis/failures.py +0 -145
  208. schemathesis/fixups/__init__.py +0 -29
  209. schemathesis/fixups/fast_api.py +0 -30
  210. schemathesis/graphql.py +0 -5
  211. schemathesis/internal.py +0 -6
  212. schemathesis/lazy.py +0 -301
  213. schemathesis/models.py +0 -1113
  214. schemathesis/parameters.py +0 -91
  215. schemathesis/runner/__init__.py +0 -470
  216. schemathesis/runner/events.py +0 -242
  217. schemathesis/runner/impl/__init__.py +0 -3
  218. schemathesis/runner/impl/core.py +0 -791
  219. schemathesis/runner/impl/solo.py +0 -85
  220. schemathesis/runner/impl/threadpool.py +0 -367
  221. schemathesis/runner/serialization.py +0 -206
  222. schemathesis/serializers.py +0 -253
  223. schemathesis/service/__init__.py +0 -18
  224. schemathesis/service/auth.py +0 -10
  225. schemathesis/service/client.py +0 -62
  226. schemathesis/service/constants.py +0 -25
  227. schemathesis/service/events.py +0 -39
  228. schemathesis/service/handler.py +0 -46
  229. schemathesis/service/hosts.py +0 -74
  230. schemathesis/service/metadata.py +0 -42
  231. schemathesis/service/models.py +0 -21
  232. schemathesis/service/serialization.py +0 -184
  233. schemathesis/service/worker.py +0 -39
  234. schemathesis/specs/graphql/loaders.py +0 -215
  235. schemathesis/specs/openapi/constants.py +0 -7
  236. schemathesis/specs/openapi/expressions/context.py +0 -12
  237. schemathesis/specs/openapi/expressions/pointers.py +0 -29
  238. schemathesis/specs/openapi/filters.py +0 -44
  239. schemathesis/specs/openapi/links.py +0 -303
  240. schemathesis/specs/openapi/loaders.py +0 -453
  241. schemathesis/specs/openapi/parameters.py +0 -430
  242. schemathesis/specs/openapi/security.py +0 -129
  243. schemathesis/specs/openapi/validation.py +0 -24
  244. schemathesis/stateful.py +0 -358
  245. schemathesis/targets.py +0 -32
  246. schemathesis/types.py +0 -38
  247. schemathesis/utils.py +0 -475
  248. schemathesis-3.15.4.dist-info/METADATA +0 -202
  249. schemathesis-3.15.4.dist-info/RECORD +0 -99
  250. schemathesis-3.15.4.dist-info/entry_points.txt +0 -7
  251. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ from contextlib import nullcontext
4
+ from typing import TYPE_CHECKING, ContextManager
5
+ from urllib.parse import urlparse
6
+
7
+ from schemathesis.core.errors import InvalidRateLimit
8
+
9
+ if TYPE_CHECKING:
10
+ from pyrate_limiter import Duration, Limiter
11
+
12
+
13
+ def ratelimit(rate_limiter: Limiter | None, base_url: str | None) -> ContextManager:
14
+ """Limit the rate of sending generated requests."""
15
+ label = urlparse(base_url).netloc
16
+ if rate_limiter is not None:
17
+ rate_limiter.try_acquire(label)
18
+ return nullcontext()
19
+
20
+
21
+ def parse_units(rate: str) -> tuple[int, int]:
22
+ from pyrate_limiter import Duration
23
+
24
+ try:
25
+ limit, interval_text = rate.split("/")
26
+ interval = {
27
+ "s": Duration.SECOND,
28
+ "m": Duration.MINUTE,
29
+ "h": Duration.HOUR,
30
+ "d": Duration.DAY,
31
+ }.get(interval_text)
32
+ if interval is None:
33
+ raise InvalidRateLimit(rate)
34
+ return int(limit), interval
35
+ except ValueError as exc:
36
+ raise InvalidRateLimit(rate) from exc
37
+
38
+
39
+ def _get_max_delay(value: int, unit: Duration) -> int:
40
+ from pyrate_limiter import Duration
41
+
42
+ if unit == Duration.SECOND:
43
+ multiplier = 1
44
+ elif unit == Duration.MINUTE:
45
+ multiplier = 60
46
+ elif unit == Duration.HOUR:
47
+ multiplier = 60 * 60
48
+ else:
49
+ multiplier = 60 * 60 * 24
50
+ # Delay is in milliseconds + `pyrate_limiter` adds 50ms on top.
51
+ # Hence adding 100 covers this
52
+ return value * multiplier * 1000 + 100
53
+
54
+
55
+ def build_limiter(rate: str) -> Limiter:
56
+ from pyrate_limiter import Limiter, Rate
57
+
58
+ limit, interval = parse_units(rate)
59
+ rate = Rate(limit, interval)
60
+ return Limiter(rate, max_delay=_get_max_delay(limit, interval))
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Callable, Generic, Sequence, TypeVar, Union
4
+
5
+ T = TypeVar("T", bound=Union[Callable, type])
6
+
7
+
8
+ class Registry(Generic[T]):
9
+ """Container for Schemathesis extensions."""
10
+
11
+ __slots__ = ("_items",)
12
+
13
+ def __init__(self) -> None:
14
+ self._items: dict[str, T] = {}
15
+
16
+ def register(self, item: T) -> T:
17
+ self._items[item.__name__] = item
18
+ return item
19
+
20
+ def unregister(self, name: str) -> None:
21
+ del self._items[name]
22
+
23
+ def get_all_names(self) -> list[str]:
24
+ return list(self._items)
25
+
26
+ def get_all(self) -> list[T]:
27
+ return list(self._items.values())
28
+
29
+ def get_one(self, name: str) -> T:
30
+ return self._items[name]
31
+
32
+ def get_by_names(self, names: Sequence[str]) -> list[T]:
33
+ """Get items by their names."""
34
+ return [self._items[name] for name in names]
@@ -0,0 +1,27 @@
1
+ from typing import Generic, TypeVar, Union
2
+
3
+ T = TypeVar("T")
4
+ E = TypeVar("E", bound=Exception)
5
+
6
+
7
+ class Ok(Generic[T]):
8
+ __slots__ = ("_value",)
9
+
10
+ def __init__(self, value: T):
11
+ self._value = value
12
+
13
+ def ok(self) -> T:
14
+ return self._value
15
+
16
+
17
+ class Err(Generic[E]):
18
+ __slots__ = ("_error",)
19
+
20
+ def __init__(self, error: E):
21
+ self._error = error
22
+
23
+ def err(self) -> E:
24
+ return self._error
25
+
26
+
27
+ Result = Union[Ok[T], Err[E]]
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Protocol
4
+
5
+ from schemathesis.config import SchemathesisWarning
6
+
7
+
8
+ class SchemaWarning(Protocol):
9
+ """Shared interface for static schema analysis warnings."""
10
+
11
+ operation_label: str
12
+
13
+ @property
14
+ def kind(self) -> SchemathesisWarning: ...
15
+
16
+ @property
17
+ def message(self) -> str: ...
@@ -0,0 +1,203 @@
1
+ """Shell detection and escaping for generating reproducible curl commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from dataclasses import dataclass
7
+ from enum import Enum
8
+
9
+
10
+ class ShellType(str, Enum):
11
+ """Supported shell types."""
12
+
13
+ BASH = "bash"
14
+ ZSH = "zsh"
15
+ FISH = "fish"
16
+ UNKNOWN = "unknown"
17
+
18
+ @property
19
+ def supports_ansi_c_quoting(self) -> bool:
20
+ r"""Whether shell supports $'...\xHH' syntax."""
21
+ return self in (ShellType.BASH, ShellType.ZSH)
22
+
23
+ @property
24
+ def supports_hex_in_quotes(self) -> bool:
25
+ r"""Whether shell interprets \xHH in single quotes."""
26
+ return self == ShellType.FISH
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class EscapeResult:
31
+ """Result of escaping a value for shell."""
32
+
33
+ escaped_value: str
34
+ """The escaped string ready for shell."""
35
+
36
+ needs_warning: bool
37
+ """Whether a warning should be shown to the user."""
38
+
39
+ original_bytes: bytes | None
40
+ """Original bytes if warning is needed, for detailed display."""
41
+
42
+ shell_used: ShellType
43
+ """Which shell type the escaping is for."""
44
+
45
+ __slots__ = ("escaped_value", "needs_warning", "original_bytes", "shell_used")
46
+
47
+
48
+ _DETECTED_SHELL: ShellType | None = None
49
+
50
+
51
+ def detect_shell() -> ShellType:
52
+ """Detect the current shell type from $SHELL environment variable."""
53
+ global _DETECTED_SHELL
54
+
55
+ if _DETECTED_SHELL is not None:
56
+ return _DETECTED_SHELL
57
+
58
+ # Check $SHELL environment variable
59
+ shell_path = os.environ.get("SHELL", "")
60
+ if shell_path:
61
+ shell_name = os.path.basename(shell_path).lower()
62
+ detected = _parse_shell_name(shell_name)
63
+ _DETECTED_SHELL = detected
64
+ return detected
65
+
66
+ _DETECTED_SHELL = ShellType.UNKNOWN
67
+ return ShellType.UNKNOWN
68
+
69
+
70
+ def _parse_shell_name(name: str) -> ShellType:
71
+ """Parse shell name string to ShellType."""
72
+ name_lower = name.lower()
73
+
74
+ # Check exact matches first
75
+ for shell_type in (ShellType.BASH, ShellType.ZSH, ShellType.FISH):
76
+ if shell_type.value == name_lower:
77
+ return shell_type
78
+
79
+ # Check substring matches
80
+ for shell_type in (ShellType.BASH, ShellType.ZSH, ShellType.FISH):
81
+ if shell_type.value in name_lower:
82
+ return shell_type
83
+
84
+ return ShellType.UNKNOWN
85
+
86
+
87
+ def has_non_printable(value: str | bytes) -> bool:
88
+ """Check if value contains ASCII control characters."""
89
+ if isinstance(value, bytes):
90
+ try:
91
+ value = value.decode("utf-8")
92
+ except UnicodeDecodeError:
93
+ # Binary data that can't be decoded - treat as non-printable
94
+ return True
95
+
96
+ # Check for ASCII control characters: 0-31 and 127 (DEL)
97
+ return any(ord(c) < 32 or ord(c) == 127 for c in value)
98
+
99
+
100
+ def escape_for_shell(value: str, shell: ShellType | None = None) -> EscapeResult:
101
+ """Escape value for shell use in curl commands."""
102
+ if shell is None:
103
+ shell = detect_shell()
104
+
105
+ # Fast path: no non-printable characters
106
+ if not has_non_printable(value):
107
+ return EscapeResult(
108
+ escaped_value=value,
109
+ needs_warning=False,
110
+ original_bytes=None,
111
+ shell_used=shell,
112
+ )
113
+
114
+ original_bytes = value.encode("utf-8")
115
+
116
+ # Bash/Zsh: Use ANSI-C quoting $'...\xHH'
117
+ if shell.supports_ansi_c_quoting:
118
+ escaped = _escape_with_ansi_c(value)
119
+ return EscapeResult(
120
+ escaped_value=f"$'{escaped}'",
121
+ needs_warning=False,
122
+ original_bytes=None,
123
+ shell_used=shell,
124
+ )
125
+
126
+ # Fish: Use \xHH in single quotes
127
+ if shell.supports_hex_in_quotes:
128
+ escaped = _escape_with_hex(value)
129
+ return EscapeResult(
130
+ escaped_value=f"'{escaped}'",
131
+ needs_warning=False,
132
+ original_bytes=None,
133
+ shell_used=shell,
134
+ )
135
+
136
+ # Unknown shell: Show bash-style with warning
137
+ escaped = _escape_with_ansi_c(value)
138
+ return EscapeResult(
139
+ escaped_value=f"$'{escaped}'",
140
+ needs_warning=True,
141
+ original_bytes=original_bytes,
142
+ shell_used=ShellType.BASH,
143
+ )
144
+
145
+
146
+ def _escape_with_ansi_c(value: str) -> str:
147
+ """Escape string for ANSI-C quoting ($'...') used in bash/zsh."""
148
+ result = []
149
+ for char in value:
150
+ code = ord(char)
151
+
152
+ # Readable escapes for common control characters
153
+ if char == "\t":
154
+ result.append("\\t")
155
+ elif char == "\n":
156
+ result.append("\\n")
157
+ elif char == "\r":
158
+ result.append("\\r")
159
+ elif code < 32:
160
+ # Other control characters as hex
161
+ result.append(f"\\x{code:02x}")
162
+ elif code == 127:
163
+ # DEL character
164
+ result.append("\\x7f")
165
+ elif char in ("'", "\\", "$", "`"):
166
+ # Shell special characters that need escaping in $'...'
167
+ result.append(f"\\{char}")
168
+ else:
169
+ result.append(char)
170
+
171
+ return "".join(result)
172
+
173
+
174
+ def _escape_with_hex(value: str) -> str:
175
+ r"""Escape string with \xHH notation for fish shell.
176
+
177
+ Fish interprets \x escapes directly in single quotes.
178
+ We still need to escape single quotes and backslashes.
179
+ """
180
+ result = []
181
+ for char in value:
182
+ code = ord(char)
183
+
184
+ # Readable escapes for common control characters
185
+ if char == "\t":
186
+ result.append("\\t")
187
+ elif char == "\n":
188
+ result.append("\\n")
189
+ elif char == "\r":
190
+ result.append("\\r")
191
+ elif code < 32 or code == 127:
192
+ # Control characters as hex
193
+ result.append(f"\\x{code:02x}")
194
+ elif char == "'":
195
+ # Escape single quote for fish
196
+ result.append("\\'")
197
+ elif char == "\\":
198
+ # Escape backslash
199
+ result.append("\\\\")
200
+ else:
201
+ result.append(char)
202
+
203
+ return "".join(result)
@@ -0,0 +1,144 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable, Dict, Iterator, List, Mapping, TypeVar, Union, overload
4
+
5
+ T = TypeVar("T")
6
+
7
+
8
+ @overload
9
+ def deepclone(value: dict) -> dict: ... # pragma: no cover
10
+
11
+
12
+ @overload
13
+ def deepclone(value: list) -> list: ... # pragma: no cover
14
+
15
+
16
+ @overload
17
+ def deepclone(value: T) -> T: ... # pragma: no cover
18
+
19
+
20
+ def deepclone(value: Any) -> Any:
21
+ """A specialized version of `deepcopy` that copies only `dict` and `list` and does unrolling.
22
+
23
+ It is on average 3x faster than `deepcopy` and given the amount of calls, it is an important optimization.
24
+ """
25
+ if isinstance(value, dict):
26
+ return {
27
+ k1: (
28
+ {
29
+ k2: (
30
+ {k3: deepclone(v3) for k3, v3 in v2.items()}
31
+ if isinstance(v2, dict)
32
+ else [deepclone(v3) for v3 in v2]
33
+ if isinstance(v2, list)
34
+ else v2
35
+ )
36
+ for k2, v2 in v1.items()
37
+ }
38
+ if isinstance(v1, dict)
39
+ else [deepclone(v2) for v2 in v1]
40
+ if isinstance(v1, list)
41
+ else v1
42
+ )
43
+ for k1, v1 in value.items()
44
+ }
45
+ if isinstance(value, list):
46
+ return [
47
+ {k2: deepclone(v2) for k2, v2 in v1.items()}
48
+ if isinstance(v1, dict)
49
+ else [deepclone(v2) for v2 in v1]
50
+ if isinstance(v1, list)
51
+ else v1
52
+ for v1 in value
53
+ ]
54
+ return value
55
+
56
+
57
+ def diff(left: Mapping[str, Any], right: Mapping[str, Any]) -> dict[str, Any]:
58
+ """Calculate the difference between two dictionaries."""
59
+ diff = {}
60
+ for key, value in right.items():
61
+ if key not in left or left[key] != value:
62
+ diff[key] = value
63
+ return diff
64
+
65
+
66
+ def merge_at(data: dict[str, Any], data_key: str, new: dict[str, Any]) -> None:
67
+ original = data[data_key] or {}
68
+ for key, value in new.items():
69
+ original[key] = value
70
+ data[data_key] = original
71
+
72
+
73
+ JsonValue = Union[Dict[str, Any], List, str, float, int]
74
+
75
+
76
+ @overload
77
+ def transform(schema: dict[str, Any], callback: Callable, *args: Any, **kwargs: Any) -> dict[str, Any]: ...
78
+
79
+
80
+ @overload
81
+ def transform(schema: list, callback: Callable, *args: Any, **kwargs: Any) -> list: ...
82
+
83
+
84
+ @overload
85
+ def transform(schema: str, callback: Callable, *args: Any, **kwargs: Any) -> str: ...
86
+
87
+
88
+ @overload
89
+ def transform(schema: float, callback: Callable, *args: Any, **kwargs: Any) -> float: ...
90
+
91
+
92
+ def transform(schema: JsonValue, callback: Callable[..., dict[str, Any]], *args: Any, **kwargs: Any) -> JsonValue:
93
+ """Apply callback recursively to the given schema."""
94
+ if isinstance(schema, dict):
95
+ schema = callback(schema, *args, **kwargs)
96
+ for key, sub_item in schema.items():
97
+ schema[key] = transform(sub_item, callback, *args, **kwargs)
98
+ elif isinstance(schema, list):
99
+ schema = [transform(sub_item, callback, *args, **kwargs) for sub_item in schema]
100
+ return schema
101
+
102
+
103
+ class Unresolvable: ...
104
+
105
+
106
+ UNRESOLVABLE = Unresolvable()
107
+
108
+
109
+ def encode_pointer(pointer: str) -> str:
110
+ return pointer.replace("~", "~0").replace("/", "~1")
111
+
112
+
113
+ def decode_pointer(value: str) -> str:
114
+ return value.replace("~1", "/").replace("~0", "~")
115
+
116
+
117
+ def iter_decoded_pointer_segments(pointer: str) -> Iterator[str]:
118
+ return map(decode_pointer, pointer.split("/")[1:])
119
+
120
+
121
+ def resolve_pointer(document: Any, pointer: str) -> dict | list | str | int | float | None | Unresolvable:
122
+ """Implementation is adapted from Rust's `serde-json` crate.
123
+
124
+ Ref: https://github.com/serde-rs/json/blob/master/src/value/mod.rs#L751
125
+ """
126
+ if not pointer:
127
+ return document
128
+ if not pointer.startswith("/"):
129
+ return UNRESOLVABLE
130
+
131
+ target = document
132
+ for token in iter_decoded_pointer_segments(pointer):
133
+ if isinstance(target, dict):
134
+ target = target.get(token, UNRESOLVABLE)
135
+ if target is UNRESOLVABLE:
136
+ return UNRESOLVABLE
137
+ elif isinstance(target, list):
138
+ try:
139
+ target = target[int(token)]
140
+ except (IndexError, ValueError):
141
+ return UNRESOLVABLE
142
+ else:
143
+ return UNRESOLVABLE
144
+ return target