jxcel 0.1.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.
- jxcel/__init__.py +3 -0
- jxcel/cli.py +420 -0
- jxcel/discovery.py +130 -0
- jxcel/jsonio.py +168 -0
- jxcel/planning.py +848 -0
- jxcel/reporting.py +357 -0
- jxcel/workbook.py +928 -0
- jxcel-0.1.0.dist-info/METADATA +263 -0
- jxcel-0.1.0.dist-info/RECORD +13 -0
- jxcel-0.1.0.dist-info/WHEEL +5 -0
- jxcel-0.1.0.dist-info/entry_points.txt +2 -0
- jxcel-0.1.0.dist-info/licenses/LICENSE +21 -0
- jxcel-0.1.0.dist-info/top_level.txt +1 -0
jxcel/__init__.py
ADDED
jxcel/cli.py
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
"""Command-line interface and pipeline orchestration for jxcel."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Sequence
|
|
10
|
+
|
|
11
|
+
from . import __version__
|
|
12
|
+
from .discovery import DatasetCandidate, discover
|
|
13
|
+
from .jsonio import JsonLoadError, load_json
|
|
14
|
+
from .planning import (
|
|
15
|
+
DatasetPlan,
|
|
16
|
+
PlanningError,
|
|
17
|
+
PreparedValue,
|
|
18
|
+
build_plan,
|
|
19
|
+
preview_rows,
|
|
20
|
+
)
|
|
21
|
+
from .reporting import (
|
|
22
|
+
render_export,
|
|
23
|
+
render_listing,
|
|
24
|
+
render_plan,
|
|
25
|
+
render_preview,
|
|
26
|
+
)
|
|
27
|
+
from .workbook import (
|
|
28
|
+
WorkbookError,
|
|
29
|
+
assign_sheet_names,
|
|
30
|
+
export_workbook,
|
|
31
|
+
validate_output_destination,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _positive_integer(value: str) -> int:
|
|
36
|
+
try:
|
|
37
|
+
number = int(value)
|
|
38
|
+
except ValueError as exc:
|
|
39
|
+
raise argparse.ArgumentTypeError(
|
|
40
|
+
f"invalid integer value: {value!r}"
|
|
41
|
+
) from exc
|
|
42
|
+
if number < 1:
|
|
43
|
+
raise argparse.ArgumentTypeError("must be a positive integer")
|
|
44
|
+
return number
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
48
|
+
parser = argparse.ArgumentParser(
|
|
49
|
+
prog="jxcel",
|
|
50
|
+
description=(
|
|
51
|
+
"Convert JSON datasets into a formatted Excel .xlsx workbook."
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
parser.add_argument(
|
|
55
|
+
"input",
|
|
56
|
+
nargs="?",
|
|
57
|
+
metavar="INPUT",
|
|
58
|
+
help="input JSON file (mutually exclusive with -i/--input)",
|
|
59
|
+
)
|
|
60
|
+
parser.add_argument(
|
|
61
|
+
"-i",
|
|
62
|
+
"--input",
|
|
63
|
+
dest="input_option",
|
|
64
|
+
metavar="PATH",
|
|
65
|
+
help="input JSON file (mutually exclusive with positional INPUT)",
|
|
66
|
+
)
|
|
67
|
+
parser.add_argument(
|
|
68
|
+
"-o",
|
|
69
|
+
"--output",
|
|
70
|
+
metavar="PATH",
|
|
71
|
+
help=(
|
|
72
|
+
"output .xlsx path; defaults to the input name with .xlsx "
|
|
73
|
+
"beside the input"
|
|
74
|
+
),
|
|
75
|
+
)
|
|
76
|
+
parser.add_argument(
|
|
77
|
+
"--overwrite",
|
|
78
|
+
action="store_true",
|
|
79
|
+
help="replace an existing output file after validation",
|
|
80
|
+
)
|
|
81
|
+
parser.add_argument(
|
|
82
|
+
"-d",
|
|
83
|
+
"--dataset",
|
|
84
|
+
action="append",
|
|
85
|
+
default=[],
|
|
86
|
+
metavar="JSON_POINTER",
|
|
87
|
+
help=(
|
|
88
|
+
"select a dataset by JSON Pointer; repeat to select more "
|
|
89
|
+
"than one (default: all discovered datasets)"
|
|
90
|
+
),
|
|
91
|
+
)
|
|
92
|
+
parser.add_argument(
|
|
93
|
+
"--list",
|
|
94
|
+
dest="list_mode",
|
|
95
|
+
action="store_true",
|
|
96
|
+
help="list datasets and unsupported structures without writing",
|
|
97
|
+
)
|
|
98
|
+
parser.add_argument(
|
|
99
|
+
"--preview",
|
|
100
|
+
action="store_true",
|
|
101
|
+
help="show bounded dataset records without writing",
|
|
102
|
+
)
|
|
103
|
+
parser.add_argument(
|
|
104
|
+
"--preview-rows",
|
|
105
|
+
type=_positive_integer,
|
|
106
|
+
default=None,
|
|
107
|
+
metavar="N",
|
|
108
|
+
help="number of preview rows per dataset (default: 5)",
|
|
109
|
+
)
|
|
110
|
+
parser.add_argument(
|
|
111
|
+
"--dry-run",
|
|
112
|
+
action="store_true",
|
|
113
|
+
help="validate and show the complete plan without writing",
|
|
114
|
+
)
|
|
115
|
+
parser.add_argument(
|
|
116
|
+
"--column-map",
|
|
117
|
+
action="append",
|
|
118
|
+
default=[],
|
|
119
|
+
metavar="JSON_PAIR",
|
|
120
|
+
help=(
|
|
121
|
+
"include an exact column using "
|
|
122
|
+
'\'["/dataset/pointer","column"]\'; repeatable'
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
parser.add_argument(
|
|
126
|
+
"--exclude-map",
|
|
127
|
+
action="append",
|
|
128
|
+
default=[],
|
|
129
|
+
metavar="JSON_PAIR",
|
|
130
|
+
help=(
|
|
131
|
+
"exclude an exact column using "
|
|
132
|
+
'\'["/dataset/pointer","column"]\'; repeatable'
|
|
133
|
+
),
|
|
134
|
+
)
|
|
135
|
+
parser.add_argument(
|
|
136
|
+
"--no-flatten",
|
|
137
|
+
action="store_true",
|
|
138
|
+
help=(
|
|
139
|
+
"use one column per top-level property and keep nested "
|
|
140
|
+
"objects as compact JSON text"
|
|
141
|
+
),
|
|
142
|
+
)
|
|
143
|
+
verbosity = parser.add_mutually_exclusive_group()
|
|
144
|
+
verbosity.add_argument(
|
|
145
|
+
"-v",
|
|
146
|
+
"--verbose",
|
|
147
|
+
action="store_true",
|
|
148
|
+
help="add diagnostic detail to reports",
|
|
149
|
+
)
|
|
150
|
+
verbosity.add_argument(
|
|
151
|
+
"-q",
|
|
152
|
+
"--quiet",
|
|
153
|
+
action="store_true",
|
|
154
|
+
help=(
|
|
155
|
+
"suppress the routine success summary; warnings and "
|
|
156
|
+
"findings remain visible"
|
|
157
|
+
),
|
|
158
|
+
)
|
|
159
|
+
parser.add_argument(
|
|
160
|
+
"--version",
|
|
161
|
+
action="version",
|
|
162
|
+
version=f"%(prog)s {__version__}",
|
|
163
|
+
)
|
|
164
|
+
return parser
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _validate_arguments(
|
|
168
|
+
parser: argparse.ArgumentParser,
|
|
169
|
+
args: argparse.Namespace,
|
|
170
|
+
) -> None:
|
|
171
|
+
if args.input is not None and args.input_option is not None:
|
|
172
|
+
parser.error(
|
|
173
|
+
"provide the input either positionally or with -i/--input, "
|
|
174
|
+
"not both"
|
|
175
|
+
)
|
|
176
|
+
if args.input is None and args.input_option is None:
|
|
177
|
+
parser.error("an input JSON file is required")
|
|
178
|
+
|
|
179
|
+
if args.list_mode and (args.preview or args.dry_run):
|
|
180
|
+
parser.error(
|
|
181
|
+
"--list cannot be combined with --preview or --dry-run"
|
|
182
|
+
)
|
|
183
|
+
if args.preview_rows is not None and not args.preview:
|
|
184
|
+
parser.error("--preview-rows requires --preview")
|
|
185
|
+
|
|
186
|
+
preview_only = args.preview and not args.dry_run
|
|
187
|
+
if args.list_mode or preview_only:
|
|
188
|
+
if args.output is not None:
|
|
189
|
+
parser.error(
|
|
190
|
+
"--output is not valid with --list or preview-only mode"
|
|
191
|
+
)
|
|
192
|
+
if args.overwrite:
|
|
193
|
+
parser.error(
|
|
194
|
+
"--overwrite is not valid with --list or preview-only mode"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
if args.preview and args.preview_rows is None:
|
|
198
|
+
args.preview_rows = 5
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _emit(text: str) -> None:
|
|
202
|
+
if text:
|
|
203
|
+
print(text)
|
|
204
|
+
sys.stdout.flush()
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _fail(message: str) -> None:
|
|
208
|
+
try:
|
|
209
|
+
print(f"jxcel: error: {message}", file=sys.stderr)
|
|
210
|
+
sys.stderr.flush()
|
|
211
|
+
except BrokenPipeError:
|
|
212
|
+
_redirect_console_after_broken_pipe()
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _configure_console_encoding() -> None:
|
|
216
|
+
"""Escape characters the active console encoding cannot represent."""
|
|
217
|
+
for stream in (sys.stdout, sys.stderr):
|
|
218
|
+
reconfigure = getattr(stream, "reconfigure", None)
|
|
219
|
+
if reconfigure is None:
|
|
220
|
+
continue
|
|
221
|
+
try:
|
|
222
|
+
reconfigure(errors="backslashreplace")
|
|
223
|
+
except (OSError, ValueError):
|
|
224
|
+
# Some wrapped or already-closed streams cannot be reconfigured.
|
|
225
|
+
# Their normal write errors are handled at the CLI boundary.
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _redirect_console_after_broken_pipe() -> None:
|
|
230
|
+
"""Prevent buffered console streams from raising again during shutdown."""
|
|
231
|
+
for stream in (sys.stdout, sys.stderr):
|
|
232
|
+
try:
|
|
233
|
+
descriptor = stream.fileno()
|
|
234
|
+
except (AttributeError, OSError, ValueError):
|
|
235
|
+
continue
|
|
236
|
+
try:
|
|
237
|
+
null_descriptor = os.open(os.devnull, os.O_WRONLY)
|
|
238
|
+
try:
|
|
239
|
+
os.dup2(null_descriptor, descriptor)
|
|
240
|
+
finally:
|
|
241
|
+
os.close(null_descriptor)
|
|
242
|
+
except OSError:
|
|
243
|
+
continue
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _exportable_plans(
|
|
247
|
+
plans: Sequence[DatasetPlan],
|
|
248
|
+
) -> tuple[DatasetPlan, ...]:
|
|
249
|
+
return tuple(
|
|
250
|
+
plan
|
|
251
|
+
for plan in plans
|
|
252
|
+
if plan.selected and plan.exportable
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _preview_rows_by_pointer(
|
|
257
|
+
plans: Sequence[DatasetPlan],
|
|
258
|
+
limit: int,
|
|
259
|
+
) -> dict[str, tuple[tuple[PreparedValue, ...], ...]]:
|
|
260
|
+
return {
|
|
261
|
+
plan.pointer: tuple(preview_rows(plan, limit))
|
|
262
|
+
for plan in plans
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
267
|
+
"""Run the jxcel command and return its exit status."""
|
|
268
|
+
_configure_console_encoding()
|
|
269
|
+
parser = _build_parser()
|
|
270
|
+
try:
|
|
271
|
+
args = parser.parse_args(argv)
|
|
272
|
+
_validate_arguments(parser, args)
|
|
273
|
+
except BrokenPipeError:
|
|
274
|
+
_redirect_console_after_broken_pipe()
|
|
275
|
+
return 1
|
|
276
|
+
|
|
277
|
+
input_path = Path(
|
|
278
|
+
args.input if args.input is not None else args.input_option
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
document = load_json(input_path)
|
|
283
|
+
findings = discover(document)
|
|
284
|
+
candidates = tuple(
|
|
285
|
+
finding
|
|
286
|
+
for finding in findings
|
|
287
|
+
if isinstance(finding, DatasetCandidate)
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
schema_only = args.list_mode or (
|
|
291
|
+
args.preview and not args.dry_run
|
|
292
|
+
)
|
|
293
|
+
result = build_plan(
|
|
294
|
+
candidates,
|
|
295
|
+
selected_pointers=args.dataset or None,
|
|
296
|
+
include_maps=args.column_map,
|
|
297
|
+
exclude_maps=args.exclude_map,
|
|
298
|
+
flatten=not args.no_flatten,
|
|
299
|
+
validation_mode="schema" if schema_only else "export",
|
|
300
|
+
)
|
|
301
|
+
plans = result.datasets
|
|
302
|
+
exportable = _exportable_plans(plans)
|
|
303
|
+
|
|
304
|
+
if args.list_mode:
|
|
305
|
+
_emit(
|
|
306
|
+
render_listing(
|
|
307
|
+
plans,
|
|
308
|
+
findings,
|
|
309
|
+
input_path=input_path,
|
|
310
|
+
verbose=args.verbose,
|
|
311
|
+
)
|
|
312
|
+
)
|
|
313
|
+
return 0
|
|
314
|
+
|
|
315
|
+
if args.preview and not args.dry_run:
|
|
316
|
+
rows = _preview_rows_by_pointer(
|
|
317
|
+
exportable,
|
|
318
|
+
args.preview_rows,
|
|
319
|
+
)
|
|
320
|
+
_emit(
|
|
321
|
+
render_preview(
|
|
322
|
+
plans,
|
|
323
|
+
rows,
|
|
324
|
+
findings,
|
|
325
|
+
input_path=input_path,
|
|
326
|
+
verbose=args.verbose,
|
|
327
|
+
)
|
|
328
|
+
)
|
|
329
|
+
return 0
|
|
330
|
+
|
|
331
|
+
if args.output is not None:
|
|
332
|
+
output_path = Path(args.output)
|
|
333
|
+
else:
|
|
334
|
+
output_path = input_path.with_suffix(".xlsx")
|
|
335
|
+
|
|
336
|
+
if args.dry_run:
|
|
337
|
+
if not exportable:
|
|
338
|
+
_emit(
|
|
339
|
+
render_listing(
|
|
340
|
+
plans,
|
|
341
|
+
findings,
|
|
342
|
+
input_path=input_path,
|
|
343
|
+
verbose=args.verbose,
|
|
344
|
+
)
|
|
345
|
+
)
|
|
346
|
+
_fail("No selected exportable datasets to write")
|
|
347
|
+
return 1
|
|
348
|
+
validate_output_destination(
|
|
349
|
+
output_path,
|
|
350
|
+
overwrite=args.overwrite,
|
|
351
|
+
input_path=input_path,
|
|
352
|
+
)
|
|
353
|
+
sheet_names = None
|
|
354
|
+
if args.verbose:
|
|
355
|
+
sheet_names = {
|
|
356
|
+
assignment.pointer: assignment.name
|
|
357
|
+
for assignment in assign_sheet_names(exportable)
|
|
358
|
+
}
|
|
359
|
+
sections = [
|
|
360
|
+
render_plan(
|
|
361
|
+
plans,
|
|
362
|
+
findings,
|
|
363
|
+
output_path,
|
|
364
|
+
overwrite=args.overwrite,
|
|
365
|
+
mapping_outcomes=result.mapping_outcomes,
|
|
366
|
+
sheet_names=sheet_names,
|
|
367
|
+
input_path=input_path,
|
|
368
|
+
verbose=args.verbose,
|
|
369
|
+
)
|
|
370
|
+
]
|
|
371
|
+
if args.preview:
|
|
372
|
+
rows = _preview_rows_by_pointer(
|
|
373
|
+
exportable,
|
|
374
|
+
args.preview_rows,
|
|
375
|
+
)
|
|
376
|
+
sections.append(
|
|
377
|
+
render_preview(plans, rows, ())
|
|
378
|
+
)
|
|
379
|
+
_emit("\n\n".join(sections))
|
|
380
|
+
return 0
|
|
381
|
+
|
|
382
|
+
if not exportable:
|
|
383
|
+
_emit(
|
|
384
|
+
render_listing(
|
|
385
|
+
plans,
|
|
386
|
+
findings,
|
|
387
|
+
input_path=input_path,
|
|
388
|
+
verbose=args.verbose,
|
|
389
|
+
)
|
|
390
|
+
)
|
|
391
|
+
_fail("No selected exportable datasets to write")
|
|
392
|
+
return 1
|
|
393
|
+
|
|
394
|
+
export = export_workbook(
|
|
395
|
+
plans,
|
|
396
|
+
output_path,
|
|
397
|
+
overwrite=args.overwrite,
|
|
398
|
+
input_path=input_path,
|
|
399
|
+
)
|
|
400
|
+
_emit(
|
|
401
|
+
render_export(
|
|
402
|
+
export,
|
|
403
|
+
plans,
|
|
404
|
+
findings,
|
|
405
|
+
input_path=input_path,
|
|
406
|
+
quiet=args.quiet,
|
|
407
|
+
verbose=args.verbose,
|
|
408
|
+
)
|
|
409
|
+
)
|
|
410
|
+
return 0
|
|
411
|
+
except (JsonLoadError, PlanningError, WorkbookError) as exc:
|
|
412
|
+
_fail(str(exc))
|
|
413
|
+
return 1
|
|
414
|
+
except BrokenPipeError:
|
|
415
|
+
_redirect_console_after_broken_pipe()
|
|
416
|
+
return 1
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
if __name__ == "__main__":
|
|
420
|
+
raise SystemExit(main())
|
jxcel/discovery.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Deterministic dataset discovery for parsed JSON documents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Literal
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
StructureKind = Literal[
|
|
10
|
+
"empty_array",
|
|
11
|
+
"mixed_array",
|
|
12
|
+
"nested_array",
|
|
13
|
+
"primitive_array",
|
|
14
|
+
"unsupported_root",
|
|
15
|
+
"scalar_value",
|
|
16
|
+
"empty_object",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class DatasetCandidate:
|
|
22
|
+
"""An array whose direct elements are all JSON objects."""
|
|
23
|
+
|
|
24
|
+
pointer: str
|
|
25
|
+
records: tuple[dict[str, Any], ...]
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def kind(self) -> Literal["dataset"]:
|
|
29
|
+
return "dataset"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class StructureFinding:
|
|
34
|
+
"""An array or root value that cannot be treated as a dataset."""
|
|
35
|
+
|
|
36
|
+
pointer: str
|
|
37
|
+
kind: StructureKind
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
Finding = DatasetCandidate | StructureFinding
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _escape_pointer_token(token: str) -> str:
|
|
44
|
+
"""Escape one JSON Pointer reference token."""
|
|
45
|
+
return token.replace("~", "~0").replace("/", "~1")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _object_child_pointer(pointer: str, key: str) -> str:
|
|
49
|
+
return f"{pointer}/{_escape_pointer_token(key)}"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _array_child_pointer(pointer: str, index: int) -> str:
|
|
53
|
+
return f"{pointer}/{index}"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _classify_array(
|
|
57
|
+
values: list[Any],
|
|
58
|
+
) -> StructureKind | Literal["dataset"]:
|
|
59
|
+
"""Classify one array using the approved precedence exactly."""
|
|
60
|
+
if not values:
|
|
61
|
+
return "empty_array"
|
|
62
|
+
if all(isinstance(item, dict) for item in values):
|
|
63
|
+
return "dataset"
|
|
64
|
+
if any(isinstance(item, dict) for item in values):
|
|
65
|
+
return "mixed_array"
|
|
66
|
+
if any(isinstance(item, list) for item in values):
|
|
67
|
+
return "nested_array"
|
|
68
|
+
return "primitive_array"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def discover(document: object) -> tuple[Finding, ...]:
|
|
72
|
+
"""Return deterministic discovery findings for a parsed JSON document.
|
|
73
|
+
|
|
74
|
+
Traversal is depth-first and pre-order. Object values are visited in source
|
|
75
|
+
key order and array elements by ascending index. Dataset arrays are recorded
|
|
76
|
+
with their original record dictionaries and are not traversed further.
|
|
77
|
+
"""
|
|
78
|
+
if not isinstance(document, (dict, list)):
|
|
79
|
+
return (StructureFinding(pointer="", kind="unsupported_root"),)
|
|
80
|
+
|
|
81
|
+
findings: list[Finding] = []
|
|
82
|
+
# The boolean tracks whether an ancestor unsupported-array finding already
|
|
83
|
+
# accounts for values in this branch. Continue traversing those branches to
|
|
84
|
+
# find independent arrays/datasets, but avoid redundant scalar/object
|
|
85
|
+
# findings beneath the array finding.
|
|
86
|
+
stack: list[tuple[object, str, bool]] = [(document, "", False)]
|
|
87
|
+
|
|
88
|
+
while stack:
|
|
89
|
+
value, pointer, covered_by_array_finding = stack.pop()
|
|
90
|
+
|
|
91
|
+
if isinstance(value, dict):
|
|
92
|
+
if not value:
|
|
93
|
+
if not covered_by_array_finding:
|
|
94
|
+
findings.append(
|
|
95
|
+
StructureFinding(pointer=pointer, kind="empty_object")
|
|
96
|
+
)
|
|
97
|
+
continue
|
|
98
|
+
for key, child in reversed(list(value.items())):
|
|
99
|
+
stack.append(
|
|
100
|
+
(
|
|
101
|
+
child,
|
|
102
|
+
_object_child_pointer(pointer, key),
|
|
103
|
+
covered_by_array_finding,
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
if not isinstance(value, list):
|
|
109
|
+
if not covered_by_array_finding:
|
|
110
|
+
findings.append(
|
|
111
|
+
StructureFinding(pointer=pointer, kind="scalar_value")
|
|
112
|
+
)
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
kind = _classify_array(value)
|
|
116
|
+
if kind == "dataset":
|
|
117
|
+
findings.append(DatasetCandidate(pointer=pointer, records=tuple(value)))
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
findings.append(StructureFinding(pointer=pointer, kind=kind))
|
|
121
|
+
for index in range(len(value) - 1, -1, -1):
|
|
122
|
+
stack.append(
|
|
123
|
+
(
|
|
124
|
+
value[index],
|
|
125
|
+
_array_child_pointer(pointer, index),
|
|
126
|
+
True,
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
return tuple(findings)
|