From aaa05a31a513a89b734722a22476acd5a7cd0837 Mon Sep 17 00:00:00 2001 From: sclaw Date: Tue, 29 Sep 2026 10:59:25 -0400 Subject: [PATCH 1/3] Feat: source tif paths from an explicit path column in the scenario database instead of deriving from an implicit file structure. --- CHANGELOG.md | 16 + cmd/controls/controls.go | 28 +- cmd/fim/fim.go | 55 ++-- cmd/validate/validate.go | 296 ++++++------------ pkg/utils/db.go | 29 ++ scripts/test_suite_linux.sh | 46 ++- scripts/test_suite_mac.sh | 46 ++- .../controls/controls_100year.csv | 4 +- .../controls/controls_10year.csv | 4 +- .../controls/controls_25year.csv | 4 +- .../controls/controls_2year.csv | 4 +- .../controls/controls_50year.csv | 4 +- .../controls/controls_5year.csv | 4 +- testdata/reference_data/db/ripple.gpkg | Bin 204800 -> 204800 bytes 14 files changed, 264 insertions(+), 276 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 068ffe9..c7610bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- `controls` and `validate` now fail with an actionable message, including SQL to backfill the column, when the `scenarios` table has no `fim_path` column +- `controls` output file now has a `fim_path` column, appended after `map_exists` + +### Changed + +- **BREAKING** - FIM file paths are now read from a `fim_path` column in the `scenarios` table (relative to the FIM library root) instead of being derived from the `/z_/f_.tif` folder layout. `controls` requires the column, and `fim` requires it in the controls file and resolves relative paths against `-lib`. Absolute and GDAL VSI paths are used as-is +- `fim` no longer requires the `z_/f_.tif` library layout. `domain.tif` is still read from `//` when `-with_domain` is set +- **BREAKING** - `validate` now matches library `.tif` files against `scenarios.fim_path` instead of parsing reach, flow and stage out of the folder layout, so any file layout is accepted. `/domain.tif` files are ignored. Scenarios with `map_exists = 1` and an empty `fim_path` are reported as missing FIMs +- **BREAKING** - `validate` output columns: `-o_fims` gains a trailing `fim_path` column, and `-o_scenarios` now has a single `fim_path` column, since unmatched files are no longer parsed into scenario fields + +### Removed + +- **BREAKING** - `validate` flag `-o_unexpected_fims` and its check. `fim_path` is only recorded for `map_exists = 1` scenarios, so a raster for a mapless scenario cannot be matched to it; such files are reported in `-o_scenarios` instead + ## [0.5.0] - 2026-08-04 **Note:** Compatible with outputs from Ripple1D Pipeline version 0.11.0 (the release containing the `scenarios` rename) to present. Not compatible with earlier outputs, which carry a `rating_curves` table instead, see the Changed section below. diff --git a/cmd/controls/controls.go b/cmd/controls/controls.go index ac2b1f9..0c2173c 100644 --- a/cmd/controls/controls.go +++ b/cmd/controls/controls.go @@ -30,6 +30,7 @@ Database file must have a table 'scenarios' and contain following columns ds_wse REAL boundary_condition TEXT CHECK(boundary_condition IN ('nd','kwse')) map_exists BOOL CHECK(map_exists IN (0, 1)) + fim_path TEXT (FIM file path relative to the FIM library root, NULL when map_exists = 0) UNIQUE(reach_id, us_flow, ds_wse, boundary_condition) Database file must have a table 'network' and contain following columns @@ -57,6 +58,7 @@ type ScenarioRecord struct { ControlReachStage float32 BoundaryCondition string MapExists bool + FimPath string } type ResultRecord struct { @@ -64,6 +66,7 @@ type ResultRecord struct { Flow int ControlReachStageStr string MapExists bool + FimPath string } func ReadFlows(filePath string) (map[int]float32, error) { @@ -180,7 +183,7 @@ func FetchUpstreamReaches(db *sql.DB, controlReachID int) ([]int, error) { func FetchNormalDepthFlowStage(db *sql.DB, reachID int, flow float32) (ScenarioRecord, error) { row := db.QueryRow(` - SELECT us_flow, us_wse, ds_wse, map_exists + SELECT us_flow, us_wse, ds_wse, map_exists, COALESCE(fim_path, '') FROM scenarios WHERE reach_id = ? AND boundary_condition = 'nd' @@ -190,7 +193,7 @@ func FetchNormalDepthFlowStage(db *sql.DB, reachID int, flow float32) (ScenarioR ) var scenario ScenarioRecord - if err := row.Scan(&scenario.Flow, &scenario.Stage, &scenario.ControlReachStage, &scenario.MapExists); err != nil { + if err := row.Scan(&scenario.Flow, &scenario.Stage, &scenario.ControlReachStage, &scenario.MapExists, &scenario.FimPath); err != nil { // Check if the error is because of no rows if err == sql.ErrNoRows { // No rows found, not an error in this context @@ -206,14 +209,14 @@ func FetchNormalDepthFlowStage(db *sql.DB, reachID int, flow float32) (ScenarioR func FetchNearestFlowStage(db *sql.DB, reachID int, flow, controlStage float32) (ScenarioRecord, error) { row := db.QueryRow(` - SELECT us_flow, us_wse, ds_wse, boundary_condition, map_exists + SELECT us_flow, us_wse, ds_wse, boundary_condition, map_exists, COALESCE(fim_path, '') FROM scenarios WHERE reach_id = ? ORDER BY ABS(us_flow - ? ), ABS(ds_wse - ?) LIMIT 1; `, reachID, flow, controlStage) var scenario ScenarioRecord - if err := row.Scan(&scenario.Flow, &scenario.Stage, &scenario.ControlReachStage, &scenario.BoundaryCondition, &scenario.MapExists); err != nil { + if err := row.Scan(&scenario.Flow, &scenario.Stage, &scenario.ControlReachStage, &scenario.BoundaryCondition, &scenario.MapExists, &scenario.FimPath); err != nil { // Check if the error is because of no rows if err == sql.ErrNoRows { // No rows found, not an error in this context @@ -288,7 +291,7 @@ func TraverseUpstream(db *sql.DB, flows map[int]float32, startReaches []ControlD } } - result := ResultRecord{ReachID: scenario.ReachID, Flow: scenario.Flow, MapExists: scenario.MapExists} + result := ResultRecord{ReachID: scenario.ReachID, Flow: scenario.Flow, MapExists: scenario.MapExists, FimPath: scenario.FimPath} if scenario.BoundaryCondition == "nd" { result.ControlReachStageStr = "nd" } else { @@ -302,8 +305,8 @@ func TraverseUpstream(db *sql.DB, flows map[int]float32, startReaches []ControlD return results, nil } -// WriteCSV writes the control table. The map_exists column is appended last so -// that positional readers of the first three columns are unaffected. +// WriteCSV writes the control table. The map_exists and fim_path columns are +// appended last so that positional readers of the first three columns are unaffected. func WriteCSV(data []ResultRecord, filePath string) error { file, err := os.Create(filePath) if err != nil { @@ -314,16 +317,16 @@ func WriteCSV(data []ResultRecord, filePath string) error { writer := csv.NewWriter(file) defer writer.Flush() - if err := writer.Write([]string{"reach_id", "flow", "control_stage", "map_exists"}); err != nil { + if err := writer.Write([]string{"reach_id", "flow", "control_stage", "map_exists", "fim_path"}); err != nil { return err } for _, d := range data { - mapExists := "0" + mapExists, fimPath := "0", "" if d.MapExists { - mapExists = "1" + mapExists, fimPath = "1", d.FimPath } - record := []string{strconv.Itoa(d.ReachID), fmt.Sprint(d.Flow), d.ControlReachStageStr, mapExists} + record := []string{strconv.Itoa(d.ReachID), fmt.Sprint(d.Flow), d.ControlReachStageStr, mapExists, fimPath} if err := writer.Write(record); err != nil { return err } @@ -422,6 +425,9 @@ func Run(args []string) (err error) { if err := utils.CheckScenariosTable(db); err != nil { return err } + if err := utils.CheckFimPathColumn(db); err != nil { + return err + } results, err := TraverseUpstream(db, flows, startReaches) if err != nil { diff --git a/cmd/fim/fim.go b/cmd/fim/fim.go index cf95a73..8715087 100644 --- a/cmd/fim/fim.go +++ b/cmd/fim/fim.go @@ -16,27 +16,17 @@ var usage string = `Usage of fim: Given a control table and a fim library folder, create a composite flood inundation map for the control conditions. GDAL VSI paths can be used (only for library and not for output), given GDAL must have access to cloud creds. +FIM file paths are read from the control table's 'fim_path' column (as written by 'flows2fim controls'), +resolved relative to the library folder. Absolute and GDAL VSI paths in that column are used as-is. + FIM Library Specifications: - All maps should have same CRS, Resolution, data type, vertical units (if any), and nodata value -- Should have following folder structure: +- If -with_domain is used, each reach folder must hold a domain.tif: . ├── 2821866 -│ ├── z_nd -│ │ ├── f_10283.tif -│ │ ├── f_104569.tif -│ │ ├── f_11199.tif -│ │ ├── f_112807.tif -│ │ ... -│ ├── z_53_5 -│ │ ├── f_102921.tif -│ │ ├── f_10485.tif -│ │ ├── f_111159.tif -│ │ ├── f_11309.tif -│ │ ... -│ ... -│ └── domain.tif (optional) +│ ├── ... (FIM files at the paths listed in the control table) +│ └── domain.tif ├── 2821867 -│ ├── z_nd ... Arguments:` // Usage should be always followed by PrintDefaults() @@ -127,15 +117,20 @@ func Run(args []string) (gdalArgs []string, err error) { return []string{}, fmt.Errorf("not enough columns in controls file, need at least 3") } - // Locate map_exists by header name rather than position, so additional + // Locate map_exists and fim_path by header name rather than position, so additional // columns can be added to the controls file later without breaking this. - mapExistsIdx := -1 + mapExistsIdx, fimPathIdx := -1, -1 for i, name := range records[0] { - if strings.TrimSpace(name) == "map_exists" { + switch strings.TrimSpace(name) { + case "map_exists": mapExistsIdx = i - break + case "fim_path": + fimPathIdx = i } } + if fimPathIdx == -1 { + return []string{}, fmt.Errorf("controls file has no fim_path column; regenerate it with 'flows2fim controls' from a database that has a fim_path column") + } if mapExistsIdx == -1 { slog.Warn("controls file has no map_exists column; assuming every record has a map", "file", controlsFile) } @@ -156,17 +151,27 @@ func Run(args []string) (gdalArgs []string, err error) { } } - record[2] = strings.Replace(record[2], ".", "_", -1) // Replace '.' with '_' - folderPath := filepath.Join(absFimLibPath, reachID, fmt.Sprintf("z_%s", record[2])) - fileName := fmt.Sprintf("f_%s.tif", record[1]) - absFIMPath := filepath.Join(folderPath, fileName) + var fimPath string + if fimPathIdx < len(record) { + fimPath = strings.TrimSpace(record[fimPathIdx]) + } + if fimPath == "" { + return []string{}, fmt.Errorf("record for reach %s (flow %s, control stage %s) has a map but no fim_path", reachID, record[1], record[2]) + } + + absFIMPath := fimPath + if !strings.HasPrefix(fimPath, "/vsi") && !filepath.IsAbs(fimPath) { + absFIMPath = filepath.Join(absFimLibPath, fimPath) + } absDomainPath := filepath.Join(absFimLibPath, reachID, "domain.tif") // join on windows may cause \vsi if strings.HasPrefix(absFIMPath, `\vsi`) { - absDomainPath = strings.ReplaceAll(absDomainPath, `\`, "/") absFIMPath = strings.ReplaceAll(absFIMPath, `\`, "/") } + if strings.HasPrefix(absDomainPath, `\vsi`) { + absDomainPath = strings.ReplaceAll(absDomainPath, `\`, "/") + } fimFiles = append(fimFiles, absFIMPath) if withDomain { diff --git a/cmd/validate/validate.go b/cmd/validate/validate.go index 1b6f441..55a835f 100644 --- a/cmd/validate/validate.go +++ b/cmd/validate/validate.go @@ -12,7 +12,6 @@ import ( "os" "os/exec" "path/filepath" - "strconv" "strings" "sync" @@ -25,28 +24,17 @@ validate there is one to one correspondence between the entries of scenarios tab GDAL VSI paths can be used, given GDAL must have access to cloud creds. Intermediate folders for output files are created if they do not exist. -Correspondence is judged against map_exists, which records whether a depth grid was -written for the scenario. Three disagreements are reported, each to its own CSV: - o_fims scenarios with map_exists=1 that have no FIM file - o_scenarios FIM files with no scenario record at all - o_unexpected_fims FIM files whose scenario says map_exists=0 +Correspondence is judged by matching each scenario's fim_path against the .tif files found +in the library. Only scenarios with map_exists=1 carry a fim_path. Two disagreements are +reported, each to its own CSV: + o_fims scenarios with map_exists=1 whose fim_path has no file (or is empty) + o_scenarios FIM files whose path is not the fim_path of any scenario Scenarios with map_exists=0 and no FIM file are the expected case and are not reported. FIM Library Specifications: - All maps should have same CRS, Resolution, vertical units (if any), and nodata value -- Should have following folder structure: -. -├── 2821866 -│ ├── z_nd -│ │ ├── f_10283.tif -│ │ ├── f_104569.tif -│ │ ├── f_11199.tif -│ │ ├── f_112807.tif -│ ├── z_53_5 -│ ├── f_102921.tif -│ ├── f_10485.tif -│ ├── f_111159.tif -│ ├── f_11309.tif +- fim_path values are relative to the library root and use '/' as separator +- /domain.tif files are reach domains, not FIMs, and are ignored Database file must have a table 'scenarios' and contain following columns reach_id INTEGER @@ -57,6 +45,7 @@ Database file must have a table 'scenarios' and contain following columns ds_wse REAL boundary_condition TEXT CHECK(boundary_condition IN ('nd','kwse')) map_exists BOOL CHECK(map_exists IN (0, 1)) + fim_path TEXT UNIQUE(reach_id, us_flow, ds_wse, boundary_condition) @@ -66,10 +55,7 @@ Arguments:` const ( queryCreateFIMEntTable = ` CREATE TABLE memdb.fim_entries ( - reach_id INTEGER, - us_flow INTEGER, - ds_wse REAL, - boundary_condition TEXT + fim_path TEXT PRIMARY KEY ); ` @@ -78,85 +64,38 @@ const ( SELECT s.reach_id, s.us_flow, - s.ds_wse, - s.boundary_condition + printf('%.1f', s.ds_wse) AS ds_wse, + s.boundary_condition, + COALESCE(s.fim_path, '') AS fim_path FROM scenarios s LEFT JOIN memdb.fim_entries f - ON (s.reach_id = f.reach_id - AND s.us_flow = f.us_flow - AND (CASE - WHEN s.boundary_condition = 'nd' THEN 0 - ELSE s.ds_wse - END) = f.ds_wse - AND s.boundary_condition = f.boundary_condition) + ON s.fim_path = f.fim_path WHERE - f.reach_id IS NULL + f.fim_path IS NULL AND s.map_exists = 1 ORDER BY s.reach_id, s.boundary_condition, s.ds_wse, s.us_flow; ` - // The mirror of the above: a raster is present even though the database says - // no depth grid was written for that scenario. - queryUnexpectedFims = ` - SELECT - f.reach_id, - f.us_flow, - f.ds_wse, - f.boundary_condition - FROM - memdb.fim_entries f - JOIN - scenarios s - ON (f.reach_id = s.reach_id - AND f.us_flow = s.us_flow - AND f.ds_wse = (CASE - WHEN f.boundary_condition = 'nd' THEN 0 - ELSE s.ds_wse - END) - AND f.boundary_condition = s.boundary_condition) - WHERE - s.map_exists = 0 - ORDER BY - f.reach_id, f.boundary_condition, f.ds_wse, f.us_flow; - ` - queryMissingScenarios = ` SELECT - f.reach_id, - f.us_flow, - f.ds_wse, - f.boundary_condition + f.fim_path FROM memdb.fim_entries f LEFT JOIN scenarios s - ON (f.reach_id = s.reach_id - AND f.us_flow = s.us_flow - AND f.ds_wse = (CASE - WHEN f.boundary_condition = 'nd' THEN 0 - ELSE s.ds_wse - END) - AND f.boundary_condition = s.boundary_condition) + ON s.fim_path = f.fim_path WHERE - s.reach_id IS NULL + s.fim_path IS NULL ORDER BY - f.reach_id, f.boundary_condition, f.ds_wse, f.us_flow; + f.fim_path; ` ) var extIgnore = []string{".aux", ".aux.xml", ".ovr", ".xml", ".tfw"} -// fimRow represents a single record discovered in the FIM library -type fimRow struct { - reachID int - usFlow int - dsWse float64 - boundaryCondition string -} - // dirEntry holds a path + info about whether it's a directory type dirEntry struct { path string @@ -247,9 +186,8 @@ func gatherVSIEntries(dir string, recursive bool) ([]dirEntry, error) { return results, nil } -// processLibEntry parse boundary condition folders (z_XXX) and flow tif files (f_*.tif) from dirEntry path. -// It sends the parsed data to fimChan channel -func processLibEntry(e dirEntry, absFimLibDir string, fimChan chan<- fimRow) { +// processLibEntry sends the library-relative, '/'-separated path of a FIM file to fimChan. +func processLibEntry(e dirEntry, absFimLibDir string, fimChan chan<- string) { // Skip directories if e.isDir { return @@ -260,10 +198,8 @@ func processLibEntry(e dirEntry, absFimLibDir string, fimChan chan<- fimRow) { slog.Error("Relative path resolution failed", "path", e.path, "error", relErr) return } - // On windows relPath will have backslashes, convert to forward slashes for /vsi paths - if strings.HasPrefix(absFimLibDir, "/vsi") { - relPath = filepath.ToSlash(relPath) - } + // fim_path values in the database always use forward slashes + relPath = filepath.ToSlash(relPath) name := filepath.Base(e.path) ext := filepath.Ext(name) @@ -274,62 +210,15 @@ func processLibEntry(e dirEntry, absFimLibDir string, fimChan chan<- fimRow) { return } - if !strings.HasPrefix(name, "f_") { - slog.Warn("Invalid file prefix", "path", relPath) - return - } - - usFlowStr := strings.TrimSuffix(strings.TrimPrefix(name, "f_"), ".tif") - usFlow, convErr := strconv.Atoi(usFlowStr) - if convErr != nil { - slog.Error("Invalid flow value", "path", relPath, "value", usFlowStr) - return - } - - parts := strings.Split(relPath, string(os.PathSeparator)) - if len(parts) != 3 { - slog.Error("Invalid path structure", "path", relPath) - return - } - reachIDStr := parts[0] - dirName := parts[1] // z_XXX - reachID, err := strconv.Atoi(reachIDStr) - if err != nil { - slog.Error("Invalid reach ID", "path", relPath, "value", parts[0]) - return - } - if !strings.HasPrefix(dirName, "z_") { - slog.Error("Invalid boundary condition", "path", relPath) + if name == "domain.tif" && strings.Count(relPath, "/") == 1 { return } - dirSuffix := strings.TrimPrefix(dirName, "z_") - - var bc string - var dsWse float64 - if dirSuffix == "nd" { - bc = "nd" - dsWse = 0.0 - } else { - bc = "kwse" - dsWseStr := strings.ReplaceAll(dirSuffix, "_", ".") - dsWseFloat, parseErr := strconv.ParseFloat(dsWseStr, 64) - if parseErr != nil { - slog.Error("Invalid downstream WSE", "path", relPath) - return - } - dsWse = dsWseFloat - } - fimChan <- fimRow{ - reachID: reachID, - usFlow: usFlow, - dsWse: dsWse, - boundaryCondition: bc, - } + fimChan <- relPath } -// batchInsertFIMs insert FIM rows in batches -func batchInsertFIMs(db *sql.DB, fimChan <-chan fimRow) error { +// batchInsertFIMs insert FIM paths in batches +func batchInsertFIMs(db *sql.DB, fimChan <-chan string) error { tx, err := db.Begin() if err != nil { return err @@ -342,7 +231,7 @@ func batchInsertFIMs(db *sql.DB, fimChan <-chan fimRow) error { }() const batchSize = 1000 - batch := make([]fimRow, 0, batchSize) + batch := make([]string, 0, batchSize) commitBatch := func() error { if len(batch) == 0 { @@ -351,14 +240,14 @@ func batchInsertFIMs(db *sql.DB, fimChan <-chan fimRow) error { // Can't do prepared statement as the final batch would not be of same size // Build single statement for multi-VALUES insert - // INSERT INTO memdb.fim_entries(reach_id, us_flow, ds_wse, boundary_condition) VALUES (?, ?, ?, ?), (?, ?, ?, ?) ... - sqlStr := "INSERT INTO memdb.fim_entries(reach_id, us_flow, ds_wse, boundary_condition) VALUES " - vals := make([]interface{}, 0, len(batch)*4) + // INSERT OR IGNORE INTO memdb.fim_entries(fim_path) VALUES (?), (?) ... + sqlStr := "INSERT OR IGNORE INTO memdb.fim_entries(fim_path) VALUES " + vals := make([]interface{}, 0, len(batch)) placeholders := make([]string, 0, len(batch)) - for _, r := range batch { - placeholders = append(placeholders, "(?,?,?,?)") - vals = append(vals, r.reachID, r.usFlow, r.dsWse, r.boundaryCondition) + for _, p := range batch { + placeholders = append(placeholders, "(?)") + vals = append(vals, p) } sqlStr += strings.Join(placeholders, ",") @@ -397,8 +286,14 @@ func writeCSV(rows *sql.Rows, outFile string, skipEmpty bool) (int, error) { // An approach that was first adopted and discarded was to write to a temp file in tmp folder and then rename it only if rows exist, // but that approach cause inter-device rename error on conatinerized environment with file mounts. - rowCount := 0 - if !rows.Next() { + // Columns must be read before Next, which closes rows when there are none + cols, err := rows.Columns() + if err != nil { + return 0, fmt.Errorf("error reading columns: %v", err) + } + + hasRow := rows.Next() + if !hasRow { // No row was found if rows.Err() == nil. if err := rows.Err(); err != nil { return 0, fmt.Errorf("error reading rows: %v", err) @@ -406,8 +301,6 @@ func writeCSV(rows *sql.Rows, outFile string, skipEmpty bool) (int, error) { if skipEmpty { return 0, nil } - } else { - rowCount++ } // Create intermediate directories if they do not exist @@ -428,48 +321,33 @@ func writeCSV(rows *sql.Rows, outFile string, skipEmpty bool) (int, error) { }() w := csv.NewWriter(tempFile) - if err := w.Write([]string{"reach_id", "us_flow", "ds_wse", "boundary_condition"}); err != nil { + if err := w.Write(cols); err != nil { return 0, fmt.Errorf("error writing CSV header: %v", err) } - if rowCount != 0 { - var reachID, usFlow int - var dsWse float64 - var bc string - if err := rows.Scan(&reachID, &usFlow, &dsWse, &bc); err != nil { - return 0, fmt.Errorf("error scanning first row: %v", err) - } + vals := make([]sql.NullString, len(cols)) + ptrs := make([]interface{}, len(cols)) + for i := range vals { + ptrs[i] = &vals[i] + } + record := make([]string, len(cols)) - if err := w.Write([]string{ - strconv.Itoa(reachID), - strconv.Itoa(usFlow), - fmt.Sprintf("%.1f", dsWse), - bc, - }); err != nil { - return 0, fmt.Errorf("error writing first row to CSV: %v", err) + rowCount := 0 + // rows is already positioned on the first row, if there is one + for ; hasRow; hasRow = rows.Next() { + if err := rows.Scan(ptrs...); err != nil { + return 0, fmt.Errorf("error scanning row: %v", err) } - - // Process remaining rows - for rows.Next() { - var reachID, usFlow int - var dsWse float64 - var bc string - if err := rows.Scan(&reachID, &usFlow, &dsWse, &bc); err != nil { - return 0, fmt.Errorf("error scanning row: %v", err) - } - if err := w.Write([]string{ - strconv.Itoa(reachID), - strconv.Itoa(usFlow), - fmt.Sprintf("%.1f", dsWse), - bc, - }); err != nil { - return 0, fmt.Errorf("error writing CSV record: %v", err) - } - rowCount++ + for i, v := range vals { + record[i] = v.String } - if err := rows.Err(); err != nil { - return 0, fmt.Errorf("error from rows iteration: %v", err) + if err := w.Write(record); err != nil { + return 0, fmt.Errorf("error writing CSV record: %v", err) } + rowCount++ + } + if err := rows.Err(); err != nil { + return 0, fmt.Errorf("error from rows iteration: %v", err) } w.Flush() @@ -495,20 +373,18 @@ func Run(args []string) error { } var ( - dbPath string - fimLibDir string - outFims string - outScenarios string - outUnexpectedFims string - concurrent int - skipEmpty bool + dbPath string + fimLibDir string + outFims string + outScenarios string + concurrent int + skipEmpty bool ) flags.StringVar(&dbPath, "db", "", "Path to the scenarios database file") flags.StringVar(&fimLibDir, "lib", "", "Path to the FIM library directory") flags.StringVar(&outFims, "o_fims", "missing_fims.csv", "Output CSV for scenario entries with map_exists=1 missing corresponding FIM files") flags.StringVar(&outScenarios, "o_scenarios", "missing_scenarios.csv", "Output CSV for FIM entries missing corresponding scenario records") - flags.StringVar(&outUnexpectedFims, "o_unexpected_fims", "unexpected_fims.csv", "Output CSV for FIM entries whose scenario records have map_exists=0") flags.IntVar(&concurrent, "cc", 25, "Concurrent Count, number of top-level reach directories to process concurrently (default 25)") flags.BoolVar(&skipEmpty, "skip_empty", false, "If true, do not create an empty output CSV file") @@ -543,6 +419,9 @@ func Run(args []string) error { if err := utils.CheckScenariosTable(db); err != nil { return err } + if err := utils.CheckFimPathColumn(db); err != nil { + return err + } // 2) Attach an in-memory DB for fim_entries _, err = db.Exec(`ATTACH ':memory:' AS memdb;`) @@ -568,7 +447,7 @@ func Run(args []string) error { } // 3) Setup concurrency - fimChan := make(chan fimRow, 2000) // buffer for discovered rows + fimChan := make(chan string, 2000) // buffer for discovered FIM paths var batchWG sync.WaitGroup // Single writer goroutine that batch-inserts rows into memdb.fim_entries @@ -607,22 +486,24 @@ func Run(args []string) error { var reachDir string for _, de := range libEntries { - if de.isDir { - wg.Add(1) - sem <- struct{}{} // Acquire concurrency token - go func(reachDir string) { - defer wg.Done() - defer func() { <-sem }() // Release token - reachEntries, err := readDir(de.path, true) - if err != nil { - slog.Warn("Reach directory read error", "path", de.path, "error", err) - return - } - for _, e := range reachEntries { - processLibEntry(e, absFimLibDir, fimChan) - } - }(reachDir) + if !de.isDir { + processLibEntry(de, absFimLibDir, fimChan) + continue } + wg.Add(1) + sem <- struct{}{} // Acquire concurrency token + go func(reachDir string) { + defer wg.Done() + defer func() { <-sem }() // Release token + reachEntries, err := readDir(de.path, true) + if err != nil { + slog.Warn("Reach directory read error", "path", de.path, "error", err) + return + } + for _, e := range reachEntries { + processLibEntry(e, absFimLibDir, fimChan) + } + }(reachDir) } // Wait for all reach processing goroutines to finish @@ -638,7 +519,6 @@ func Run(args []string) error { }{ {outFims, queryMissingFims, "missing FIMs"}, {outScenarios, queryMissingScenarios, "missing scenarios"}, - {outUnexpectedFims, queryUnexpectedFims, "unexpected FIMs"}, } for _, task := range tasks { rows, err := db.Query(task.query) diff --git a/pkg/utils/db.go b/pkg/utils/db.go index 6e3883d..aedbacd 100644 --- a/pkg/utils/db.go +++ b/pkg/utils/db.go @@ -89,3 +89,32 @@ func CheckScenariosTable(db *sql.DB) error { return nil } + +// FimPathColumn holds each scenario's FIM file path, relative to the FIM library root. +const FimPathColumn = "fim_path" + +// CheckFimPathColumn verifies ScenariosTable has FimPathColumn, and returns an +// actionable error, with SQL that backfills it from the legacy folder layout, when it does not. +func CheckFimPathColumn(db *sql.DB) error { + var count int + err := db.QueryRow(`SELECT COUNT(*) FROM pragma_table_info(?) WHERE name = ?;`, ScenariosTable, FimPathColumn).Scan(&count) + if err != nil { + return fmt.Errorf("error inspecting '%s' columns: %w", ScenariosTable, err) + } + if count > 0 { + return nil + } + + return fmt.Errorf( + "database table '%[1]s' has no '%[2]s' column. FIM file paths are now read from the database "+ + "instead of being derived from the library folder layout. "+ + "Regenerate the database with a matching ripple1d-pipeline release, or add it in place for a library using the "+ + "/z_/f_.tif layout with\n"+ + " ALTER TABLE %[1]s ADD COLUMN %[2]s TEXT;\n"+ + " UPDATE %[1]s SET %[2]s = reach_id || '/z_' ||\n"+ + " CASE boundary_condition WHEN 'nd' THEN 'nd' ELSE replace(printf('%%.1f', ds_wse), '.', '_') END ||\n"+ + " '/f_' || us_flow || '.tif'\n"+ + " WHERE map_exists = 1;", + ScenariosTable, FimPathColumn, + ) +} diff --git a/scripts/test_suite_linux.sh b/scripts/test_suite_linux.sh index add5ddf..1c6644c 100755 --- a/scripts/test_suite_linux.sh +++ b/scripts/test_suite_linux.sh @@ -276,7 +276,7 @@ controls_test_cases() { # Remove temp file rm "$tempfile" # Assign error string - assert_file_output="reach_id,flow,control_stage,map_exists" + assert_file_output="reach_id,flow,control_stage,map_exists,fim_path" # Compare Error messaging and print if [ "$file_contents" = "$assert_file_output" ]; then printf "\t \u2714 Passed: Output file created and empty. \n\n" @@ -364,11 +364,11 @@ controls_test_cases() { -f "$temp_flows" \ -o "$temp_out" \ -scsv $start_reaches_dir/start_reaches.csv &> /dev/null - # The mapless scenario should be the one picked, flagged with a trailing 0 - if grep -q "^24274741,70000,nd,0$" "$temp_out"; then + # The mapless scenario should be the one picked, flagged with map_exists=0 and an empty fim_path + if grep -q "^24274741,70000,nd,0,$" "$temp_out"; then printf "\t ✔ Passed: mapless scenario selected and flagged map_exists=0. \n\n" else - printf "\t ❌ Failed: expected 24274741,70000,nd,0 in controls file, got: $(tail -n 1 "$temp_out") \n\n" + printf "\t ❌ Failed: expected 24274741,70000,nd,0, in controls file, got: $(tail -n 1 "$temp_out") \n\n" failed_controls_testcases=$((failed_controls_testcases + 1)) fi # Remove temp files @@ -380,7 +380,7 @@ controls_test_cases() { } fim_test_cases() { - local num_test_cases_fim=10 + local num_test_cases_fim=11 local failed_fim_testcases=0 total_count=$(( total_count + num_test_cases_fim)) # If previous directories exists, remove them @@ -539,7 +539,7 @@ fim_test_cases() { temp_controls=$(mktemp) # Only record is flagged as having no map, so there is nothing left to # composite. fim must say so rather than handing GDAL a missing raster. - printf "reach_id,flow,control_stage,map_exists\n24274741,17668,nd,0\n" > "$temp_controls" + printf "reach_id,flow,control_stage,map_exists,fim_path\n24274741,17668,nd,0,\n" > "$temp_controls" # Test case $cmd fim \ -c "$temp_controls" \ @@ -563,9 +563,9 @@ fim_test_cases() { # Create and assign temp files tempfile=$(mktemp) temp_controls=$(mktemp) - # Controls files written before 0.5.0 have no map_exists column. They must - # keep working, with a warning that map existence is unknown. - printf "reach_id,flow,control_stage\n24274741,17668,nd\n" > "$temp_controls" + # A controls file without a map_exists column must keep working, with a + # warning that map existence is unknown. + printf "reach_id,flow,control_stage,fim_path\n24274741,17668,nd,24274741/z_nd/f_17668.tif\n" > "$temp_controls" # Test case $cmd fim \ -c "$temp_controls" \ @@ -585,7 +585,33 @@ fim_test_cases() { rm "$tempfile" rm "$temp_controls" - #printf "(11/${num_test_cases_fim})\t>>>> Test flows2fim fim pull from S3 <<<<\n\n" + printf "(11/${num_test_cases_fim})\t>>>> Controls file without a fim_path column is rejected <<<<\n\n" + # Create and assign temp files + tempfile=$(mktemp) + temp_controls=$(mktemp) + # FIM paths are no longer derived from the folder layout, so a controls + # file without them cannot be composited. + printf "reach_id,flow,control_stage,map_exists\n24274741,17668,nd,1\n" > "$temp_controls" + # Test case + $cmd fim \ + -c "$temp_controls" \ + -fmt $format \ + -lib $library_benchmark \ + -o $fim_test_outputs/fim_no_fim_path.tif &> "$tempfile" + # Assign error string + expected_error_substring="controls file has no fim_path column" + # Search (grep) for expected error substring in temporary output file + if grep -q "$expected_error_substring" "$tempfile"; then + printf "\t ✔ Correct error thrown. \n\n" + else + printf "\t ❌ Error messaging inconsistent \n\n" + failed_fim_testcases=$((failed_fim_testcases + 1)) + fi + # Remove temp files + rm "$tempfile" + rm "$temp_controls" + + #printf "(12/${num_test_cases_fim})\t>>>> Test flows2fim fim pull from S3 <<<<\n\n" fim_passed=$((num_test_cases_fim - failed_fim_testcases)) total_passed=$(( total_passed + fim_passed)) diff --git a/scripts/test_suite_mac.sh b/scripts/test_suite_mac.sh index 657304d..35010c5 100755 --- a/scripts/test_suite_mac.sh +++ b/scripts/test_suite_mac.sh @@ -276,7 +276,7 @@ controls_test_cases() { # Remove temp file rm "$tempfile" # Assign error string - assert_file_output="reach_id,flow,control_stage,map_exists" + assert_file_output="reach_id,flow,control_stage,map_exists,fim_path" # Compare Error messaging and print if [ "$file_contents" = "$assert_file_output" ]; then printf "\t \u2714 Passed: Output file created and empty. \n\n" @@ -364,11 +364,11 @@ controls_test_cases() { -f "$temp_flows" \ -o "$temp_out" \ -scsv $start_reaches_dir/start_reaches.csv &> /dev/null - # The mapless scenario should be the one picked, flagged with a trailing 0 - if grep -q "^24274741,70000,nd,0$" "$temp_out"; then + # The mapless scenario should be the one picked, flagged with map_exists=0 and an empty fim_path + if grep -q "^24274741,70000,nd,0,$" "$temp_out"; then printf "\t ✔ Passed: mapless scenario selected and flagged map_exists=0. \n\n" else - printf "\t ❌ Failed: expected 24274741,70000,nd,0 in controls file, got: $(tail -n 1 "$temp_out") \n\n" + printf "\t ❌ Failed: expected 24274741,70000,nd,0, in controls file, got: $(tail -n 1 "$temp_out") \n\n" failed_controls_testcases=$((failed_controls_testcases + 1)) fi # Remove temp files @@ -380,7 +380,7 @@ controls_test_cases() { } fim_test_cases() { - local num_test_cases_fim=10 + local num_test_cases_fim=11 local failed_fim_testcases=0 total_count=$(( total_count + num_test_cases_fim)) # If previous directories exists, remove them @@ -539,7 +539,7 @@ fim_test_cases() { temp_controls=$(mktemp) # Only record is flagged as having no map, so there is nothing left to # composite. fim must say so rather than handing GDAL a missing raster. - printf "reach_id,flow,control_stage,map_exists\n24274741,17668,nd,0\n" > "$temp_controls" + printf "reach_id,flow,control_stage,map_exists,fim_path\n24274741,17668,nd,0,\n" > "$temp_controls" # Test case $cmd fim \ -c "$temp_controls" \ @@ -563,9 +563,9 @@ fim_test_cases() { # Create and assign temp files tempfile=$(mktemp) temp_controls=$(mktemp) - # Controls files written before 0.5.0 have no map_exists column. They must - # keep working, with a warning that map existence is unknown. - printf "reach_id,flow,control_stage\n24274741,17668,nd\n" > "$temp_controls" + # A controls file without a map_exists column must keep working, with a + # warning that map existence is unknown. + printf "reach_id,flow,control_stage,fim_path\n24274741,17668,nd,24274741/z_nd/f_17668.tif\n" > "$temp_controls" # Test case $cmd fim \ -c "$temp_controls" \ @@ -585,7 +585,33 @@ fim_test_cases() { rm "$tempfile" rm "$temp_controls" - #printf "(11/${num_test_cases_fim})\t>>>> Test flows2fim fim pull from S3 <<<<\n\n" + printf "(11/${num_test_cases_fim})\t>>>> Controls file without a fim_path column is rejected <<<<\n\n" + # Create and assign temp files + tempfile=$(mktemp) + temp_controls=$(mktemp) + # FIM paths are no longer derived from the folder layout, so a controls + # file without them cannot be composited. + printf "reach_id,flow,control_stage,map_exists\n24274741,17668,nd,1\n" > "$temp_controls" + # Test case + $cmd fim \ + -c "$temp_controls" \ + -fmt $format \ + -lib $library_benchmark \ + -o $fim_test_outputs/fim_no_fim_path.tif &> "$tempfile" + # Assign error string + expected_error_substring="controls file has no fim_path column" + # Search (grep) for expected error substring in temporary output file + if grep -q "$expected_error_substring" "$tempfile"; then + printf "\t ✔ Correct error thrown. \n\n" + else + printf "\t ❌ Error messaging inconsistent \n\n" + failed_fim_testcases=$((failed_fim_testcases + 1)) + fi + # Remove temp files + rm "$tempfile" + rm "$temp_controls" + + #printf "(12/${num_test_cases_fim})\t>>>> Test flows2fim fim pull from S3 <<<<\n\n" fim_passed=$((num_test_cases_fim - failed_fim_testcases)) total_passed=$(( total_passed + fim_passed)) diff --git a/testdata/reference_data/controls/controls_100year.csv b/testdata/reference_data/controls/controls_100year.csv index 86e2784..cf739f2 100644 --- a/testdata/reference_data/controls/controls_100year.csv +++ b/testdata/reference_data/controls/controls_100year.csv @@ -1,2 +1,2 @@ -reach_id,flow,control_stage,map_exists -24274741,63346,nd,1 +reach_id,flow,control_stage,map_exists,fim_path +24274741,63346,nd,1,24274741/z_nd/f_63346.tif diff --git a/testdata/reference_data/controls/controls_10year.csv b/testdata/reference_data/controls/controls_10year.csv index b28eeff..f7f5812 100644 --- a/testdata/reference_data/controls/controls_10year.csv +++ b/testdata/reference_data/controls/controls_10year.csv @@ -1,2 +1,2 @@ -reach_id,flow,control_stage,map_exists -24274741,33211,nd,1 +reach_id,flow,control_stage,map_exists,fim_path +24274741,33211,nd,1,24274741/z_nd/f_33211.tif diff --git a/testdata/reference_data/controls/controls_25year.csv b/testdata/reference_data/controls/controls_25year.csv index d8ace63..4d653d7 100644 --- a/testdata/reference_data/controls/controls_25year.csv +++ b/testdata/reference_data/controls/controls_25year.csv @@ -1,2 +1,2 @@ -reach_id,flow,control_stage,map_exists -24274741,43213,nd,1 +reach_id,flow,control_stage,map_exists,fim_path +24274741,43213,nd,1,24274741/z_nd/f_43213.tif diff --git a/testdata/reference_data/controls/controls_2year.csv b/testdata/reference_data/controls/controls_2year.csv index 32ae9a1..89c0f16 100644 --- a/testdata/reference_data/controls/controls_2year.csv +++ b/testdata/reference_data/controls/controls_2year.csv @@ -1,2 +1,2 @@ -reach_id,flow,control_stage,map_exists -24274741,17668,nd,1 +reach_id,flow,control_stage,map_exists,fim_path +24274741,17668,nd,1,24274741/z_nd/f_17668.tif diff --git a/testdata/reference_data/controls/controls_50year.csv b/testdata/reference_data/controls/controls_50year.csv index 8b479f0..af24389 100644 --- a/testdata/reference_data/controls/controls_50year.csv +++ b/testdata/reference_data/controls/controls_50year.csv @@ -1,2 +1,2 @@ -reach_id,flow,control_stage,map_exists -24274741,48810,nd,1 +reach_id,flow,control_stage,map_exists,fim_path +24274741,48810,nd,1,24274741/z_nd/f_48810.tif diff --git a/testdata/reference_data/controls/controls_5year.csv b/testdata/reference_data/controls/controls_5year.csv index 0dcf35f..26ea60d 100644 --- a/testdata/reference_data/controls/controls_5year.csv +++ b/testdata/reference_data/controls/controls_5year.csv @@ -1,2 +1,2 @@ -reach_id,flow,control_stage,map_exists -24274741,24894,nd,1 +reach_id,flow,control_stage,map_exists,fim_path +24274741,24894,nd,1,24274741/z_nd/f_24894.tif diff --git a/testdata/reference_data/db/ripple.gpkg b/testdata/reference_data/db/ripple.gpkg index 930ad29893374735d4f516880b56b57ed21b0915..1e0c0e7bc5135f04d0c241f1c9796f0210f5d088 100644 GIT binary patch delta 789 zcmZvZ&1(};5WwF@lHJXIWucIY(9{ZIX-Rh9e&n?X*-MRrJt$c4AW>Mh?ZMDWNsHuA zQ4hKKfdmigRRmEeHizPFPM%Z{J$dk-@UFi1nm|c1!`yz%%x`ADukH7>$EPg|06`h?~#kM7G@+ve=Fp6B}_ z8Cf#CyY544>lAzr+s%lbOko8;UE@kaeRYz4;JRKQMys41>Mt|>_0iFR?Nc$jrW~`7 z>+{(ld0Pl~|HeFK!q$PyqZy4h@F;aegH?I3n7Ch(p!u=;c`k9pUsiJ`*)!<`)@7{2 zC04SGMMH@-cIz7>?;6>i~P8{?;01TJ(73a0soH_KA7RDvC)=R^DsDzowCoyOMf eosI7IIx2Qe6?bw@C0AT2J=<2Nd!JRjJoO(Q6uCqI delta 532 zcmZoTz|(MmXM(h#00RSq5fJMGF%uALP1G@F6=2XS>uyYGO<~-c!ZdHa{9OiK-t`Q; z>-jJ7ujVi3_u?1jd(O9subVH1PmlLM?{>ulOo_af+Pqv0EX?fe9PEM|j790DOm#vI z%4a~}tV6%OF$louyc8WqpkgL=c6KH~&grZ6GAZc+E$0AfmjD~X$i%?}F@{mFK$3AP zV@+O)E)T>4upZHUOv*eu+#nG~pfNyG;`T8q@#u0PscGKBq-?Cp2~oocR0DDr!@gAl z4(0#SKtRr+Y%|zlc`3#mP>mp?p6_GQB%h2g45-> znLD@FFf&hLWn!8$U4WC>V)`iuCSDfq+f2dRZ#yvAGl_BEW)SdIXJp9COG&LLF3d61 TXA^gopIpc-vHch~vy}({lN@g_ From dc4bda056c83b644b9482cefab879b6a64b3e200 Mon Sep 17 00:00:00 2001 From: sclaw Date: Tue, 6 Oct 2026 15:00:32 -0400 Subject: [PATCH 2/3] feat: add iac for ECR repo --- .dockerignore | 11 ++ .github/workflows/ecr-image.yml | 40 +++++ .github/workflows/infrastructure.yml | 65 ++++++++ .gitignore | 8 + infrastructure/app.py | 42 +++++ infrastructure/cdk.json | 3 + infrastructure/pyproject.toml | 14 ++ infrastructure/uv.lock | 224 +++++++++++++++++++++++++++ 8 files changed, 407 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/workflows/ecr-image.yml create mode 100644 .github/workflows/infrastructure.yml create mode 100644 infrastructure/app.py create mode 100644 infrastructure/cdk.json create mode 100644 infrastructure/pyproject.toml create mode 100644 infrastructure/uv.lock diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..308a127 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +.env* +**/.env* +.aws/ +**/.aws/ +*.pem +*.key +.venv/ +cdk.out/ +**/__pycache__/ +infrastructure/ +infrastructure-as-code/ \ No newline at end of file diff --git a/.github/workflows/ecr-image.yml b/.github/workflows/ecr-image.yml new file mode 100644 index 0000000..4503dd5 --- /dev/null +++ b/.github/workflows/ecr-image.yml @@ -0,0 +1,40 @@ +name: Push ECR image + +on: + push: + branches: [main] + paths-ignore: + - 'infrastructure/**' + - 'infrastructure-as-code/**' + - '.github/workflows/infrastructure.yml' + workflow_dispatch: + +permissions: + contents: read + id-token: write + +jobs: + push: + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + persist-credentials: false + - uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ vars.ECR_PUSH_ROLE_ARN }} + aws-region: ${{ vars.AWS_REGION }} + - name: Verify ECR repository + run: aws ecr describe-repositories --repository-names flows2fim --query 'repositories[0].repositoryName' --output text > /dev/null + - uses: aws-actions/amazon-ecr-login@v2 + id: login + - uses: docker/setup-buildx-action@v3 + - uses: docker/build-push-action@v6 + with: + context: . + target: prod + push: true + platforms: linux/amd64,linux/arm64 + tags: ${{ steps.login.outputs.registry }}/flows2fim:sha-${{ github.sha }} \ No newline at end of file diff --git a/.github/workflows/infrastructure.yml b/.github/workflows/infrastructure.yml new file mode 100644 index 0000000..d15a300 --- /dev/null +++ b/.github/workflows/infrastructure.yml @@ -0,0 +1,65 @@ +name: Deploy ECR infrastructure + +on: + pull_request: + branches: [main] + paths: + - 'infrastructure/**' + - '.github/workflows/infrastructure.yml' + push: + branches: [main] + paths: + - 'infrastructure/**' + - '.github/workflows/infrastructure.yml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + validate: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + defaults: + run: + working-directory: infrastructure + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v6 + - uses: actions/setup-node@v4 + with: + node-version: '22' + - run: npm install --global aws-cdk@2.1143.0 + - run: uv sync --locked + - run: uv run --locked ruff check app.py + - run: uv run --locked pyright + - run: uv run --locked cdk synth + + deploy: + if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' + runs-on: ubuntu-latest + defaults: + run: + working-directory: infrastructure + permissions: + contents: read + id-token: write + concurrency: + group: flows2fim-registry + cancel-in-progress: false + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v6 + - uses: actions/setup-node@v4 + with: + node-version: '22' + - run: npm install --global aws-cdk@2.1143.0 + - run: uv sync --locked + - uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ vars.CDK_DEPLOY_ROLE_ARN }} + aws-region: ${{ vars.AWS_REGION }} + - run: uv run --locked cdk synth + - run: uv run --locked cdk deploy --require-approval never --role-arn "$CFN_EXECUTION_ROLE_ARN" + env: + CFN_EXECUTION_ROLE_ARN: ${{ vars.CFN_EXECUTION_ROLE_ARN }} diff --git a/.gitignore b/.gitignore index 78d0bc2..8394b8c 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,14 @@ builds/* # Go workspace file go.work +# Python CDK tooling and synthesized templates +.venv/ +__pycache__/ +*.egg-info/ +.ruff_cache/ +.pyright/ +cdk.out/ + # Data files *.csv *.vrt diff --git a/infrastructure/app.py b/infrastructure/app.py new file mode 100644 index 0000000..4b51757 --- /dev/null +++ b/infrastructure/app.py @@ -0,0 +1,42 @@ +"""Define the container registry used by flows2fim releases.""" + +import aws_cdk as cdk +from aws_cdk import aws_ecr as ecr + +app = cdk.App() +stack = cdk.Stack( + app, + "Flows2fimRegistry", + env=cdk.Environment( + account=cdk.Aws.ACCOUNT_ID, + region=cdk.Aws.REGION, + ), + synthesizer=cdk.DefaultStackSynthesizer(generate_bootstrap_version_rule=False), +) + +repository = ecr.Repository( + stack, + "Repository", + repository_name="flows2fim", + image_scan_on_push=True, + image_tag_mutability=ecr.TagMutability.IMMUTABLE, + encryption=ecr.RepositoryEncryption.AES_256, + removal_policy=cdk.RemovalPolicy.RETAIN, + lifecycle_rules=[ + ecr.LifecycleRule( + description="Keep the 100 newest commit images", + tag_prefix_list=["sha-"], + max_image_count=100, + ), + ecr.LifecycleRule( + description="Remove untagged images after seven days", + tag_status=ecr.TagStatus.UNTAGGED, + max_image_age=cdk.Duration.days(7), + ), + ], +) + +cdk.CfnOutput(stack, "RepositoryName", value=repository.repository_name) +cdk.CfnOutput(stack, "RepositoryUri", value=repository.repository_uri) + +app.synth() diff --git a/infrastructure/cdk.json b/infrastructure/cdk.json new file mode 100644 index 0000000..03d0169 --- /dev/null +++ b/infrastructure/cdk.json @@ -0,0 +1,3 @@ +{ + "app": "python app.py" +} \ No newline at end of file diff --git a/infrastructure/pyproject.toml b/infrastructure/pyproject.toml new file mode 100644 index 0000000..c696823 --- /dev/null +++ b/infrastructure/pyproject.toml @@ -0,0 +1,14 @@ +[project] +name = "flows2fim-infrastructure" +dynamic = ["version"] +requires-python = ">=3.11" +dependencies = [ + "aws-cdk-lib>=2.200,<3", + "constructs>=10,<11", + "pyright>=1.1,<2", + "ruff>=0.12,<1", +] + +[tool.pyright] +include = ["app.py"] +pythonVersion = "3.11" \ No newline at end of file diff --git a/infrastructure/uv.lock b/infrastructure/uv.lock new file mode 100644 index 0000000..c26e65c --- /dev/null +++ b/infrastructure/uv.lock @@ -0,0 +1,224 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "aws-cdk-asset-awscli-v1" +version = "2.2.292" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsii" }, + { name = "publication" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/2c/cb258b03e0fa3dcfd6e73e081e81783bbe138ab96641b49fe134308b9c5b/aws_cdk_asset_awscli_v1-2.2.292.tar.gz", hash = "sha256:06da203eeaecbbbf5f1ecd3b12140104e6a59b6a7f6182cfbda5c5deda4b6344", size = 21189256, upload-time = "2026-08-03T15:28:55.2Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/23/c307fefce170e5703b102398d6c541a9b52534a43718760c58677e62e98a/aws_cdk_asset_awscli_v1-2.2.292-py3-none-any.whl", hash = "sha256:01cfb33e2913df2d63448b0954ea4ed1bcb1d064514b4b7aa266eda915e8ebfc", size = 21187714, upload-time = "2026-08-03T15:28:51.844Z" }, +] + +[[package]] +name = "aws-cdk-asset-node-proxy-agent-v6" +version = "2.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsii" }, + { name = "publication" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/b2/a8e84037da16463ff33c1174d3e8b2fdb9a1c4457654b547e9f2249b86ce/aws_cdk_asset_node_proxy_agent_v6-2.1.3.tar.gz", hash = "sha256:1724285cb3ab50494f4c641c236d0e57d1f2da82bda71d15585a203dc0e75516", size = 1559117, upload-time = "2026-09-08T11:01:50.746Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/58/2d8e000954ea1f8bf22e826e42ef0d283df9b86e25e78a06c20750dcb71d/aws_cdk_asset_node_proxy_agent_v6-2.1.3-py3-none-any.whl", hash = "sha256:b8adb536ad19f8a44e94054c712f5ea5ff487cc3c10ecdc158846b29ca2cbbe5", size = 1557696, upload-time = "2026-09-08T11:01:48.74Z" }, +] + +[[package]] +name = "aws-cdk-cloud-assembly-schema" +version = "54.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsii" }, + { name = "publication" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/fa/00a49d33b33f995664e209d484b80a98873ed19f5d0d2d9c66ef72770d98/aws_cdk_cloud_assembly_schema-54.26.0.tar.gz", hash = "sha256:f13e1d4ff3dc4fd681288912fcee39a34fdd964e490e1858f541187f6bb32e35", size = 283123, upload-time = "2026-09-30T20:26:55.811Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/49/9898141e25e77bffaa8c1b3c4beb7c1ef480630c0b241b016c84e85b093b/aws_cdk_cloud_assembly_schema-54.26.0-py3-none-any.whl", hash = "sha256:b63a8214f85327c728c440b96daff9008c4632b7c60a40852b1e3c1dbda48485", size = 282213, upload-time = "2026-09-30T20:26:54.141Z" }, +] + +[[package]] +name = "aws-cdk-lib" +version = "2.272.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aws-cdk-asset-awscli-v1" }, + { name = "aws-cdk-asset-node-proxy-agent-v6" }, + { name = "aws-cdk-cloud-assembly-schema" }, + { name = "constructs" }, + { name = "jsii" }, + { name = "publication" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/38/55703f8f6158e900a75ea9c8ccdb789677e09dd886d8f4cad80d16b8bd09/aws_cdk_lib-2.272.0.tar.gz", hash = "sha256:65d511c98da2af713665af85e506334524aca275d1114523abd830152027c116", size = 58231015, upload-time = "2026-09-30T14:01:55.815Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/30/c3ea7fc6cb647419f33eef1c994d0bdbf5187ed78ca95be1b7ef5a6da64f/aws_cdk_lib-2.272.0-py3-none-any.whl", hash = "sha256:e0be631712be4ca138bdaa112de556cebba8741bb5ef7fc3159d95e0f0717115", size = 58959556, upload-time = "2026-09-30T14:01:13.12Z" }, +] + +[[package]] +name = "cattrs" +version = "26.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/75/e72b839c3dc869c990b4842f3dba730bdcdf5215f68fc7955edf849a1792/cattrs-26.2.1.tar.gz", hash = "sha256:679132bfdc225c5ee40c024fc42519954767c387f950dc6751946c586bccdc6d", size = 525617, upload-time = "2026-09-26T20:53:21.114Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/cf/22794a399d99480486120e26e879ef008e21f5e85274c2ed591d568bb326/cattrs-26.2.1-py3-none-any.whl", hash = "sha256:a12aaa3453dc8f633a815293179f08b7421ed18d2575c459c3c736f840beac24", size = 74843, upload-time = "2026-09-26T20:53:19.767Z" }, +] + +[[package]] +name = "constructs" +version = "10.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsii" }, + { name = "publication" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/7e/5caadda94765496d4096528bcc432ae256ade10cf4b847e88a5d09918c1d/constructs-10.8.1.tar.gz", hash = "sha256:9f6e4eb1f6b8b1ac8dcbc85457dcbb1e3f9bd93bbeef03154f6cc7fbbba74b06", size = 788838, upload-time = "2026-08-03T15:28:03.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/df/3afc4e2c4c6bdf58bfe27d7e082f82f3364562592613c159209ed50ddd16/constructs-10.8.1-py3-none-any.whl", hash = "sha256:f71297db64723889147c82de46dcacaa76b120f856d5b5ac5f65abb22ea88355", size = 787025, upload-time = "2026-08-03T15:28:02.082Z" }, +] + +[[package]] +name = "flows2fim-infrastructure" +source = { virtual = "." } +dependencies = [ + { name = "aws-cdk-lib" }, + { name = "constructs" }, + { name = "pyright" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "aws-cdk-lib", specifier = ">=2.200,<3" }, + { name = "constructs", specifier = ">=10,<11" }, + { name = "pyright", specifier = ">=1.1,<2" }, + { name = "ruff", specifier = ">=0.12,<1" }, +] + +[[package]] +name = "jsii" +version = "1.141.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "cattrs" }, + { name = "publication" }, + { name = "python-dateutil" }, + { name = "typeguard" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/ea/1b572564cd47fc22f6a248dc2a4fbec91b421877e2901fdd85aaf291f017/jsii-1.141.0.tar.gz", hash = "sha256:e574efa7523b2218f6a4495e9f1ba75c9947b84965c5a8079931f37d7911a687", size = 532264, upload-time = "2026-10-01T16:58:15.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/a5/aa73a8196be12872cb636a9bd5320730a2743d2a9111dc150ad72b12e71d/jsii-1.141.0-py3-none-any.whl", hash = "sha256:72ca269b483c5190e5002c9e1f0f43971c3aead1cd444ea0c64690c05e1b0da0", size = 502557, upload-time = "2026-10-01T16:58:13.825Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/105de02c1322cfada6d9710d9146ef8026419d433c9d08359a2d35805811/nodeenv-1.11.0.tar.gz", hash = "sha256:3ce8fe5b71d16e8af7039ca65257354100bc772965d6bc549070649e53b1b146", size = 99301, upload-time = "2026-09-26T11:29:21.367Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/c8/12811c9b48fde162bb72b6f2e78fada9a247a09d7bb5be2050a5d099c77b/nodeenv-1.11.0-py2.py3-none-any.whl", hash = "sha256:edaa16e6c14d7cf395d75d4bbd5a26390f4dc06501a33b4e76282b02cc688a25", size = 34277, upload-time = "2026-09-26T11:29:19.933Z" }, +] + +[[package]] +name = "publication" +version = "0.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/8e/8c9fe7e32fdf9c386f83d59610cc819a25dadb874b5920f2d0ef7d35f46d/publication-0.0.3.tar.gz", hash = "sha256:68416a0de76dddcdd2930d1c8ef853a743cc96c82416c4e4d3b5d901c6276dc4", size = 5484, upload-time = "2019-01-15T07:52:23.914Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/d3/6308debad7afcdb3ea5f50b4b3d852f41eb566a311fbcb4da23755a28155/publication-0.0.3-py2.py3-none-any.whl", hash = "sha256:0248885351febc11d8a1098d5c8e3ab2dabcf3e8c0c96db1e17ecd12b53afbe6", size = 7687, upload-time = "2019-01-15T07:52:22.151Z" }, +] + +[[package]] +name = "pyright" +version = "1.1.414" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/1b/244c7b710031ada80f27e579ec20d28a2285dfc318fed0339866b1047f12/pyright-1.1.414.tar.gz", hash = "sha256:523c0a97c60da6333234955c277730c9cf4f5bd6d5399e7b7d2b0fc5d3599524", size = 4154638, upload-time = "2026-09-10T12:26:53.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/ba/18b6e682ead424ad24bcc134339ae5d1b931cd9ae260540592a058a91279/pyright-1.1.414-py3-none-any.whl", hash = "sha256:2a6b4b3298c9eec174c5ed83bd338de6eee82df2992f3e1930e6199d381be36f", size = 6225049, upload-time = "2026-09-10T12:26:51.427Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "ruff" +version = "0.16.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/49/23802c45f093eb14bde54b141d2b2f058edfa63a06db7beed047308cc08f/ruff-0.16.10.tar.gz", hash = "sha256:eff4728c4eaae93f0955cd264d24b2ab348e74bf59986ccf282ba6dc16b3b017", size = 4958724, upload-time = "2026-10-01T18:03:21.697Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/21/ebce22e1d90cdb2cd691026b9c6e9bec6499f0b396089481755b5d49efff/ruff-0.16.10-py3-none-linux_armv6l.whl", hash = "sha256:488b0fe3f3574210e5cf80d9f59b9e3ab17a127a8155de3f307b392589cfb511", size = 10095557, upload-time = "2026-10-01T18:02:36.072Z" }, + { url = "https://files.pythonhosted.org/packages/cb/98/a54de85876a8b2612bfa0d84c7b9abfb39c6a3354aee7800b09c1649b9e3/ruff-0.16.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e748ff95c934c4e978783b8e687bc174e7bd84e8ad24e3243e1ecfcda5e0282d", size = 10340577, upload-time = "2026-10-01T18:02:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/9f/16/1a5a4a2657effe4806110f2b907313802f1367fcdbb29e8122766e407fab/ruff-0.16.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3031a4a2e8e7b8a46f70be45f198c35a11ece509a94b80334d8d397a33c67550", size = 9774282, upload-time = "2026-10-01T18:02:41.79Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fb/470085af734da396e68cd80fb0a3e7c109a459716ae59588c0f6fab8a17d/ruff-0.16.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:494401c86df4c4c25f69b9419605d944467ee98c42fb6ad405ef4fa40b8fb67d", size = 9920895, upload-time = "2026-10-01T18:02:44.458Z" }, + { url = "https://files.pythonhosted.org/packages/57/de/f10cffe4f88a37ea6615ec460f01bf76f0bc1477737a35d9ee61a630a78b/ruff-0.16.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d203abc0ff2b773ee33d00ab8df0bb67046fbc7c07b119332f08b9b345cf8221", size = 9902707, upload-time = "2026-10-01T18:02:47.041Z" }, + { url = "https://files.pythonhosted.org/packages/ff/44/3fdcedf83ae60ef837dd239170e480dce606a9142cfa2db911afdf855fd9/ruff-0.16.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd83d1235a5258d318477bc5b576303974cbdef5df0c01a1bff14efcc23bd12a", size = 10619287, upload-time = "2026-10-01T18:02:49.485Z" }, + { url = "https://files.pythonhosted.org/packages/c1/62/02e76a5574002153618eafbb70e159468addc72a5d2c488e65cbb4639d2d/ruff-0.16.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc2610fb269fa56dd8a68669ae470fa6272902668c0fc2ebc3aa112b2633d5b8", size = 11339942, upload-time = "2026-10-01T18:02:52.008Z" }, + { url = "https://files.pythonhosted.org/packages/6b/c4/cde27d47ad8d4126c587e608c67c46e7feac11763f606d261a1bc8a489e6/ruff-0.16.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e70175e29cc94c26ea296c80e470180b744b7419026898e58f520c6ab32578e", size = 10934316, upload-time = "2026-10-01T18:02:54.811Z" }, + { url = "https://files.pythonhosted.org/packages/e4/03/17234145f302a645a123e8c3bb2411ecf4669fbc350de3b0d803230a1729/ruff-0.16.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f33f43a864a8483eebd160e713336c8bab02c934feaff0a33cf5ccb41546d09a", size = 10387968, upload-time = "2026-10-01T18:02:57.497Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/2493af60240ee7644b38a4b821f5f9c3b5a4fa3178770fe1d0c685217395/ruff-0.16.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1dfc6f0088149fb6a362c1c446bcbb3fd2157b3852fe2fa68409276eab9ad9b3", size = 10537906, upload-time = "2026-10-01T18:03:00.006Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9a/f56b28f3b143bb9e518c34fb89ab191b626bca8b70dbf8af0d2e2d473572/ruff-0.16.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6553498afc35f580f036030795810b9e6bcea31604b0fd9e8d352795473042e3", size = 10012938, upload-time = "2026-10-01T18:03:03.006Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/fca37362848ea4d4d80712df13632e645e7c7cfb1bdedf140699ac7a090b/ruff-0.16.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a3b8471dea115d37f123882be852bed13403746d3a76c11de4a19ec5f9ff5a03", size = 9897945, upload-time = "2026-10-01T18:03:05.818Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c7/e0bc57664d6e0c61fd22f620af260fe9662d7e1ddb320ea7f160e76ef165/ruff-0.16.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:92e59a70bcbd9d3a5483656da906ec28edfdacfce00afd99edb8b4e9d15644be", size = 10333134, upload-time = "2026-10-01T18:03:08.25Z" }, + { url = "https://files.pythonhosted.org/packages/21/aa/5c9f3b68737c0e4a33a1db5dfab44f7b786d96ca91a7233112ddab1e6dc2/ruff-0.16.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7ae7375f803b5520dc9f546bed7e3a0acb70b91812e9bb4b19927de22f25b77d", size = 10741702, upload-time = "2026-10-01T18:03:10.775Z" }, + { url = "https://files.pythonhosted.org/packages/78/fa/0f9c2020dc316c53d983be011720b8157cc052b97e3f4dfa7db6d0f880a6/ruff-0.16.10-py3-none-win32.whl", hash = "sha256:2a12e01cb9156c10c466f63b46eaae5ecea28dfbd21b5836353ae498e7d1349a", size = 10139176, upload-time = "2026-10-01T18:03:13.267Z" }, + { url = "https://files.pythonhosted.org/packages/99/29/cfb0df9448d4d4ad48c2de029ada9ebd71baa6da983a6c77ee6c6cd0fe82/ruff-0.16.10-py3-none-win_amd64.whl", hash = "sha256:97f2015c92aa97105b0eab19eb5d224884399281cfc5da86a92db4ab5e7fb2ca", size = 10584734, upload-time = "2026-10-01T18:03:16.006Z" }, + { url = "https://files.pythonhosted.org/packages/fc/05/c16957eb287c3fc062e032619a25868d93d408a844b725bcf514f7a378ff/ruff-0.16.10-py3-none-win_arm64.whl", hash = "sha256:25a65fe998c4e6861ec079ada5826a2fc605e6cbccbe9dcd7fac1f54e791621b", size = 10366440, upload-time = "2026-10-01T18:03:19.04Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "typeguard" +version = "2.13.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/38/c61bfcf62a7b572b5e9363a802ff92559cb427ee963048e1442e3aef7490/typeguard-2.13.3.tar.gz", hash = "sha256:00edaa8da3a133674796cf5ea87d9f4b4c367d77476e185e80251cc13dfbb8c4", size = 40604, upload-time = "2021-12-10T21:09:39.158Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/bb/d43e5c75054e53efce310e79d63df0ac3f25e34c926be5dffb7d283fb2a8/typeguard-2.13.3-py3-none-any.whl", hash = "sha256:5e3e3be01e887e7eafae5af63d1f36c849aaa94e3a0112097312aabfa16284f1", size = 17605, upload-time = "2021-12-10T21:09:37.844Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] From 96f5f9ead2b48cc7ae653d141ed956ef4cf37896 Mon Sep 17 00:00:00 2001 From: sclaw Date: Tue, 6 Oct 2026 15:19:52 -0400 Subject: [PATCH 3/3] fix: update branch targets and remove mac tests --- .github/workflows/build_and_test_mac.yml | 80 ------------------------ .github/workflows/infrastructure.yml | 6 +- 2 files changed, 3 insertions(+), 83 deletions(-) delete mode 100644 .github/workflows/build_and_test_mac.yml diff --git a/.github/workflows/build_and_test_mac.yml b/.github/workflows/build_and_test_mac.yml deleted file mode 100644 index c6032a3..0000000 --- a/.github/workflows/build_and_test_mac.yml +++ /dev/null @@ -1,80 +0,0 @@ -name: Build and Test - MacOS - -on: - push: - branches: [main] - pull_request: - workflow_dispatch: - -jobs: - build: - name: Build flows2fim - - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@v4 - - - name: Set up Docker Compose - uses: docker/setup-buildx-action@v3 - - - name: Run Docker Compose - run: | - docker compose up -d - docker ps - - - name: Attach to running container and Build - run: | - CONTAINER_ID=$(docker ps -q | head -n 1) - if [ -n "$CONTAINER_ID" ]; then - docker exec $CONTAINER_ID /bin/bash -c "./scripts/build-darwin-arm64.sh" - docker compose down - else - echo "No running containers found" - exit 1 - fi - - - name: Upload Artifact - uses: actions/upload-artifact@v4 - with: - name: flows2fim - path: builds/darwin-arm64 - - test: - name: Test flows2fim - runs-on: macos-latest - needs: build - - steps: - - name: Checkout Repository - uses: actions/checkout@v4 - - - name: Download artifact - uses: actions/download-artifact@v4 - with: - name: flows2fim - path: builds/darwin-arm64 - - - name: Add flows2fim to PATH - run: | - ls -R builds/darwin-arm64 - sudo chmod a+x ./builds/darwin-arm64/flows2fim - ./builds/darwin-arm64/flows2fim --version - echo "builds/darwin-arm64" >> $GITHUB_PATH - - - name: Install & Configure GDAL - run: | - brew install gdal - - - name: Verify gdal installation - run: | - gdalinfo --version - - - name: Issue flows2fim controls test cases - run: | - ./scripts/test_suite_mac.sh controls - - - name: Issue flows2fim fim test cases - run: | - ./scripts/test_suite_mac.sh fim diff --git a/.github/workflows/infrastructure.yml b/.github/workflows/infrastructure.yml index d15a300..55a51ab 100644 --- a/.github/workflows/infrastructure.yml +++ b/.github/workflows/infrastructure.yml @@ -2,12 +2,12 @@ name: Deploy ECR infrastructure on: pull_request: - branches: [main] + branches: [master] paths: - 'infrastructure/**' - '.github/workflows/infrastructure.yml' push: - branches: [main] + branches: [master] paths: - 'infrastructure/**' - '.github/workflows/infrastructure.yml' @@ -36,7 +36,7 @@ jobs: - run: uv run --locked cdk synth deploy: - if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' + if: github.ref == 'refs/heads/master' && github.event_name != 'pull_request' runs-on: ubuntu-latest defaults: run: