oscura 0.7.0__py3-none-any.whl → 0.10.0__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 (175) hide show
  1. oscura/__init__.py +19 -19
  2. oscura/analyzers/__init__.py +2 -0
  3. oscura/analyzers/digital/extraction.py +2 -3
  4. oscura/analyzers/digital/quality.py +1 -1
  5. oscura/analyzers/digital/timing.py +1 -1
  6. oscura/analyzers/eye/__init__.py +5 -1
  7. oscura/analyzers/eye/generation.py +501 -0
  8. oscura/analyzers/jitter/__init__.py +6 -6
  9. oscura/analyzers/jitter/timing.py +419 -0
  10. oscura/analyzers/patterns/__init__.py +94 -0
  11. oscura/analyzers/patterns/reverse_engineering.py +991 -0
  12. oscura/analyzers/power/__init__.py +35 -12
  13. oscura/analyzers/power/basic.py +3 -3
  14. oscura/analyzers/power/soa.py +1 -1
  15. oscura/analyzers/power/switching.py +3 -3
  16. oscura/analyzers/signal_classification.py +529 -0
  17. oscura/analyzers/signal_integrity/sparams.py +3 -3
  18. oscura/analyzers/statistics/__init__.py +4 -0
  19. oscura/analyzers/statistics/basic.py +152 -0
  20. oscura/analyzers/statistics/correlation.py +47 -6
  21. oscura/analyzers/validation.py +1 -1
  22. oscura/analyzers/waveform/__init__.py +2 -0
  23. oscura/analyzers/waveform/measurements.py +329 -163
  24. oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
  25. oscura/analyzers/waveform/spectral.py +498 -54
  26. oscura/api/dsl/commands.py +15 -6
  27. oscura/api/server/templates/base.html +137 -146
  28. oscura/api/server/templates/export.html +84 -110
  29. oscura/api/server/templates/home.html +248 -267
  30. oscura/api/server/templates/protocols.html +44 -48
  31. oscura/api/server/templates/reports.html +27 -35
  32. oscura/api/server/templates/session_detail.html +68 -78
  33. oscura/api/server/templates/sessions.html +62 -72
  34. oscura/api/server/templates/waveforms.html +54 -64
  35. oscura/automotive/__init__.py +1 -1
  36. oscura/automotive/can/session.py +1 -1
  37. oscura/automotive/dbc/generator.py +638 -23
  38. oscura/automotive/dtc/data.json +102 -17
  39. oscura/automotive/uds/decoder.py +99 -6
  40. oscura/cli/analyze.py +8 -2
  41. oscura/cli/batch.py +36 -5
  42. oscura/cli/characterize.py +18 -4
  43. oscura/cli/export.py +47 -5
  44. oscura/cli/main.py +2 -0
  45. oscura/cli/onboarding/wizard.py +10 -6
  46. oscura/cli/pipeline.py +585 -0
  47. oscura/cli/visualize.py +6 -4
  48. oscura/convenience.py +400 -32
  49. oscura/core/config/loader.py +0 -1
  50. oscura/core/measurement_result.py +286 -0
  51. oscura/core/progress.py +1 -1
  52. oscura/core/schemas/device_mapping.json +8 -2
  53. oscura/core/schemas/packet_format.json +24 -4
  54. oscura/core/schemas/protocol_definition.json +12 -2
  55. oscura/core/types.py +300 -199
  56. oscura/correlation/multi_protocol.py +1 -1
  57. oscura/export/legacy/__init__.py +11 -0
  58. oscura/export/legacy/wav.py +75 -0
  59. oscura/exporters/__init__.py +19 -0
  60. oscura/exporters/wireshark.py +809 -0
  61. oscura/hardware/acquisition/file.py +5 -19
  62. oscura/hardware/acquisition/saleae.py +10 -10
  63. oscura/hardware/acquisition/socketcan.py +4 -6
  64. oscura/hardware/acquisition/synthetic.py +1 -5
  65. oscura/hardware/acquisition/visa.py +6 -6
  66. oscura/hardware/security/side_channel_detector.py +5 -508
  67. oscura/inference/message_format.py +686 -1
  68. oscura/jupyter/display.py +2 -2
  69. oscura/jupyter/magic.py +3 -3
  70. oscura/loaders/__init__.py +17 -12
  71. oscura/loaders/binary.py +1 -1
  72. oscura/loaders/chipwhisperer.py +1 -2
  73. oscura/loaders/configurable.py +1 -1
  74. oscura/loaders/csv_loader.py +2 -2
  75. oscura/loaders/hdf5_loader.py +1 -1
  76. oscura/loaders/lazy.py +6 -1
  77. oscura/loaders/mmap_loader.py +0 -1
  78. oscura/loaders/numpy_loader.py +8 -7
  79. oscura/loaders/preprocessing.py +3 -5
  80. oscura/loaders/rigol.py +21 -7
  81. oscura/loaders/sigrok.py +2 -5
  82. oscura/loaders/tdms.py +3 -2
  83. oscura/loaders/tektronix.py +38 -32
  84. oscura/loaders/tss.py +20 -27
  85. oscura/loaders/vcd.py +13 -8
  86. oscura/loaders/wav.py +1 -6
  87. oscura/pipeline/__init__.py +76 -0
  88. oscura/pipeline/handlers/__init__.py +165 -0
  89. oscura/pipeline/handlers/analyzers.py +1045 -0
  90. oscura/pipeline/handlers/decoders.py +899 -0
  91. oscura/pipeline/handlers/exporters.py +1103 -0
  92. oscura/pipeline/handlers/filters.py +891 -0
  93. oscura/pipeline/handlers/loaders.py +640 -0
  94. oscura/pipeline/handlers/transforms.py +768 -0
  95. oscura/reporting/__init__.py +88 -1
  96. oscura/reporting/automation.py +348 -0
  97. oscura/reporting/citations.py +374 -0
  98. oscura/reporting/core.py +54 -0
  99. oscura/reporting/formatting/__init__.py +11 -0
  100. oscura/reporting/formatting/measurements.py +320 -0
  101. oscura/reporting/html.py +57 -0
  102. oscura/reporting/interpretation.py +431 -0
  103. oscura/reporting/summary.py +329 -0
  104. oscura/reporting/templates/enhanced/protocol_re.html +504 -503
  105. oscura/reporting/visualization.py +542 -0
  106. oscura/side_channel/__init__.py +38 -57
  107. oscura/utils/builders/signal_builder.py +5 -5
  108. oscura/utils/comparison/compare.py +7 -9
  109. oscura/utils/comparison/golden.py +1 -1
  110. oscura/utils/filtering/convenience.py +2 -2
  111. oscura/utils/math/arithmetic.py +38 -62
  112. oscura/utils/math/interpolation.py +20 -20
  113. oscura/utils/pipeline/__init__.py +4 -17
  114. oscura/utils/progressive.py +1 -4
  115. oscura/utils/triggering/edge.py +1 -1
  116. oscura/utils/triggering/pattern.py +2 -2
  117. oscura/utils/triggering/pulse.py +2 -2
  118. oscura/utils/triggering/window.py +3 -3
  119. oscura/validation/hil_testing.py +11 -11
  120. oscura/visualization/__init__.py +47 -284
  121. oscura/visualization/batch.py +160 -0
  122. oscura/visualization/plot.py +542 -53
  123. oscura/visualization/styles.py +184 -318
  124. oscura/workflows/__init__.py +2 -0
  125. oscura/workflows/batch/advanced.py +1 -1
  126. oscura/workflows/batch/aggregate.py +7 -8
  127. oscura/workflows/complete_re.py +251 -23
  128. oscura/workflows/digital.py +27 -4
  129. oscura/workflows/multi_trace.py +136 -17
  130. oscura/workflows/waveform.py +788 -0
  131. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/METADATA +59 -79
  132. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/RECORD +135 -149
  133. oscura/side_channel/dpa.py +0 -1025
  134. oscura/utils/optimization/__init__.py +0 -19
  135. oscura/utils/optimization/parallel.py +0 -443
  136. oscura/utils/optimization/search.py +0 -532
  137. oscura/utils/pipeline/base.py +0 -338
  138. oscura/utils/pipeline/composition.py +0 -248
  139. oscura/utils/pipeline/parallel.py +0 -449
  140. oscura/utils/pipeline/pipeline.py +0 -375
  141. oscura/utils/search/__init__.py +0 -16
  142. oscura/utils/search/anomaly.py +0 -424
  143. oscura/utils/search/context.py +0 -294
  144. oscura/utils/search/pattern.py +0 -288
  145. oscura/utils/storage/__init__.py +0 -61
  146. oscura/utils/storage/database.py +0 -1166
  147. oscura/visualization/accessibility.py +0 -526
  148. oscura/visualization/annotations.py +0 -371
  149. oscura/visualization/axis_scaling.py +0 -305
  150. oscura/visualization/colors.py +0 -451
  151. oscura/visualization/digital.py +0 -436
  152. oscura/visualization/eye.py +0 -571
  153. oscura/visualization/histogram.py +0 -281
  154. oscura/visualization/interactive.py +0 -1035
  155. oscura/visualization/jitter.py +0 -1042
  156. oscura/visualization/keyboard.py +0 -394
  157. oscura/visualization/layout.py +0 -400
  158. oscura/visualization/optimization.py +0 -1079
  159. oscura/visualization/palettes.py +0 -446
  160. oscura/visualization/power.py +0 -508
  161. oscura/visualization/power_extended.py +0 -955
  162. oscura/visualization/presets.py +0 -469
  163. oscura/visualization/protocols.py +0 -1246
  164. oscura/visualization/render.py +0 -223
  165. oscura/visualization/rendering.py +0 -444
  166. oscura/visualization/reverse_engineering.py +0 -838
  167. oscura/visualization/signal_integrity.py +0 -989
  168. oscura/visualization/specialized.py +0 -643
  169. oscura/visualization/spectral.py +0 -1226
  170. oscura/visualization/thumbnails.py +0 -340
  171. oscura/visualization/time_axis.py +0 -351
  172. oscura/visualization/waveform.py +0 -454
  173. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/WHEEL +0 -0
  174. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/entry_points.txt +0 -0
  175. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,286 @@
