equiframe 0.12.4__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.
- equiframe/__init__.py +832 -0
- equiframe/__main__.py +3 -0
- equiframe/_base.py +84 -0
- equiframe/_cli_support.py +125 -0
- equiframe/_comparison_io.py +161 -0
- equiframe/_dataset_provenance.py +146 -0
- equiframe/_decimal.py +22 -0
- equiframe/_diff_capture.py +433 -0
- equiframe/_doctor.py +123 -0
- equiframe/_document_io.py +61 -0
- equiframe/_documents.py +79 -0
- equiframe/_duckdb_transport.py +149 -0
- equiframe/_error_reporting.py +52 -0
- equiframe/_first_use.py +102 -0
- equiframe/_path_compat.py +26 -0
- equiframe/_physical.py +133 -0
- equiframe/_r_review.R +308 -0
- equiframe/_regular_io.py +167 -0
- equiframe/_report_format_v1.py +298 -0
- equiframe/_report_io.py +591 -0
- equiframe/_stability_inventory.py +1421 -0
- equiframe/_timezones.py +257 -0
- equiframe/_vendor/cyclonedx/LICENSE +201 -0
- equiframe/_vendor/cyclonedx/PROVENANCE.md +18 -0
- equiframe/_vendor/cyclonedx/bom-1.6.schema.json +5673 -0
- equiframe/_vendor/cyclonedx/jsf-0.82.schema.json +240 -0
- equiframe/_vendor/cyclonedx/spdx.schema.json +712 -0
- equiframe/adapters.py +454 -0
- equiframe/assertion_types.py +122 -0
- equiframe/assertions.py +271 -0
- equiframe/benchmark.py +101 -0
- equiframe/bundle.py +240 -0
- equiframe/cli.py +693 -0
- equiframe/comparison.py +937 -0
- equiframe/comparison_report.py +660 -0
- equiframe/comparison_types.py +257 -0
- equiframe/compatibility.py +546 -0
- equiframe/contracts.py +879 -0
- equiframe/datasets.py +1301 -0
- equiframe/decision.py +34 -0
- equiframe/destinations.py +295 -0
- equiframe/detector_types.py +431 -0
- equiframe/detectors.py +389 -0
- equiframe/diff.py +192 -0
- equiframe/diff_contracts.py +523 -0
- equiframe/docs/adoption.md +329 -0
- equiframe/docs/api.md +534 -0
- equiframe/docs/arquitecto.md +143 -0
- equiframe/docs/choosing-api.md +84 -0
- equiframe/docs/cientifico.md +180 -0
- equiframe/docs/compatibility.md +254 -0
- equiframe/docs/contributing.md +171 -0
- equiframe/docs/dependencies.md +237 -0
- equiframe/docs/eda.md +166 -0
- equiframe/docs/extending.md +158 -0
- equiframe/docs/fast-ci.md +52 -0
- equiframe/docs/first-use.md +78 -0
- equiframe/docs/index.md +142 -0
- equiframe/docs/ingestion.md +145 -0
- equiframe/docs/inicio.md +129 -0
- equiframe/docs/installed-private.md +97 -0
- equiframe/docs/journeys.md +362 -0
- equiframe/docs/lectura-conversion.md +211 -0
- equiframe/docs/licensing.md +17 -0
- equiframe/docs/maintenance.md +233 -0
- equiframe/docs/migration-ir.md +333 -0
- equiframe/docs/option-audit.md +45 -0
- equiframe/docs/outcome-gates.md +134 -0
- equiframe/docs/performance.md +263 -0
- equiframe/docs/piloto-simulado.md +56 -0
- equiframe/docs/quickstart.md +156 -0
- equiframe/docs/r_review.md +212 -0
- equiframe/docs/recipes.md +248 -0
- equiframe/docs/releasing.md +183 -0
- equiframe/docs/security.md +218 -0
- equiframe/docs/semantics.md +274 -0
- equiframe/docs/sqlite-snapshots.md +262 -0
- equiframe/docs/universal-sources.md +49 -0
- equiframe/docs/universalidad-cinco-pasos.md +107 -0
- equiframe/docs/upgrading.md +538 -0
- equiframe/duckdb_capture.py +410 -0
- equiframe/eda/__init__.py +36 -0
- equiframe/eda/__main__.py +3 -0
- equiframe/eda/_backend.py +612 -0
- equiframe/eda/_inventory.py +49 -0
- equiframe/eda/_paths.py +49 -0
- equiframe/eda/_report.py +291 -0
- equiframe/eda/api.py +535 -0
- equiframe/eda/cli.py +102 -0
- equiframe/eda/models.py +239 -0
- equiframe/errors.py +83 -0
- equiframe/evidence.py +375 -0
- equiframe/examples/integrations/eda_generic.py +60 -0
- equiframe/examples/integrations/outcome_gates.py +94 -0
- equiframe/examples/integrations/sqlite_snapshot.py +246 -0
- equiframe/examples/integrations/sqlite_snapshot_demo.py +226 -0
- equiframe/examples/integrations/universal_first_use.py +296 -0
- equiframe/examples/journeys/README.md +31 -0
- equiframe/examples/journeys/__init__.py +1 -0
- equiframe/examples/journeys/a_pandas_refactor.py +88 -0
- equiframe/examples/journeys/b_native_migration.py +181 -0
- equiframe/examples/journeys/c_retail_etl.py +460 -0
- equiframe/examples/journeys/common.py +136 -0
- equiframe/examples/journeys/d_financial_reconciliation.py +96 -0
- equiframe/examples/journeys/e_telemetry.py +135 -0
- equiframe/examples/journeys/f_data_science_preparation.py +84 -0
- equiframe/examples/journeys/g_external_snapshots.py +197 -0
- equiframe/examples/journeys/h_portable_ci.py +129 -0
- equiframe/examples/journeys/requirements.lock +249 -0
- equiframe/examples/journeys/verify_journeys.py +117 -0
- equiframe/examples/load_convert_compare.py +85 -0
- equiframe/execution.py +355 -0
- equiframe/execution_types.py +177 -0
- equiframe/ingestion.py +744 -0
- equiframe/local.py +126 -0
- equiframe/migration/__init__.py +94 -0
- equiframe/migration/_physical.py +8 -0
- equiframe/migration/backends/_timestamp_transport.py +109 -0
- equiframe/migration/backends/duckdb.py +651 -0
- equiframe/migration/backends/duckdb_decimal.py +125 -0
- equiframe/migration/backends/duckdb_time.py +87 -0
- equiframe/migration/backends/pandas.py +544 -0
- equiframe/migration/backends/polars.py +915 -0
- equiframe/migration/backends/polars_decimal.py +231 -0
- equiframe/migration/io.py +198 -0
- equiframe/migration/ports.py +96 -0
- equiframe/migration/runner.py +545 -0
- equiframe/migration/runner_types.py +405 -0
- equiframe/migration/types.py +354 -0
- equiframe/migration/validation.py +619 -0
- equiframe/normalization.py +358 -0
- equiframe/pipeline.py +274 -0
- equiframe/pipeline_types.py +247 -0
- equiframe/ports.py +39 -0
- equiframe/preprocessing.py +110 -0
- equiframe/profile_types.py +343 -0
- equiframe/profiles.py +447 -0
- equiframe/py.typed +0 -0
- equiframe/r_review.py +706 -0
- equiframe/reduction.py +518 -0
- equiframe/registry.py +121 -0
- equiframe/report_rendering.py +366 -0
- equiframe/report_types.py +294 -0
- equiframe/reporting.py +26 -0
- equiframe/rule_types.py +178 -0
- equiframe/rules.py +819 -0
- equiframe/schemas/r-execution-binding-1.0.schema.json +46 -0
- equiframe/schemas/r-review-binding-1.0.schema.json +28 -0
- equiframe/schemas/r-review-options-1.0.schema.json +38 -0
- equiframe/schemas/r-review-result-1.0.schema.json +207 -0
- equiframe/schemas/reviewed-decision-1.0.schema.json +3394 -0
- equiframe/serialization.py +152 -0
- equiframe/snapshot.py +296 -0
- equiframe/sources.py +283 -0
- equiframe/tables.py +459 -0
- equiframe/types.py +71 -0
- equiframe/values.py +252 -0
- equiframe/verification_request.py +35 -0
- equiframe/verify.py +799 -0
- equiframe/verify_types.py +255 -0
- equiframe/workflow.py +510 -0
- equiframe/workflow_request.py +38 -0
- equiframe-0.12.4.dist-info/METADATA +268 -0
- equiframe-0.12.4.dist-info/RECORD +170 -0
- equiframe-0.12.4.dist-info/WHEEL +4 -0
- equiframe-0.12.4.dist-info/entry_points.txt +3 -0
- equiframe-0.12.4.dist-info/licenses/LICENSE +21 -0
- equiframe-0.12.4.dist-info/licenses/THIRD_PARTY_NOTICES.md +42 -0
- equiframe-0.12.4.dist-info/licenses/tools/cyclonedx/LICENSE +201 -0
- equiframe-0.12.4.dist-info/licenses/tools/cyclonedx/PROVENANCE.md +18 -0
equiframe/_r_review.R
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# Independent exact comparison. Only base R is loaded by the Python launcher.
|
|
2
|
+
# No Python verdict, detector score, expected label or normalized row is read.
|
|
3
|
+
|
|
4
|
+
main <- function(directory) {
|
|
5
|
+
options(warn = 2)
|
|
6
|
+
Sys.setlocale("LC_COLLATE", "C")
|
|
7
|
+
split <- function(line) strsplit(line, "\t", fixed = TRUE)[[1L]]
|
|
8
|
+
expect <- function(condition) {
|
|
9
|
+
if (!isTRUE(condition)) stop("invalid review transport", call. = FALSE)
|
|
10
|
+
}
|
|
11
|
+
integer_text <- function(value) {
|
|
12
|
+
expect(grepl("^-?[0-9]+$", value))
|
|
13
|
+
negative <- startsWith(value, "-")
|
|
14
|
+
digits <- sub("^-", "", value)
|
|
15
|
+
digits <- sub("^0+", "", digits)
|
|
16
|
+
if (!nzchar(digits)) return("0")
|
|
17
|
+
paste0(if (negative) "-" else "", digits)
|
|
18
|
+
}
|
|
19
|
+
# Arbitrary precision integer comparison uses decimal digits, never double.
|
|
20
|
+
int_compare_canonical <- function(left, right) {
|
|
21
|
+
a_negative <- startsWith(left, "-")
|
|
22
|
+
b_negative <- startsWith(right, "-")
|
|
23
|
+
if (a_negative != b_negative) return(if (a_negative) -1L else 1L)
|
|
24
|
+
a <- sub("^-", "", left)
|
|
25
|
+
b <- sub("^-", "", right)
|
|
26
|
+
result <- if (nchar(a) != nchar(b)) sign(nchar(a) - nchar(b)) else {
|
|
27
|
+
if (a == b) 0L else if (a < b) -1L else 1L
|
|
28
|
+
}
|
|
29
|
+
if (a_negative) -result else result
|
|
30
|
+
}
|
|
31
|
+
int_compare <- function(left, right) {
|
|
32
|
+
int_compare_canonical(integer_text(left), integer_text(right))
|
|
33
|
+
}
|
|
34
|
+
twice <- function(value) {
|
|
35
|
+
digits <- as.integer(strsplit(value, "", fixed = TRUE)[[1L]])
|
|
36
|
+
carry <- 0L
|
|
37
|
+
for (i in rev(seq_along(digits))) {
|
|
38
|
+
number <- 2L * digits[i] + carry
|
|
39
|
+
digits[i] <- number %% 10L
|
|
40
|
+
carry <- number %/% 10L
|
|
41
|
+
}
|
|
42
|
+
paste0(if (carry) as.character(carry) else "", paste0(digits, collapse = ""))
|
|
43
|
+
}
|
|
44
|
+
subtract_one <- function(value) {
|
|
45
|
+
digits <- as.integer(strsplit(value, "", fixed = TRUE)[[1L]])
|
|
46
|
+
for (i in rev(seq_along(digits))) {
|
|
47
|
+
if (digits[i] > 0L) {
|
|
48
|
+
digits[i] <- digits[i] - 1L
|
|
49
|
+
break
|
|
50
|
+
}
|
|
51
|
+
digits[i] <- 9L
|
|
52
|
+
}
|
|
53
|
+
integer_text(paste0(digits, collapse = ""))
|
|
54
|
+
}
|
|
55
|
+
power_two <- function(bits) {
|
|
56
|
+
result <- "1"
|
|
57
|
+
for (i in seq_len(bits)) result <- twice(result)
|
|
58
|
+
result
|
|
59
|
+
}
|
|
60
|
+
decimal_parts <- function(value) {
|
|
61
|
+
expect(grepl("^-?[0-9]+(\\.[0-9]+)?([Ee][+-]?[0-9]+)?$", value))
|
|
62
|
+
exponent_parts <- strsplit(tolower(value), "e", fixed = TRUE)[[1L]]
|
|
63
|
+
exponent <- if (length(exponent_parts) == 2L) as.integer(exponent_parts[2L]) else 0L
|
|
64
|
+
expect(!is.na(exponent) && abs(exponent) <= 152L)
|
|
65
|
+
negative <- startsWith(exponent_parts[1L], "-")
|
|
66
|
+
body <- sub("^-", "", exponent_parts[1L])
|
|
67
|
+
dot <- regexpr(".", body, fixed = TRUE)[1L]
|
|
68
|
+
fraction <- if (dot < 0L) 0L else nchar(body) - dot
|
|
69
|
+
digits <- gsub(".", "", body, fixed = TRUE)
|
|
70
|
+
scale <- fraction - exponent
|
|
71
|
+
if (scale < 0L) {
|
|
72
|
+
digits <- paste0(digits, strrep("0", -scale))
|
|
73
|
+
scale <- 0L
|
|
74
|
+
}
|
|
75
|
+
digits <- sub("^0+", "", digits)
|
|
76
|
+
if (!nzchar(digits)) return(list(number = "0", scale = 0L))
|
|
77
|
+
while (scale > 0L && endsWith(digits, "0")) {
|
|
78
|
+
digits <- substr(digits, 1L, nchar(digits) - 1L)
|
|
79
|
+
scale <- scale - 1L
|
|
80
|
+
}
|
|
81
|
+
list(number = paste0(if (negative) "-" else "", digits), scale = scale)
|
|
82
|
+
}
|
|
83
|
+
decimal_compare <- function(left, right) {
|
|
84
|
+
a <- strsplit(left, ":", fixed = TRUE)[[1L]]
|
|
85
|
+
b <- strsplit(right, ":", fixed = TRUE)[[1L]]
|
|
86
|
+
a_scale <- as.integer(a[2L])
|
|
87
|
+
b_scale <- as.integer(b[2L])
|
|
88
|
+
int_compare(
|
|
89
|
+
paste0(a[1L], strrep("0", max(0L, b_scale - a_scale))),
|
|
90
|
+
paste0(b[1L], strrep("0", max(0L, a_scale - b_scale)))
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
read_table <- function(path) {
|
|
94
|
+
lines <- readLines(path, encoding = "ASCII", warn = TRUE)
|
|
95
|
+
expect(length(lines) >= 2L && lines[1L] == "EFR_TABLE_1")
|
|
96
|
+
dims <- as.integer(split(lines[2L]))
|
|
97
|
+
expect(length(dims) == 2L && all(!is.na(dims)) && all(dims >= 0L))
|
|
98
|
+
rows <- dims[1L]
|
|
99
|
+
columns <- dims[2L]
|
|
100
|
+
expect(columns > 0L && length(lines) == 2L + columns + rows)
|
|
101
|
+
metadata <- lapply(lines[2L + seq_len(columns)], split)
|
|
102
|
+
expect(all(lengths(metadata) == 2L))
|
|
103
|
+
names <- vapply(metadata, `[[`, "", 1L)
|
|
104
|
+
signatures <- vapply(metadata, `[[`, "", 2L)
|
|
105
|
+
expect(!anyDuplicated(names))
|
|
106
|
+
data <- if (rows) lapply(lines[2L + columns + seq_len(rows)], split) else list()
|
|
107
|
+
expect(all(lengths(data) == columns))
|
|
108
|
+
list(rows = rows, names = names, signatures = signatures, data = data)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
config <- readLines(file.path(directory, "contract.txt"), encoding = "ASCII")
|
|
112
|
+
expect(length(config) >= 5L && config[1L] == "EFR_CONTRACT_1")
|
|
113
|
+
header <- split(config[2L])
|
|
114
|
+
count <- as.integer(header[1L])
|
|
115
|
+
expect(length(header) == 3L && !is.na(count) && count > 0L)
|
|
116
|
+
expect(header[2L] %in% c("significant", "ignored"))
|
|
117
|
+
expect(header[3L] %in% c("preserve", "distinct"))
|
|
118
|
+
mode <- config[3L]
|
|
119
|
+
expect(mode %in% c("sequence", "bag"))
|
|
120
|
+
columns <- lapply(config[3L + seq_len(count)], split)
|
|
121
|
+
expect(all(lengths(columns) == 4L))
|
|
122
|
+
expected_names <- vapply(columns, `[[`, "", 1L)
|
|
123
|
+
expected_types <- vapply(columns, `[[`, "", 2L)
|
|
124
|
+
cursor <- 4L + count
|
|
125
|
+
key_count <- as.integer(config[cursor])
|
|
126
|
+
expect(!is.na(key_count) && key_count >= 0L)
|
|
127
|
+
cursor <- cursor + 1L
|
|
128
|
+
keys <- if (key_count) lapply(config[cursor - 1L + seq_len(key_count)], split) else list()
|
|
129
|
+
expect(all(lengths(keys) == 3L))
|
|
130
|
+
cursor <- cursor + key_count
|
|
131
|
+
order_header <- split(config[cursor])
|
|
132
|
+
sort_count <- as.integer(order_header[1L])
|
|
133
|
+
expect(length(order_header) == 2L && !is.na(sort_count) && sort_count >= 0L)
|
|
134
|
+
cursor <- cursor + 1L
|
|
135
|
+
sorts <- if (sort_count) lapply(config[cursor - 1L + seq_len(sort_count)], split) else list()
|
|
136
|
+
expect(all(lengths(sorts) == 3L) && length(config) == cursor - 1L + sort_count)
|
|
137
|
+
expect((mode == "sequence") == (sort_count > 0L))
|
|
138
|
+
|
|
139
|
+
violations <- 0L
|
|
140
|
+
violate <- function() violations <<- violations + 1L
|
|
141
|
+
# Bounds belong to the declared integer type, not to an individual cell.
|
|
142
|
+
# Keep the original decimal-digit arithmetic and reuse its exact result.
|
|
143
|
+
integer_ranges <- new.env(hash = TRUE, parent = emptyenv())
|
|
144
|
+
integer_range <- function(bits, signed) {
|
|
145
|
+
key <- paste0(bits, ":", as.integer(signed))
|
|
146
|
+
if (!exists(key, envir = integer_ranges, inherits = FALSE)) {
|
|
147
|
+
limit <- power_two(bits - as.integer(signed))
|
|
148
|
+
assign(key, list(
|
|
149
|
+
minimum = if (signed) paste0("-", limit) else "0",
|
|
150
|
+
maximum = subtract_one(limit)
|
|
151
|
+
), envir = integer_ranges)
|
|
152
|
+
}
|
|
153
|
+
get(key, envir = integer_ranges, inherits = FALSE)
|
|
154
|
+
}
|
|
155
|
+
column_signatures <- lapply(columns, function(column) {
|
|
156
|
+
strsplit(column[2L], ":", fixed = TRUE)[[1L]]
|
|
157
|
+
})
|
|
158
|
+
column_integer_ranges <- lapply(column_signatures, function(signature) {
|
|
159
|
+
if (signature[1L] == "integer") {
|
|
160
|
+
integer_range(as.integer(signature[2L]), signature[3L] == "1")
|
|
161
|
+
} else NULL
|
|
162
|
+
})
|
|
163
|
+
normalize <- function(token, column, signature, bounds) {
|
|
164
|
+
expect(nzchar(token))
|
|
165
|
+
tag <- substr(token, 1L, 1L)
|
|
166
|
+
value <- substring(token, 2L)
|
|
167
|
+
if (tag == "N") {
|
|
168
|
+
expect(token == "N")
|
|
169
|
+
if (column[3L] == "0") violate()
|
|
170
|
+
return(token)
|
|
171
|
+
}
|
|
172
|
+
kind <- signature[1L]
|
|
173
|
+
if (kind == "boolean") {
|
|
174
|
+
expect(token %in% c("B0", "B1"))
|
|
175
|
+
} else if (kind == "string") {
|
|
176
|
+
expect(tag == "S" && (value == "-" || grepl("^([0-9a-f]{2})+$", value)))
|
|
177
|
+
} else if (kind == "integer") {
|
|
178
|
+
expect(tag == "I")
|
|
179
|
+
canonical <- integer_text(value)
|
|
180
|
+
if (int_compare_canonical(canonical, bounds$minimum) < 0L ||
|
|
181
|
+
int_compare_canonical(canonical, bounds$maximum) > 0L) violate()
|
|
182
|
+
token <- paste0("I", canonical)
|
|
183
|
+
} else if (kind == "decimal") {
|
|
184
|
+
expect(tag == "D")
|
|
185
|
+
parts <- decimal_parts(value)
|
|
186
|
+
precision <- as.integer(signature[2L])
|
|
187
|
+
scale <- as.integer(signature[3L])
|
|
188
|
+
digits <- sub("^-", "", parts$number)
|
|
189
|
+
if (parts$number != "0" &&
|
|
190
|
+
(parts$scale > scale || nchar(digits) + scale - parts$scale > precision)) violate()
|
|
191
|
+
token <- paste0("D", parts$number, ":", parts$scale)
|
|
192
|
+
} else if (kind == "float") {
|
|
193
|
+
expect(tag %in% c("F", "Q", "P", "M"))
|
|
194
|
+
if (tag == "Q" && column[4L] == "forbid") violate()
|
|
195
|
+
if (tag == "F") {
|
|
196
|
+
number <- as.numeric(value)
|
|
197
|
+
expect(is.finite(number))
|
|
198
|
+
# R performs its own binary conversion; signed zero is equal.
|
|
199
|
+
token <- paste0("F", if (number == 0) "0" else sprintf("%a", number))
|
|
200
|
+
} else expect(nchar(token) == 1L)
|
|
201
|
+
} else if (kind %in% c("date", "timestamp")) {
|
|
202
|
+
expect(tag == if (kind == "date") "A" else "T")
|
|
203
|
+
token <- paste0(tag, integer_text(value))
|
|
204
|
+
} else stop("unsupported type", call. = FALSE)
|
|
205
|
+
token
|
|
206
|
+
}
|
|
207
|
+
compare_cell <- function(a, b, sort_spec) {
|
|
208
|
+
if (a == "N" || b == "N") {
|
|
209
|
+
result <- as.integer(a == "N") - as.integer(b == "N")
|
|
210
|
+
return(result * if (sort_spec[3L] == "first") -1L else 1L)
|
|
211
|
+
}
|
|
212
|
+
tag <- substr(a, 1L, 1L)
|
|
213
|
+
other <- substr(b, 1L, 1L)
|
|
214
|
+
if (tag %in% c("M", "F", "P", "Q")) {
|
|
215
|
+
rank <- c(M = 0L, F = 1L, P = 2L, Q = 3L)
|
|
216
|
+
result <- sign(rank[tag] - rank[other])
|
|
217
|
+
if (!result && tag == "F") {
|
|
218
|
+
result <- sign(as.numeric(substring(a, 2L)) - as.numeric(substring(b, 2L)))
|
|
219
|
+
}
|
|
220
|
+
} else if (tag %in% c("I", "A", "T")) {
|
|
221
|
+
result <- int_compare_canonical(substring(a, 2L), substring(b, 2L))
|
|
222
|
+
} else if (tag == "D") {
|
|
223
|
+
result <- decimal_compare(substring(a, 2L), substring(b, 2L))
|
|
224
|
+
} else {
|
|
225
|
+
result <- if (a == b) 0L else if (a < b) -1L else 1L
|
|
226
|
+
}
|
|
227
|
+
result * if (sort_spec[2L] == "ascending") 1L else -1L
|
|
228
|
+
}
|
|
229
|
+
prepare <- function(table) {
|
|
230
|
+
aligned <- match(expected_names, table$names)
|
|
231
|
+
if (length(table$names) != count || anyNA(aligned) ||
|
|
232
|
+
(header[2L] == "significant" && !identical(table$names, expected_names))) {
|
|
233
|
+
violate()
|
|
234
|
+
return(NULL)
|
|
235
|
+
}
|
|
236
|
+
if (!identical(unname(table$signatures[aligned]), unname(expected_types))) {
|
|
237
|
+
violate()
|
|
238
|
+
return(NULL)
|
|
239
|
+
}
|
|
240
|
+
result <- lapply(table$data, function(row) {
|
|
241
|
+
vapply(seq_len(count), function(j) {
|
|
242
|
+
normalize(row[aligned[j]], columns[[j]], column_signatures[[j]],
|
|
243
|
+
column_integer_ranges[[j]])
|
|
244
|
+
}, "")
|
|
245
|
+
})
|
|
246
|
+
encoded <- vapply(result, paste, "", collapse = "\t")
|
|
247
|
+
if (header[3L] == "distinct") violations <<- violations + sum(duplicated(encoded))
|
|
248
|
+
for (key in keys) {
|
|
249
|
+
indices <- as.integer(strsplit(key[3L], ",", fixed = TRUE)[[1L]])
|
|
250
|
+
expect(all(!is.na(indices)) && all(indices >= 1L & indices <= count))
|
|
251
|
+
key_rows <- lapply(result, `[`, indices)
|
|
252
|
+
incomplete <- vapply(key_rows, function(values) any(values == "N"), FALSE)
|
|
253
|
+
if (key[2L] == "reject") violations <<- violations + sum(incomplete)
|
|
254
|
+
if (key[1L] == "1") {
|
|
255
|
+
complete <- vapply(key_rows[!incomplete], paste, "", collapse = "\t")
|
|
256
|
+
violations <<- violations + sum(duplicated(complete))
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (length(result) > 1L && mode == "sequence") {
|
|
260
|
+
for (i in 2L:length(result)) {
|
|
261
|
+
comparison <- 0L
|
|
262
|
+
for (sort_spec in sorts) {
|
|
263
|
+
j <- as.integer(sort_spec[1L])
|
|
264
|
+
expect(!is.na(j) && j >= 1L && j <= count)
|
|
265
|
+
comparison <- compare_cell(result[[i - 1L]][j], result[[i]][j], sort_spec)
|
|
266
|
+
if (comparison) break
|
|
267
|
+
}
|
|
268
|
+
if (comparison > 0L || (comparison == 0L && order_header[2L] == "reject")) violate()
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (mode == "bag") sort(encoded, method = "radix") else encoded
|
|
272
|
+
}
|
|
273
|
+
left <- read_table(file.path(directory, "left.txt"))
|
|
274
|
+
right <- read_table(file.path(directory, "right.txt"))
|
|
275
|
+
a <- prepare(left)
|
|
276
|
+
b <- prepare(right)
|
|
277
|
+
matched <- 0L
|
|
278
|
+
if (!is.null(a) && !is.null(b)) {
|
|
279
|
+
if (mode == "sequence") {
|
|
280
|
+
n <- min(length(a), length(b))
|
|
281
|
+
if (n) matched <- sum(a[seq_len(n)] == b[seq_len(n)])
|
|
282
|
+
} else {
|
|
283
|
+
i <- 1L
|
|
284
|
+
j <- 1L
|
|
285
|
+
while (i <= length(a) && j <= length(b)) {
|
|
286
|
+
if (a[i] == b[j]) {
|
|
287
|
+
matched <- matched + 1L
|
|
288
|
+
i <- i + 1L
|
|
289
|
+
j <- j + 1L
|
|
290
|
+
} else if (a[i] < b[j]) i <- i + 1L else j <- j + 1L
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
unmatched <- left$rows + right$rows - 2L * matched
|
|
295
|
+
state <- if (unmatched || violations) "different" else "equivalent"
|
|
296
|
+
version <- paste(sprintf("%02x", as.integer(charToRaw(R.version.string))), collapse = "")
|
|
297
|
+
writeLines(c("EFR_RESULT_1", state, as.character(matched), as.character(unmatched),
|
|
298
|
+
as.character(violations), as.character(left$rows), as.character(right$rows),
|
|
299
|
+
version), file.path(directory, "result.txt"), useBytes = TRUE)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
arguments <- commandArgs(trailingOnly = TRUE)
|
|
303
|
+
if (length(arguments) != 1L) quit(status = 2L)
|
|
304
|
+
tryCatch(main(arguments[1L]), error = function(error) {
|
|
305
|
+
# No row values or paths are printed by the process error channel.
|
|
306
|
+
writeLines("equiframe independent R review failed", stderr())
|
|
307
|
+
quit(status = 3L)
|
|
308
|
+
})
|
equiframe/_regular_io.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Regular-file reads without following links or blocking on special files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import errno
|
|
6
|
+
import os
|
|
7
|
+
import stat
|
|
8
|
+
from collections.abc import Iterator
|
|
9
|
+
from contextlib import contextmanager
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import BinaryIO
|
|
12
|
+
|
|
13
|
+
from equiframe.errors import ArtifactIntegrityError, ArtifactIOError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def absolute_path(
|
|
17
|
+
path: Path, *, path_error: type[ArtifactIntegrityError] = ArtifactIntegrityError
|
|
18
|
+
) -> Path:
|
|
19
|
+
if ".." in path.parts:
|
|
20
|
+
raise path_error("parent traversal is not permitted")
|
|
21
|
+
try:
|
|
22
|
+
return path.absolute()
|
|
23
|
+
except OSError as error:
|
|
24
|
+
raise ArtifactIOError("cannot determine absolute evidence path") from error
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def regular_directory(
|
|
28
|
+
path: Path, *, path_error: type[ArtifactIntegrityError] = ArtifactIntegrityError
|
|
29
|
+
) -> Path:
|
|
30
|
+
path = absolute_path(path, path_error=path_error)
|
|
31
|
+
try:
|
|
32
|
+
for item in (*reversed(path.parents), path):
|
|
33
|
+
inspected = item.lstat()
|
|
34
|
+
if not (
|
|
35
|
+
stat.S_ISDIR(inspected.st_mode)
|
|
36
|
+
and not getattr(inspected, "st_file_attributes", 0) & 0x400
|
|
37
|
+
):
|
|
38
|
+
raise path_error("evidence directories must not be symlinks or reparse points")
|
|
39
|
+
except OSError as error:
|
|
40
|
+
raise ArtifactIOError("cannot inspect evidence directory") from error
|
|
41
|
+
return path
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def local_input_path(path: Path) -> Path:
|
|
45
|
+
"""Admit explicit local parent navigation after inspecting traversed directories.
|
|
46
|
+
|
|
47
|
+
Evidence confinement still uses absolute_path and refuses parent navigation.
|
|
48
|
+
This helper is for caller-selected local paths, which have no fixed root.
|
|
49
|
+
"""
|
|
50
|
+
if not isinstance(path, Path):
|
|
51
|
+
raise TypeError("local path must be a Path")
|
|
52
|
+
path = path.absolute()
|
|
53
|
+
current = Path(path.anchor)
|
|
54
|
+
for component in path.parts[1:]:
|
|
55
|
+
if component == "..":
|
|
56
|
+
regular_directory(current)
|
|
57
|
+
current = current.parent
|
|
58
|
+
else:
|
|
59
|
+
current = current / component
|
|
60
|
+
return current
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def new_directory_path(path: Path, *, ordinary_file_error: bool = False) -> Path:
|
|
64
|
+
"""Check existing lexical ancestors before creating a new output directory."""
|
|
65
|
+
path = absolute_path(path)
|
|
66
|
+
for item in (*reversed(path.parents), path):
|
|
67
|
+
try:
|
|
68
|
+
inspected = item.lstat()
|
|
69
|
+
except FileNotFoundError:
|
|
70
|
+
continue
|
|
71
|
+
except OSError as error:
|
|
72
|
+
raise ArtifactIOError("cannot inspect output directory") from error
|
|
73
|
+
if (
|
|
74
|
+
ordinary_file_error
|
|
75
|
+
and stat.S_ISREG(inspected.st_mode)
|
|
76
|
+
and not (getattr(inspected, "st_file_attributes", 0) & 0x400)
|
|
77
|
+
):
|
|
78
|
+
raise NotADirectoryError(errno.ENOTDIR, "operation output requires a directory")
|
|
79
|
+
if not stat.S_ISDIR(inspected.st_mode) or (
|
|
80
|
+
getattr(inspected, "st_file_attributes", 0) & 0x400
|
|
81
|
+
):
|
|
82
|
+
raise ArtifactIntegrityError("output directories must not be links or special files")
|
|
83
|
+
return path
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _walk_error(error: OSError) -> None:
|
|
87
|
+
raise ArtifactIOError("cannot inventory evidence directory") from error
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def local_output_directory(path: Path) -> Path:
|
|
91
|
+
"""Inspect caller-selected ancestors before normalizing a new operation root.
|
|
92
|
+
|
|
93
|
+
Returning a path does not create directories or sandbox concurrent filesystem
|
|
94
|
+
owners. Each writer retains its existing exclusive publication mechanism.
|
|
95
|
+
"""
|
|
96
|
+
return new_directory_path(local_input_path(path), ordinary_file_error=True)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def regular_files(directory: Path) -> tuple[Path, ...]:
|
|
100
|
+
"""Inventory a regular directory tree, refusing links and special members."""
|
|
101
|
+
root = regular_directory(directory)
|
|
102
|
+
members: list[Path] = []
|
|
103
|
+
for parent, directories, files in os.walk(root, followlinks=False, onerror=_walk_error):
|
|
104
|
+
for names, expected in ((directories, stat.S_ISDIR), (files, stat.S_ISREG)):
|
|
105
|
+
for name in names:
|
|
106
|
+
path = Path(parent) / name
|
|
107
|
+
try:
|
|
108
|
+
inspected = path.lstat()
|
|
109
|
+
except OSError as error:
|
|
110
|
+
raise ArtifactIOError("cannot inspect evidence member") from error
|
|
111
|
+
if not expected(inspected.st_mode) or (
|
|
112
|
+
getattr(inspected, "st_file_attributes", 0) & 0x400
|
|
113
|
+
):
|
|
114
|
+
raise ArtifactIntegrityError(
|
|
115
|
+
"evidence inventory rejects links and special files"
|
|
116
|
+
)
|
|
117
|
+
members.extend(Path(parent) / name for name in files)
|
|
118
|
+
return tuple(sorted(members))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@contextmanager
|
|
122
|
+
def open_regular_file(
|
|
123
|
+
path: Path, *, path_error: type[ArtifactIntegrityError] = ArtifactIntegrityError
|
|
124
|
+
) -> Iterator[BinaryIO]:
|
|
125
|
+
"""Pin POSIX directories; on other hosts check identities around open.
|
|
126
|
+
|
|
127
|
+
These checks do not sandbox hostile concurrent mounts or filesystem owners.
|
|
128
|
+
"""
|
|
129
|
+
parent = regular_directory(path.parent, path_error=path_error)
|
|
130
|
+
descriptor: int | None = None
|
|
131
|
+
directory_descriptors: list[int] = []
|
|
132
|
+
try:
|
|
133
|
+
before = path.lstat()
|
|
134
|
+
if not (
|
|
135
|
+
stat.S_ISREG(before.st_mode) and not getattr(before, "st_file_attributes", 0) & 0x400
|
|
136
|
+
):
|
|
137
|
+
raise path_error("evidence must be a regular file, not a link or reparse point")
|
|
138
|
+
flags = os.O_RDONLY | getattr(os, "O_BINARY", 0) | getattr(os, "O_NONBLOCK", 0)
|
|
139
|
+
if os.open in os.supports_dir_fd and hasattr(os, "O_NOFOLLOW"):
|
|
140
|
+
no_follow = getattr(os, "O_NOFOLLOW", 0)
|
|
141
|
+
directory_flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | no_follow
|
|
142
|
+
current = os.open(parent.anchor, directory_flags)
|
|
143
|
+
directory_descriptors.append(current)
|
|
144
|
+
for component in parent.parts[1:]:
|
|
145
|
+
current = os.open(component, directory_flags, dir_fd=current)
|
|
146
|
+
directory_descriptors.append(current)
|
|
147
|
+
descriptor = os.open(path.name, flags | no_follow, dir_fd=current)
|
|
148
|
+
else:
|
|
149
|
+
descriptor = os.open(path, flags)
|
|
150
|
+
regular_directory(parent, path_error=path_error)
|
|
151
|
+
after = os.fstat(descriptor)
|
|
152
|
+
if not (
|
|
153
|
+
stat.S_ISREG(after.st_mode) and not getattr(after, "st_file_attributes", 0) & 0x400
|
|
154
|
+
):
|
|
155
|
+
raise path_error("evidence must remain a regular file without reparse points")
|
|
156
|
+
if (before.st_dev, before.st_ino) != (after.st_dev, after.st_ino):
|
|
157
|
+
raise ArtifactIntegrityError("evidence file changed while opening")
|
|
158
|
+
with os.fdopen(descriptor, "rb") as stream:
|
|
159
|
+
descriptor = None
|
|
160
|
+
yield stream
|
|
161
|
+
except OSError as error:
|
|
162
|
+
raise ArtifactIOError("cannot read evidence file") from error
|
|
163
|
+
finally:
|
|
164
|
+
if descriptor is not None:
|
|
165
|
+
os.close(descriptor)
|
|
166
|
+
for directory_descriptor in reversed(directory_descriptors):
|
|
167
|
+
os.close(directory_descriptor)
|