From 1bd91841ca3d8e1761e131b3dc69405032d14bff Mon Sep 17 00:00:00 2001 From: Alan Francis Date: Tue, 30 Apr 2024 10:28:40 +0530 Subject: [PATCH] feat: initial version of orchestration HTTP service #72 --- ...vices.http => orch-http-service.test.http} | 80 ++- .../screening/orch-http-service.ts | 516 ++++++++---------- .../synthetic-content/INGRESS_CSV_1.zip | Bin 0 -> 23090 bytes .../synthetic-content/INGRESS_CSV_2.zip | Bin 0 -> 23216 bytes .../synthetic-content/INGRESS_CSV_3.zip | Bin 0 -> 23216 bytes .../synthetic-content/INGRESS_CSV_4.zip | Bin 0 -> 23216 bytes .../synthetic-content/INGRESS_CSV_5.zip | Bin 0 -> 23216 bytes 7 files changed, 311 insertions(+), 285 deletions(-) rename src/ahc-hrsn-elt/screening/{orch-services.http => orch-http-service.test.http} (70%) create mode 100644 support/assurance/ahc-hrsn-elt/screening/synthetic-content/INGRESS_CSV_1.zip create mode 100644 support/assurance/ahc-hrsn-elt/screening/synthetic-content/INGRESS_CSV_2.zip create mode 100644 support/assurance/ahc-hrsn-elt/screening/synthetic-content/INGRESS_CSV_3.zip create mode 100644 support/assurance/ahc-hrsn-elt/screening/synthetic-content/INGRESS_CSV_4.zip create mode 100644 support/assurance/ahc-hrsn-elt/screening/synthetic-content/INGRESS_CSV_5.zip diff --git a/src/ahc-hrsn-elt/screening/orch-services.http b/src/ahc-hrsn-elt/screening/orch-http-service.test.http similarity index 70% rename from src/ahc-hrsn-elt/screening/orch-services.http rename to src/ahc-hrsn-elt/screening/orch-http-service.test.http index 779182bd..593f6fc9 100644 --- a/src/ahc-hrsn-elt/screening/orch-services.http +++ b/src/ahc-hrsn-elt/screening/orch-http-service.test.http @@ -2,6 +2,7 @@ # the end point receives data as files and text in POST method # ------------------------------------------------------------------------ # * requires the `vscode-httpyac` Visual Studio Code extension +# * `adamraichu.zip-viewer` Visual Studio Code extension is desirable for viewing the emitted zip files # * requires a `.env` file with HOST, PORT, INGRESS_FILE_PATH, EGRESS_FILE_PATH defined # * the below Deno command will get the HTTP orch server up and running # * deno run -A ./src/ahc-hrsn-elt/screening/orch-http-service.ts --port 8088 --host 0.0.0.0 --shinny-fhir-url "https://40lafnwsw7.execute-api.us-east-1.amazonaws.com/dev/processingAgent=QE" --session-artifacts-home /HTTP @@ -9,14 +10,14 @@ ### -@contentType = multipart/form-data; boundary=form-boundary +# @openWith adamraichu.zip-viewer @hostName = {{$dotenv HOST}} @port = {{$dotenv PORT}} @ingressFilePath = {{$dotenv INGRESS_FILE_PATH}} @egressFilePath = {{$dotenv EGRESS_FILE_PATH}} POST http://{{hostName}}:{{port}}/orchestrate.zip HTTP/1.1 -Content-Type: {{contentType}} +Content-Type: multipart/form-data; boundary=form-boundary {{ const { equal } = require('assert'); test('status code 200', () => { @@ -52,14 +53,13 @@ Content-Type: application/zip ### -@contentType = multipart/form-data; boundary=form-boundary @hostName = {{$dotenv HOST}} @port = {{$dotenv PORT}} @ingressFilePath = {{$dotenv INGRESS_FILE_PATH}} -@egressFilePath = {{$dotenv EGRESS_FILE_PATH}} +@egressFilePath = {{$dotenv EGRESS_FILE_PATH}} POST http://{{hostName}}:{{port}}/orchestrate.json HTTP/1.1 -Content-Type: {{contentType}} +Content-Type: multipart/form-data; boundary=form-boundary {{ const { equal } = require('assert'); test('status code 200', () => { @@ -69,7 +69,7 @@ Content-Type: {{contentType}} equal(response.contentType.contentType, 'application/json; charset=UTF-8'); }); test('check version', () => { - equal(JSON.parse(response.body).version, '0.16.2'); + equal(JSON.parse(response.body).version, '0.17.1'); }); }} @@ -98,14 +98,14 @@ Content-Type: application/zip ### -@contentType = multipart/form-data; boundary=form-boundary +# @openWith adamraichu.zip-viewer @hostName = {{$dotenv HOST}} @port = {{$dotenv PORT}} @ingressFilePath = {{$dotenv INGRESS_FILE_PATH}} @egressFilePath = {{$dotenv EGRESS_FILE_PATH}} POST http://{{hostName}}:{{port}}/orchestrate.zip HTTP/1.1 -Content-Type: {{contentType}} +Content-Type: multipart/form-data; boundary=form-boundary {{ const { equal } = require('assert'); test('status code 200', () => { @@ -152,14 +152,14 @@ Content-Type: text/csv ### -@contentType = multipart/form-data; boundary=form-boundary +# @openWith adamraichu.zip-viewer @hostName = {{$dotenv HOST}} @port = {{$dotenv PORT}} @ingressFilePath = {{$dotenv INGRESS_FILE_PATH}} @egressFilePath = {{$dotenv EGRESS_FILE_PATH}} POST http://{{hostName}}:{{port}}/orchestrate.zip HTTP/1.1 -Content-Type: {{contentType}} +Content-Type: multipart/form-data; boundary=form-boundary {{ const { equal } = require('assert'); test('status code 200', () => { @@ -193,3 +193,63 @@ Content-Type: text/csv --form-boundary-- >>! {{egressFilePath}}egress-tx.zip + + +### + +# @openWith adamraichu.zip-viewer +@hostName = {{$dotenv HOST}} +@port = {{$dotenv PORT}} +@ingressFilePath = {{$dotenv INGRESS_FILE_PATH}} +@egressFilePath = {{$dotenv EGRESS_FILE_PATH}} + +POST http://{{hostName}}:{{port}}/orchestrate.zip HTTP/1.1 +Content-Type: multipart/form-data; boundary=form-boundary +{{ + const { equal } = require('assert'); + test('status code 200', () => { + equal(response.statusCode, 200); + }); + test.hasResponseBody(); + test('Check content-type', () => { + equal(response.contentType.contentType, 'application/zip'); + }); +}} + + +--form-boundary +Content-Disposition: form-data; name="qe"; + +healthelink +--form-boundary +Content-Disposition: form-data; name="submit-shin-ny"; + +yes +--form-boundary +Content-Disposition: form-data; name="file"; filename="INGRESS_CSV_1.zip" +Content-Type: application/zip + +< {{ingressFilePath}}INGRESS_CSV_1.zip +--form-boundary +Content-Disposition: form-data; name="file"; filename="INGRESS_CSV_2.zip" +Content-Type: application/zip + +< {{ingressFilePath}}INGRESS_CSV_2.zip +--form-boundary +Content-Disposition: form-data; name="file"; filename="INGRESS_CSV_3.zip" +Content-Type: application/zip + +< {{ingressFilePath}}INGRESS_CSV_3.zip +--form-boundary +Content-Disposition: form-data; name="file"; filename="INGRESS_CSV_4.zip" +Content-Type: application/zip + +< {{ingressFilePath}}INGRESS_CSV_4.zip +--form-boundary +Content-Disposition: form-data; name="file"; filename="INGRESS_CSV_5.zip" +Content-Type: application/zip + +< {{ingressFilePath}}INGRESS_CSV_5.zip +--form-boundary-- + +>>! {{egressFilePath}}egress-tx.zip diff --git a/src/ahc-hrsn-elt/screening/orch-http-service.ts b/src/ahc-hrsn-elt/screening/orch-http-service.ts index d846484c..575b15ce 100644 --- a/src/ahc-hrsn-elt/screening/orch-http-service.ts +++ b/src/ahc-hrsn-elt/screening/orch-http-service.ts @@ -2,9 +2,9 @@ import { Command } from "https://deno.land/x/cliffy@v1.0.0-rc.3/command/mod.ts"; import { Application, - Context, Router, -} from "https://deno.land/x/oak@v12.6.2/mod.ts"; + RouterContext, +} from "https://deno.land/x/oak@v15.0.0/mod.ts"; import JSZip from "npm:jszip"; import { colors as c, @@ -197,299 +197,265 @@ const runServer = async ( const app = new Application(); const router = new Router(); - router.post("/orchestrate.zip", async (context: Context) => { - if (!context.request.hasBody) { - context.response.status = 400; - context.response.body = "No data submitted"; - return; - } - - const bodyResult = context.request.body({ type: "form-data" }); - const formData = await bodyResult.value.read(); - if (!formData.fields.qe) { - context.response.status = 400; - context.response.body = "No QE found in the request."; - return; - } - let submitShinNY = "yes"; - if (formData.fields["submit-shin-ny"]) { - submitShinNY = formData.fields["submit-shin-ny"].toLowerCase(); - } - let persistDiagnostics = "no"; - if (formData.fields["persist-diagnostics"]) { - persistDiagnostics = formData.fields["persist-diagnostics"].toLowerCase(); - } - const formDataFiles = formData.files ? formData.files : []; - if (formDataFiles.length == 0) { - context.response.status = 400; - context.response.body = "No files found in the request."; - return; - } - const qe = formData.fields.qe; - const govn = new ddbo.DuckDbOrchGovernance( - true, - new ddbo.DuckDbOrchEmitContext(), - ); - const sessionID = await govn.emitCtx.newUUID(false); - const basePath = `${rootPath}/${qe}`; - const egressPath = `${rootPath}/${qe}/egress`; - const ingressTxPath = `${egressPath}/${sessionID}/.ingress-tx`; - const egressSessionPath = `${egressPath}/${sessionID}`; - const workflowPaths = mod.orchEngineWorkflowPaths( - basePath, - sessionID, - ); + router.post( + "/orchestrate.zip", + async (context: RouterContext<"/orchestrate.zip">) => { + if (!context.request.hasBody) { + context.response.status = 400; + context.response.body = "No data submitted"; + return; + } - const ingressTxPaths = mod.orchEngineIngressPaths( - workflowPaths.ingressTx.home, - ); - await workflowPaths.initializePaths?.(); - for (const files of formDataFiles) { - if (files.contentType == "application/zip" && files.filename) { - const zip = new JSZip(); - // Read the file from the filename path - const zipData = await Deno.readFile(files.filename); - const unzippedData = await zip.loadAsync(zipData); + const bodyResult = context.request.body; + const formData = await bodyResult.formData(); - await Promise.all( - Object.keys(unzippedData.files).map(async (fileName) => { - const file = unzippedData.files[fileName]; - if (!file.dir) { - const content = await file.async("uint8array"); - const filePath = path.join(ingressTxPath, fileName); - await Deno.writeFile(filePath, content); - } - }), - ); - } else { - if (files.filename) { - const filePath = path.join(ingressTxPath, files.originalName); - await Deno.writeFile(filePath, await Deno.readFile(files.filename)); - } + // console.log(formData); + if (!formData.has("qe")) { + context.response.status = 400; + context.response.body = "No QE found in the request."; + return; } - } - const screeningGroups = new mod.ScreeningIngressGroups( - async (group) => { - await orchestrateFiles( - sessionID, - govn, - ingressTxPaths, - group, - workflowPaths, - referenceDataHome, - submitShinNY, - shinnyFhirUrl, - ); - }, - ); - const watchPaths: o.WatchFsPath[] = [{ - pathID: "ingress", - rootPath: ingressTxPaths.ingress.home, - onIngress: (entry) => { - const group = screeningGroups.potential(entry); - try { - orchestrateFiles( - sessionID, - govn, - ingressTxPaths, - group ?? entry, - workflowPaths, - referenceDataHome, - submitShinNY, - shinnyFhirUrl, - ); - } catch (err) { - // TODO: store the error in a proper log - console.dir(entry); - console.error(err); + let submitShinNY = "yes"; + let persistDiagnostics = "no"; + const formDataFiles = []; + for (const [key, value] of formData) { + if (typeof value == "object") { + formDataFiles.push(value); + } else { + if (key == "submit-shin-ny") { + submitShinNY = value; + } else if (key == "persist-diagnostics") { + persistDiagnostics = value; + } } - }, - }]; - - console.log(`Processing files in ${ingressTxPaths.ingress.home}`); + } + if (formDataFiles.length == 0) { + context.response.status = 400; + context.response.body = "No files found in the request."; + return; + } + console.log( + "Submitted " + formDataFiles.length + + (formDataFiles.length > 1 ? " files." : " file."), + ); + const qe = formData.get("qe")?.toString(); + const govn = new ddbo.DuckDbOrchGovernance( + true, + new ddbo.DuckDbOrchEmitContext(), + ); + const sessionID = await govn.emitCtx.newUUID(false); + const basePath = `${rootPath}/${qe}`; + const egressPath = `${rootPath}/${qe}/egress`; + const ingressTxPath = `${egressPath}/${sessionID}/.ingress-tx`; + const egressSessionPath = `${egressPath}/${sessionID}`; + const workflowPaths = mod.orchEngineWorkflowPaths( + basePath, + sessionID, + ); - await o.ingestWatchedFs({ - drain: async (entries) => { - if (entries.length) { - await orchestrateFiles( - sessionID, - govn, - ingressTxPaths, - entries, - workflowPaths, - referenceDataHome, - submitShinNY, - shinnyFhirUrl, - ); + const ingressTxPaths = mod.orchEngineIngressPaths( + workflowPaths.ingressTx.home, + ); + await workflowPaths.initializePaths?.(); + for (const files of formDataFiles) { + if (files.type == "application/zip" && files.name) { const zip = new JSZip(); - await addFolderToZip(zip, egressSessionPath, persistDiagnostics); - const zipContent = await zip.generateAsync({ - type: "uint8array", - }); - // Specify the final ZIP file path - const finalZipPath = path.join( - egressPath, - sessionID, - "egress-tx.zip", - ); - await Deno.writeFile(finalZipPath, zipContent); - console.log( - `Completed processing files in ${ingressTxPaths.ingress.home}`, + // Read the file from the filename path + const zipData = await files.arrayBuffer(); + const unzippedData = await zip.loadAsync(zipData); + + await Promise.all( + Object.keys(unzippedData.files).map(async (fileName) => { + const file = unzippedData.files[fileName]; + if (!file.dir) { + const content = await file.async("uint8array"); + const filePath = path.join(ingressTxPath, fileName); + await Deno.writeFile(filePath, content); + } + }), ); - // context.response.body = "ZIP file processed and saved successfully."; - // Use below code to emit the zip file - context.response.body = zipContent; - context.response.type = "application/zip"; + } else { + if (files.name) { + const filePath = path.join(ingressTxPath, files.name); + await Deno.writeFile(filePath, files.stream()); + } } - }, - watch: false, - watchPaths, - }); - }); + } - router.post("/orchestrate.json", async (context: Context) => { - if (!context.request.hasBody) { - context.response.status = 400; - context.response.body = "No data submitted"; - return; - } + const watchPaths: o.WatchFsPath[] = [{ + pathID: "ingress", + rootPath: ingressTxPaths.ingress.home, + onIngress: () => {}, + }]; - const bodyResult = context.request.body({ type: "form-data" }); - const formData = await bodyResult.value.read(); - if (!formData.fields.qe) { - context.response.status = 400; - context.response.body = "No QE found in the request."; - return; - } - let submitShinNY = "yes"; - if (formData.fields["submit-shin-ny"]) { - submitShinNY = formData.fields["submit-shin-ny"].toLowerCase(); - } - const formDataFiles = formData.files ? formData.files : []; - if (formDataFiles.length == 0) { - context.response.status = 400; - context.response.body = "No files found in the request."; - return; - } - const qe = formData.fields.qe; - const govn = new ddbo.DuckDbOrchGovernance( - true, - new ddbo.DuckDbOrchEmitContext(), - ); - const sessionID = await govn.emitCtx.newUUID(false); - const basePath = `${rootPath}/${qe}`; - const egressPath = `${rootPath}/${qe}/egress`; - const ingressTxPath = `${egressPath}/${sessionID}/.ingress-tx`; - const egressSessionPath = `${egressPath}/${sessionID}`; - const workflowPaths = mod.orchEngineWorkflowPaths( - basePath, - sessionID, - ); + console.log(`Processing files in ${ingressTxPaths.ingress.home}`); + + await o.ingestWatchedFs({ + drain: async (entries) => { + if (entries.length) { + await orchestrateFiles( + sessionID, + govn, + ingressTxPaths, + entries, + workflowPaths, + referenceDataHome, + submitShinNY, + shinnyFhirUrl, + ); + const zip = new JSZip(); + await addFolderToZip(zip, egressSessionPath, persistDiagnostics); + const zipContent = await zip.generateAsync({ + type: "uint8array", + }); + // Specify the final ZIP file path + const finalZipPath = path.join( + egressPath, + sessionID, + "egress-tx.zip", + ); + await Deno.writeFile(finalZipPath, zipContent); + console.log( + `Completed processing files in ${ingressTxPaths.ingress.home}`, + ); + context.response.body = zipContent; + context.response.type = "application/zip"; + } + }, + watch: false, + watchPaths, + }); + }, + ); + + router.post( + "/orchestrate.json", + async (context: RouterContext<"/orchestrate.json">) => { + if (!context.request.hasBody) { + context.response.status = 400; + context.response.body = "No data submitted"; + return; + } - const ingressTxPaths = mod.orchEngineIngressPaths( - workflowPaths.ingressTx.home, - ); - await workflowPaths.initializePaths?.(); - for (const files of formDataFiles) { - if (files.contentType == "application/zip" && files.filename) { - const zip = new JSZip(); - // Read the file from the filename path - const zipData = await Deno.readFile(files.filename); - const unzippedData = await zip.loadAsync(zipData); + const bodyResult = context.request.body; + const formData = await bodyResult.formData(); - await Promise.all( - Object.keys(unzippedData.files).map(async (fileName) => { - const file = unzippedData.files[fileName]; - if (!file.dir) { - const content = await file.async("uint8array"); - const filePath = path.join(ingressTxPath, fileName); - await Deno.writeFile(filePath, content); - } - }), - ); - } else { - if (files.filename) { - const filePath = path.join(ingressTxPath, files.originalName); - await Deno.writeFile(filePath, await Deno.readFile(files.filename)); + // console.log(formData); + if (!formData.has("qe")) { + context.response.status = 400; + context.response.body = "No QE found in the request."; + return; + } + let submitShinNY = "yes"; + const formDataFiles = []; + for (const [key, value] of formData) { + if (typeof value == "object") { + formDataFiles.push(value); + } else { + if (key == "submit-shin-ny") { + submitShinNY = value; + } } } - } - const screeningGroups = new mod.ScreeningIngressGroups( - async (group) => { - await orchestrateFiles( - sessionID, - govn, - ingressTxPaths, - group, - workflowPaths, - referenceDataHome, - submitShinNY, - shinnyFhirUrl, - ); - }, - ); - const watchPaths: o.WatchFsPath[] = [{ - pathID: "ingress", - rootPath: ingressTxPaths.ingress.home, - onIngress: (entry) => { - const group = screeningGroups.potential(entry); - try { - orchestrateFiles( - sessionID, - govn, - ingressTxPaths, - group ?? entry, - workflowPaths, - referenceDataHome, - submitShinNY, - shinnyFhirUrl, + if (formDataFiles.length == 0) { + context.response.status = 400; + context.response.body = "No files found in the request."; + return; + } + console.log( + "Submitted " + formDataFiles.length + + (formDataFiles.length > 1 ? " files." : " file."), + ); + const qe = formData.get("qe")?.toString(); + const govn = new ddbo.DuckDbOrchGovernance( + true, + new ddbo.DuckDbOrchEmitContext(), + ); + const sessionID = await govn.emitCtx.newUUID(false); + const basePath = `${rootPath}/${qe}`; + const egressPath = `${rootPath}/${qe}/egress`; + const ingressTxPath = `${egressPath}/${sessionID}/.ingress-tx`; + const egressSessionPath = `${egressPath}/${sessionID}`; + const workflowPaths = mod.orchEngineWorkflowPaths( + basePath, + sessionID, + ); + + const ingressTxPaths = mod.orchEngineIngressPaths( + workflowPaths.ingressTx.home, + ); + await workflowPaths.initializePaths?.(); + for (const files of formDataFiles) { + if (files.type == "application/zip" && files.name) { + const zip = new JSZip(); + // Read the file from the filename path + const zipData = await files.arrayBuffer(); + const unzippedData = await zip.loadAsync(zipData); + + await Promise.all( + Object.keys(unzippedData.files).map(async (fileName) => { + const file = unzippedData.files[fileName]; + if (!file.dir) { + const content = await file.async("uint8array"); + const filePath = path.join(ingressTxPath, fileName); + await Deno.writeFile(filePath, content); + } + }), ); - } catch (err) { - // TODO: store the error in a proper log - console.dir(entry); - console.error(err); + } else { + if (files.name) { + const filePath = path.join(ingressTxPath, files.name); + await Deno.writeFile(filePath, files.stream()); + } } - }, - }]; + } - console.log(`Processing files in ${ingressTxPaths.ingress.home}`); + const watchPaths: o.WatchFsPath[] = [{ + pathID: "ingress", + rootPath: ingressTxPaths.ingress.home, + onIngress: () => {}, + }]; - await o.ingestWatchedFs({ - drain: async (entries) => { - if (entries.length) { - await orchestrateFiles( - sessionID, - govn, - ingressTxPaths, - entries, - workflowPaths, - referenceDataHome, - submitShinNY, - shinnyFhirUrl, - ); - const combinedJson = {}; - for await (const entry of Deno.readDir(egressSessionPath)) { - const filePath = path.join(egressSessionPath, entry.name); - if ( - entry.isFile && - (entry.name == "session.json" || entry.name == "diagnostics.json") - ) { - const jsonData = await Deno.readTextFile(filePath); - const json = JSON.parse(jsonData); - Object.assign(combinedJson, json); + console.log(`Processing files in ${ingressTxPaths.ingress.home}`); + + await o.ingestWatchedFs({ + drain: async (entries) => { + if (entries.length) { + await orchestrateFiles( + sessionID, + govn, + ingressTxPaths, + entries, + workflowPaths, + referenceDataHome, + submitShinNY, + shinnyFhirUrl, + ); + const combinedJson = {}; + for await (const entry of Deno.readDir(egressSessionPath)) { + const filePath = path.join(egressSessionPath, entry.name); + if ( + entry.isFile && + (entry.name == "session.json" || + entry.name == "diagnostics.json") + ) { + const jsonData = await Deno.readTextFile(filePath); + const json = JSON.parse(jsonData); + Object.assign(combinedJson, json); + } } + console.log( + `Completed processing files in ${ingressTxPaths.ingress.home}`, + ); + context.response.body = combinedJson; + context.response.type = "application/json"; } - console.log( - `Completed processing files in ${ingressTxPaths.ingress.home}`, - ); - context.response.body = combinedJson; - context.response.type = "application/json"; - } - }, - watch: false, - watchPaths, - }); - }); + }, + watch: false, + watchPaths, + }); + }, + ); app.use(router.routes()); app.use(router.allowedMethods()); diff --git a/support/assurance/ahc-hrsn-elt/screening/synthetic-content/INGRESS_CSV_1.zip b/support/assurance/ahc-hrsn-elt/screening/synthetic-content/INGRESS_CSV_1.zip new file mode 100644 index 0000000000000000000000000000000000000000..4ed553ce2bf63a83852991cc0a5913e928a31c84 GIT binary patch literal 23090 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 J`^!uE^dE;x%sc=9 literal 0 HcmV?d00001 diff --git a/support/assurance/ahc-hrsn-elt/screening/synthetic-content/INGRESS_CSV_2.zip b/support/assurance/ahc-hrsn-elt/screening/synthetic-content/INGRESS_CSV_2.zip new file mode 100644 index 0000000000000000000000000000000000000000..2b7709e4c153ba98c4baf4886be9f0f3fa01f1d2 GIT binary patch literal 23216 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 Mzv18CUDKz(1LAGws{jB1 literal 0 HcmV?d00001 diff --git a/support/assurance/ahc-hrsn-elt/screening/synthetic-content/INGRESS_CSV_3.zip b/support/assurance/ahc-hrsn-elt/screening/synthetic-content/INGRESS_CSV_3.zip new file mode 100644 index 0000000000000000000000000000000000000000..a0f5e5e1053ead5f7b400559ddac473feac2c050 GIT binary patch literal 23216 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 Hnm+vtQQhOb literal 0 HcmV?d00001 diff --git a/support/assurance/ahc-hrsn-elt/screening/synthetic-content/INGRESS_CSV_4.zip b/support/assurance/ahc-hrsn-elt/screening/synthetic-content/INGRESS_CSV_4.zip new file mode 100644 index 0000000000000000000000000000000000000000..0f6a0a1de1fbe6398ef5207aa412b7f51faacdf7 GIT binary patch literal 23216 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 Pef;;o`1iLr^y&Wq%+Km- literal 0 HcmV?d00001