1
+ """Helper functions for creating MeasurementResult instances.
2
+
3
+ This module provides utilities to construct MeasurementResult TypedDict
4
+ instances with proper formatting and applicability tracking.
5
+
6
+ Example:
7
+ >>> from oscura.core.measurement_result import make_measurement, make_inapplicable
8
+ >>> # Applicable measurement
9
+ >>> freq = make_measurement(1000.0, "Hz")
10
+ >>> print(freq["display"])
11
+ 1.000 kHz
12
+
13
+ >>> # Inapplicable measurement
14
+ >>> period = make_inapplicable("s", "Aperiodic signal")
15
+ >>> print(f"{period['display']} - {period['reason']}")
16
+ N/A - Aperiodic signal
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import math
22
+ from typing import TYPE_CHECKING, Any
23
+
24
+ import numpy as np
25
+
26
+ if TYPE_CHECKING:
27
+ from oscura.core.types import MeasurementResult
28
+
29
+
30
+ def format_si_prefix(value: float, unit: str, precision: int = 3) -> str:
31
+ """Format value with appropriate SI prefix.
32
+
33
+ Args:
34
+ value: Numeric value to format.
35
+ unit: Base unit (e.g., "Hz", "V", "s").
36
+ precision: Number of significant figures.
37
+
38
+ Returns:
39
+ Formatted string with SI prefix (e.g., "1.234 kHz", "5.67 mV").
40
+
41
+ Example:
42
+ >>> format_si_prefix(1000, "Hz")
43
+ '1.000 kHz'
44
+ >>> format_si_prefix(0.001, "V")
45
+ '1.000 mV'
46
+ >>> format_si_prefix(1e6, "Hz", precision=2)
47
+ '1.00 MHz'
48
+ """
49
+ if value == 0 or not math.isfinite(value):
50
+ return f"{value:.{precision}g} {unit}"
51
+
52
+ # SI prefixes and their powers of 10
53
+ prefixes = [
54
+ (1e24, "Y"), # yotta
55
+ (1e21, "Z"), # zetta
56
+ (1e18, "E"), # exa
57
+ (1e15, "P"), # peta
58
+ (1e12, "T"), # tera
59
+ (1e9, "G"), # giga
60
+ (1e6, "M"), # mega
61
+ (1e3, "k"), # kilo
62
+ (1, ""), # no prefix
63
+ (1e-3, "m"), # milli
64
+ (1e-6, "µ"), # micro
65
+ (1e-9, "n"), # nano
66
+ (1e-12, "p"), # pico
67
+ (1e-15, "f"), # femto
68
+ (1e-18, "a"), # atto
69
+ (1e-21, "z"), # zepto
70
+ (1e-24, "y"), # yocto
71
+ ]
72
+
73
+ abs_value = abs(value)
74
+
75
+ # Find appropriate prefix
76
+ for scale, prefix in prefixes:
77
+ if abs_value >= scale:
78
+ scaled = value / scale
79
+ return f"{scaled:.{precision}f} {prefix}{unit}"
80
+
81
+ # Fallback for very small values
82
+ return f"{value:.{precision}e} {unit}"
83
+
84
+
85
+ def format_percentage(value: float, precision: int = 2) -> str:
86
+ """Format percentage value.
87
+
88
+ Args:
89
+ value: Percentage value (0-100).
90
+ precision: Decimal places.
91
+
92
+ Returns:
93
+ Formatted percentage string.
94
+
95
+ Example:
96
+ >>> format_percentage(5.2)
97
+ '5.20%'
98
+ >>> format_percentage(0.123, precision=3)
99
+ '0.123%'
100
+ """
101
+ return f"{value:.{precision}f}%"
102
+
103
+
104
+ def format_ratio(value: float, precision: int = 3) -> str:
105
+ """Format ratio value (0-1) as percentage.
106
+
107
+ Args:
108
+ value: Ratio value (0.0 to 1.0).
109
+ precision: Decimal places.
110
+
111
+ Returns:
112
+ Formatted percentage string.
113
+
114
+ Example:
115
+ >>> format_ratio(0.052)
116
+ '5.200%'
117
+ >>> format_ratio(0.5)
118
+ '50.000%'
119
+ """
120
+ return f"{value * 100:.{precision}f}%"
121
+
122
+
123
+ def format_decibel(value: float, precision: int = 1) -> str:
124
+ """Format decibel value.
125
+
126
+ Args:
127
+ value: Decibel value.
128
+ precision: Decimal places.
129
+
130
+ Returns:
131
+ Formatted dB string.
132
+
133
+ Example:
134
+ >>> format_decibel(42.5)
135
+ '42.5 dB'
136
+ >>> format_decibel(-3.01, precision=2)
137
+ '-3.01 dB'
138
+ """
139
+ return f"{value:.{precision}f} dB"
140
+
141
+
142
+ def make_measurement(
143
+ value: float, unit: str, *, precision: int = 3, raw_value: bool = False
144
+ ) -> MeasurementResult:
145
+ """Create an applicable MeasurementResult.
146
+
147
+ Args:
148
+ value: Measurement value.
149
+ unit: Unit string ("V", "Hz", "s", "dB", "%", "ratio", or "").
150
+ precision: Formatting precision (default: 3).
151
+ raw_value: If True, format as raw number (default: False).
152
+
153
+ Returns:
154
+ MeasurementResult with applicable=True.
155
+
156
+ Example:
157
+ >>> result = make_measurement(1000, "Hz")
158
+ >>> result["value"]
159
+ 1000.0
160
+ >>> result["display"]
161
+ '1.000 kHz'
162
+ >>> result["applicable"]
163
+ True
164
+
165
+ >>> # Percentage measurement
166
+ >>> thd = make_measurement(5.2, "%")
167
+ >>> thd["display"]
168
+ '5.200%'
169
+
170
+ >>> # Ratio measurement (auto-converts to percentage)
171
+ >>> duty = make_measurement(0.3, "ratio")
172
+ >>> duty["display"]
173
+ '30.000%'
174
+ """
175
+ # Handle NaN/Inf values
176
+ if not math.isfinite(value):
177
+ return make_inapplicable(unit, f"Invalid value: {value}")
178
+
179
+ # Format display string based on unit type
180
+ if raw_value:
181
+ display = f"{value:.{precision}g}"
182
+ elif unit == "dB":
183
+ display = format_decibel(value, precision=precision)
184
+ elif unit == "%":
185
+ display = format_percentage(value, precision=precision)
186
+ elif unit == "ratio":
187
+ # Convert ratio (0-1) to percentage for display
188
+ display = format_ratio(value, precision=precision)
189
+ elif unit == "":
190
+ # Dimensionless - format as integer or float
191
+ if isinstance(value, (int, np.integer)) or value == int(value):
192
+ display = f"{int(value)}"
193
+ else:
194
+ display = f"{value:.{precision}g}"
195
+ else:
196
+ # SI units (V, Hz, s, A, W, etc.)
197
+ display = format_si_prefix(value, unit, precision=precision)
198
+
199
+ return {
200
+ "value": float(value),
201
+ "unit": unit,
202
+ "applicable": True,
203
+ "reason": None,
204
+ "display": display,
205
+ }
206
+
207
+
208
+ def make_inapplicable(unit: str, reason: str) -> MeasurementResult:
209
+ """Create an inapplicable MeasurementResult (replaces NaN).
210
+
211
+ Args:
212
+ unit: Unit string for metadata.
213
+ reason: Human-readable explanation (e.g., "Aperiodic signal").
214
+
215
+ Returns:
216
+ MeasurementResult with applicable=False, value=None.
217
+
218
+ Example:
219
+ >>> result = make_inapplicable("s", "Aperiodic signal (single impulse)")
220
+ >>> result["value"] is None
221
+ True
222
+ >>> result["applicable"]
223
+ False
224
+ >>> result["display"]
225
+ 'N/A'
226
+ >>> result["reason"]
227
+ 'Aperiodic signal (single impulse)'
228
+ """
229
+ return {
230
+ "value": None,
231
+ "unit": unit,
232
+ "applicable": False,
233
+ "reason": reason,
234
+ "display": "N/A",
235
+ }
236
+
237
+
238
+ def make_measurement_safe(
239
+ value: float | None, unit: str, inapplicable_reason: str | None = None, **kwargs: Any
240
+ ) -> MeasurementResult:
241
+ """Create MeasurementResult with automatic NaN/None handling.
242
+
243
+ Args:
244
+ value: Measurement value (can be NaN or None).
245
+ unit: Unit string.
246
+ inapplicable_reason: Reason if inapplicable (required if value is None/NaN).
247
+ **kwargs: Additional arguments passed to make_measurement().
248
+
249
+ Returns:
250
+ MeasurementResult (applicable if value is valid, inapplicable otherwise).
251
+
252
+ Example:
253
+ >>> # Valid value
254
+ >>> result = make_measurement_safe(1000, "Hz")
255
+ >>> result["applicable"]
256
+ True
257
+
258
+ >>> # NaN value
259
+ >>> result = make_measurement_safe(float('nan'), "s", "Aperiodic signal")
260
+ >>> result["applicable"]
261
+ False
262
+ >>> result["reason"]
263
+ 'Aperiodic signal'
264
+
265
+ >>> # None value
266
+ >>> result = make_measurement_safe(None, "V", "DC signal")
267
+ >>> result["display"]
268
+ 'N/A'
269
+ """
270
+ # Check if value is invalid (None, NaN, Inf)
271
+ if value is None or not math.isfinite(value):
272
+ reason = inapplicable_reason or "Invalid or undefined value"
273
+ return make_inapplicable(unit, reason)
274
+
275
+ return make_measurement(value, unit, **kwargs)
276
+
277
+
278
+ __all__ = [
279
+ "format_decibel",
280
+ "format_percentage",
281
+ "format_ratio",
282
+ "format_si_prefix",
283
+ "make_inapplicable",
284
+ "make_measurement",
285
+ "make_measurement_safe",
286
+ ]
oscura/core/progress.py CHANGED
@@ -29,7 +29,7 @@ try:
29
29
 
30
30
  _HAS_PSUTIL = True
31
31
  except ImportError:
32
- psutil = None # type: ignore[assignment]
32
+ psutil = None
33
33
  _HAS_PSUTIL = False
34
34
 
35
35
  if TYPE_CHECKING:
@@ -149,14 +149,20 @@
149
149
  "type": "array",
150
150
  "description": "Device IDs to include (whitelist)",
151
151
  "items": {
152
- "oneOf": [{ "type": "integer" }, { "type": "string", "pattern": "^0[xX][0-9A-Fa-f]+$" }]
152
+ "oneOf": [
153
+ { "type": "integer" },
154
+ { "type": "string", "pattern": "^0[xX][0-9A-Fa-f]+$" }
155
+ ]
153
156
  }
154
157
  },
155
158
  "exclude_devices": {
156
159
  "type": "array",
157
160
  "description": "Device IDs to exclude (blacklist)",
158
161
  "items": {
159
- "oneOf": [{ "type": "integer" }, { "type": "string", "pattern": "^0[xX][0-9A-Fa-f]+$" }]
162
+ "oneOf": [
163
+ { "type": "integer" },
164
+ { "type": "string", "pattern": "^0[xX][0-9A-Fa-f]+$" }
165
+ ]
160
166
  }
161
167
  },
162
168
  "include_categories": {
@@ -118,7 +118,10 @@
118
118
  },
119
119
  "value": {
120
120
  "description": "Expected constant value for validation",
121
- "oneOf": [{ "type": "integer" }, { "type": "array", "items": { "type": "integer" } }]
121
+ "oneOf": [
122
+ { "type": "integer" },
123
+ { "type": "array", "items": { "type": "integer" } }
124
+ ]
122
125
  },
123
126
  "description": {
124
127
  "type": "string",
@@ -185,7 +188,18 @@
185
188
  },
186
189
  "type": {
187
190
  "type": "string",
188
- "enum": ["uint8", "uint16", "uint32", "uint64", "int8", "int16", "int32", "int64", "float32", "float64"],
191
+ "enum": [
192
+ "uint8",
193
+ "uint16",
194
+ "uint32",
195
+ "uint64",
196
+ "int8",
197
+ "int16",
198
+ "int32",
199
+ "int64",
200
+ "float32",
201
+ "float64"
202
+ ],
189
203
  "description": "Sample data type"
190
204
  },
191
205
  "endian": {
@@ -289,7 +303,10 @@
289
303
  },
290
304
  "expected": {
291
305
  "description": "Expected value",
292
- "oneOf": [{ "type": "integer" }, { "type": "array", "items": { "type": "integer" } }]
306
+ "oneOf": [
307
+ { "type": "integer" },
308
+ { "type": "array", "items": { "type": "integer" } }
309
+ ]
293
310
  },
294
311
  "on_failure": {
295
312
  "type": "string",
@@ -362,7 +379,10 @@
362
379
  },
363
380
  "pattern": {
364
381
  "description": "Idle pattern to detect",
365
- "oneOf": [{ "type": "string", "enum": ["auto", "zeros", "ones"] }, { "type": "integer" }]
382
+ "oneOf": [
383
+ { "type": "string", "enum": ["auto", "zeros", "ones"] },
384
+ { "type": "integer" }
385
+ ]
366
386
  },
367
387
  "min_duration": {
368
388
  "type": "integer",
@@ -241,7 +241,12 @@
241
241
  },
242
242
  "value": {
243
243
  "description": "Expected constant value for validation",
244
- "oneOf": [{ "type": "integer" }, { "type": "number" }, { "type": "string" }, { "type": "array" }]
244
+ "oneOf": [
245
+ { "type": "integer" },
246
+ { "type": "number" },
247
+ { "type": "string" },
248
+ { "type": "array" }
249
+ ]
245
250
  },
246
251
  "condition": {
247
252
  "type": "string",
@@ -326,7 +331,12 @@
326
331
  },
327
332
  "expected": {
328
333
  "description": "Expected value",
329
- "oneOf": [{ "type": "integer" }, { "type": "number" }, { "type": "string" }, { "type": "array" }]
334
+ "oneOf": [
335
+ { "type": "integer" },
336
+ { "type": "number" },
337
+ { "type": "string" },
338
+ { "type": "array" }
339
+ ]
330
340
  },
331
341
  "on_mismatch": {
332
342
  "type": "string",