Introduction

This notebook documents the data ingestion stage of the housing affordability analysis pipeline. Its purpose is to retrieve, validate, and store raw data from multiple public sources in a consistent and reproducible manner. All subsequent transformation, analysis, and modeling steps depend on the integrity of the data collected here.

The ingestion process integrates data from three primary sources. Zillow Home Value Index (ZHVI) data provide monthly measures of median home values by metropolitan area. Macroeconomic indicators, including mortgage interest rates and inflation, are sourced from the Federal Reserve Economic Data (FRED) API. Median household income data are obtained from the U.S. Census Bureau’s American Community Survey (ACS). Together, these datasets form the foundation for constructing a longitudinal panel of housing market conditions across U.S. metropolitan areas.

This notebook focuses exclusively on data acquisition and initial validation. It performs no feature engineering or analytical transformations beyond those required to standardize variable names, date formats, and geographic identifiers. Each ingestion step includes basic verification checks to confirm data completeness and expected ranges before writing outputs to disk.

All raw data generated by this notebook are saved to the data/raw/ directory and are intentionally excluded from version control. Reproducibility is ensured through the use of API-based retrieval, environment-managed dependencies, and explicit documentation of data sources and parameters. Downstream notebooks rely on these raw files as immutable inputs for transformation and analysis.

library(tidyverse)
library(lubridate)
library(broom)
library(corrplot)
library(fredr)
set.seed(42)

Data Ingestion

library(tidycensus)
census_api_key(Sys.getenv("CENSUS_API_KEY"))
library(fredr)
fredr_set_key(Sys.getenv("FRED_API_KEY"))
# --- Helper: pull a FRED series into a tidy tibble ---
get_fred_series <- function(series_id, start_date = as.Date("2000-01-01")) {
  fredr::fredr(
    series_id = series_id,
    observation_start = start_date
  ) %>%
    select(date, value) %>%
    rename(!!series_id := value)
}

# --- Choose series to ingest ---
# Commonly useful for housing affordability:
series_ids <- c(
  "MORTGAGE30US",  # 30-year fixed mortgage rate (weekly)
  "CSUSHPISA",     # Case-Shiller US HPI (monthly)
  "CPIAUCSL"       # CPI (monthly)
)

start_date <- as.Date("2000-01-01")

verify_fred_df <- function(df) {
  #basic ingestion checks
  stopifnot("date" %in% names(df))
  stopifnot(nrow(df) > 0)
  stopifnot(is.Date(df$date))
  
  value_col <- setdiff(names(df), "date")
  stopifnot(length(value_col) == 1)
  
  tibble(
    series_id   = value_col,
    n_rows      = nrow(df),
    start_date  = min(df$date, na.rm = TRUE),
    end_date    = max(df$date, na.rm = TRUE),
    n_missing   = sum(is.na(df[[value_col]])),
    n_dup_dates = sum(duplicated(df$date))
  )
}

ingestion_report <- map_dfr(series_ids, function(sid){
  df <- get_fred_series(sid,start_date = start_date)
  
  #verification summary
  report <- verify_fred_df(df)
  
  #quick peek (optional)
  message("\n--",sid," ---")
  print(head(df,3))
  print(tail(df,3))
  print(report)
  out_path <- file.path("./data/raw", paste0("fred_", sid,"_",format(start_date, "%Y%m%d"),".csv"))
  write_csv(df, out_path)
  report %>% mutate(saved_to = out_path)
})
# A tibble: 3 × 2
  date       MORTGAGE30US
  <date>            <dbl>
1 2000-01-07         8.15
2 2000-01-14         8.18
3 2000-01-21         8.26
# A tibble: 3 × 2
  date       MORTGAGE30US
  <date>            <dbl>
1 2025-12-31         6.15
2 2026-01-08         6.16
3 2026-01-15         6.06
# A tibble: 1 × 6
  series_id    n_rows start_date end_date   n_missing n_dup_dates
  <chr>         <int> <date>     <date>         <int>       <int>
1 MORTGAGE30US   1359 2000-01-07 2026-01-15         0           0
# A tibble: 3 × 2
  date       CSUSHPISA
  <date>         <dbl>
1 2000-01-01      101.
2 2000-02-01      101.
3 2000-03-01      102.
# A tibble: 3 × 2
  date       CSUSHPISA
  <date>         <dbl>
1 2025-08-01      327.
2 2025-09-01      328.
3 2025-10-01      329.
# A tibble: 1 × 6
  series_id n_rows start_date end_date   n_missing n_dup_dates
  <chr>      <int> <date>     <date>         <int>       <int>
1 CSUSHPISA    310 2000-01-01 2025-10-01         0           0
# A tibble: 3 × 2
  date       CPIAUCSL
  <date>        <dbl>
1 2000-01-01     169.
2 2000-02-01     170 
3 2000-03-01     171 
# A tibble: 3 × 2
  date       CPIAUCSL
  <date>        <dbl>
1 2025-10-01      NA 
2 2025-11-01     325.
3 2025-12-01     326.
# A tibble: 1 × 6
  series_id n_rows start_date end_date   n_missing n_dup_dates
  <chr>      <int> <date>     <date>         <int>       <int>
1 CPIAUCSL     312 2000-01-01 2025-12-01         1           0

ingestion_report
NA

Census Data Ingestion

# --- Helper: ACS median household income (B19013) by Metro (CBSA) ---
get_acs_median_hh_income_metro <- function(year = 2023) {
  df <- get_acs(
    geography = "metropolitan statistical area/micropolitan statistical area",
    table = "B19013",
    year = year,
    survey = "acs5",
    cache_table = TRUE
  ) %>%
    transmute(
      year = year,
      geo_id = GEOID,
      region = NAME,
      income_median_hh = estimate,
      moe = moe
    )

  df
}

#---Pull data ---
YEAR <- 2023
income_metro <- get_acs_median_hh_income_metro(year = YEAR)

#---Verification---
stopifnot(nrow(income_metro) > 0)
stopifnot(all(c("geo_id","region","income_median_hh") %in% names(income_metro)))

verification <- income_metro %>%
  summarise(
    year   = first(year),
    n_rows = n(),
    n_missing_income = sum(is.na(income_median_hh)),
    min_income = min(income_median_hh, na.rm = TRUE),
    max_income = max(income_median_hh, na.rm = TRUE)
  )

print(head(income_metro, 5))
# A tibble: 5 × 5
   year geo_id region                  income_median_hh   moe
  <dbl> <chr>  <chr>                              <dbl> <dbl>
1  2023 10100  Aberdeen, SD Micro Area            70761  3303
2  2023 10140  Aberdeen, WA Micro Area            63539  2689
3  2023 10180  Abilene, TX Metro Area             66464  2039
4  2023 10220  Ada, OK Micro Area                 62564  4420
5  2023 10300  Adrian, MI Micro Area              67013  1691
print(verification)
# A tibble: 1 × 5
   year n_rows n_missing_income min_income max_income
  <dbl>  <int>            <int>      <dbl>      <dbl>
1  2023    935                0      17624     157444
#---Save to raw---
out_path <- file.path("./data/raw",paste0("ac5_median_hh_income_metro_",YEAR,".csv"))
write_csv(income_metro, out_path)

message("Saved:", out_path)

Updated ingestion (multi-year ACS5 income by metro)

library(tidyverse)
library(tidycensus)
library(readr)

# --- Helper: ACS median household income (B19013) by Metro (CBSA), multi-year ---
get_acs_median_hh_income_metro <- function(years = 2009:2023) {
  stopifnot(length(years) > 0)

  df <- purrr::map_dfr(years, function(y) {
    get_acs(
      geography = "metropolitan statistical area/micropolitan statistical area",
      table = "B19013",
      year = y,
      survey = "acs5",
      cache_table = TRUE
    ) %>%
      transmute(
        year = y,
        geo_id = GEOID,
        region = NAME,
        income_median_hh = estimate,
        moe = moe
      )
  })

  df
}

# --- Pull data ---
YEARS <- 2009:2023
income_metro <- get_acs_median_hh_income_metro(years = YEARS)

# --- Verification ---
stopifnot(nrow(income_metro) > 0)
stopifnot(all(c("year", "geo_id", "region", "income_median_hh") %in% names(income_metro)))

verification <- income_metro %>%
  summarise(
    year_min = min(year),
    year_max = max(year),
    n_rows = n(),
    n_regions = n_distinct(region),
    n_missing_income = sum(is.na(income_median_hh)),
    min_income = min(income_median_hh, na.rm = TRUE),
    max_income = max(income_median_hh, na.rm = TRUE)
  )

print(head(income_metro, 5))
# A tibble: 5 × 5
   year geo_id region                   income_median_hh   moe
  <int> <chr>  <chr>                               <dbl> <dbl>
1  2009 10020  Abbeville, LA Micro Area            40785  1650
2  2009 10100  Aberdeen, SD Micro Area             45108  1529
3  2009 10140  Aberdeen, WA Micro Area             41618  1674
4  2009 10180  Abilene, TX Metro Area              40715  1343
5  2009 10220  Ada, OK Micro Area                  36446  1565
print(verification)
# A tibble: 1 × 7
  year_min year_max n_rows n_regions n_missing_income min_income max_income
     <int>    <int>  <int>     <int>            <int>      <dbl>      <dbl>
1     2009     2023  14130      1262                1      10499     157444
# Optional: verify coverage by year
coverage_by_year <- income_metro %>%
  group_by(year) %>%
  summarise(
    n_regions = n_distinct(region),
    pct_missing_income = mean(is.na(income_median_hh)),
    .groups = "drop"
  )

print(head(coverage_by_year, 10))
# A tibble: 10 × 3
    year n_regions pct_missing_income
   <int>     <int>              <dbl>
 1  2009       953            0      
 2  2010       955            0      
 3  2011       955            0      
 4  2012       955            0      
 5  2013       929            0      
 6  2014       929            0      
 7  2015       929            0      
 8  2016       945            0      
 9  2017       945            0      
10  2018       945            0.00106
# --- Save to raw ---
dir.create("data/raw", recursive = TRUE, showWarnings = FALSE)

out_path <- file.path(
  "data/raw",
  paste0("acs5_median_hh_income_metro_", min(YEARS), "_", max(YEARS), ".csv")
)

write_csv(income_metro, out_path)
message("Saved: ", out_path)
LS0tDQp0aXRsZTogImhvdXNlaW5nLWFmZm9yZGFiaWxpdHktUiINCm91dHB1dDogaHRtbF9ub3RlYm9vaw0KLS0tDQoNCiMgSW50cm9kdWN0aW9uDQoNClRoaXMgbm90ZWJvb2sgZG9jdW1lbnRzIHRoZSAqKmRhdGEgaW5nZXN0aW9uIHN0YWdlKiogb2YgdGhlIGhvdXNpbmcgYWZmb3JkYWJpbGl0eSBhbmFseXNpcyBwaXBlbGluZS4gSXRzIHB1cnBvc2UgaXMgdG8gcmV0cmlldmUsIHZhbGlkYXRlLCBhbmQgc3RvcmUgcmF3IGRhdGEgZnJvbSBtdWx0aXBsZSBwdWJsaWMgc291cmNlcyBpbiBhIGNvbnNpc3RlbnQgYW5kIHJlcHJvZHVjaWJsZSBtYW5uZXIuIEFsbCBzdWJzZXF1ZW50IHRyYW5zZm9ybWF0aW9uLCBhbmFseXNpcywgYW5kIG1vZGVsaW5nIHN0ZXBzIGRlcGVuZCBvbiB0aGUgaW50ZWdyaXR5IG9mIHRoZSBkYXRhIGNvbGxlY3RlZCBoZXJlLg0KDQpUaGUgaW5nZXN0aW9uIHByb2Nlc3MgaW50ZWdyYXRlcyBkYXRhIGZyb20gdGhyZWUgcHJpbWFyeSBzb3VyY2VzLiBaaWxsb3cgSG9tZSBWYWx1ZSBJbmRleCAoWkhWSSkgZGF0YSBwcm92aWRlIG1vbnRobHkgbWVhc3VyZXMgb2YgbWVkaWFuIGhvbWUgdmFsdWVzIGJ5IG1ldHJvcG9saXRhbiBhcmVhLiBNYWNyb2Vjb25vbWljIGluZGljYXRvcnMsIGluY2x1ZGluZyBtb3J0Z2FnZSBpbnRlcmVzdCByYXRlcyBhbmQgaW5mbGF0aW9uLCBhcmUgc291cmNlZCBmcm9tIHRoZSBGZWRlcmFsIFJlc2VydmUgRWNvbm9taWMgRGF0YSAoRlJFRCkgQVBJLiBNZWRpYW4gaG91c2Vob2xkIGluY29tZSBkYXRhIGFyZSBvYnRhaW5lZCBmcm9tIHRoZSBVLlMuIENlbnN1cyBCdXJlYXXigJlzIEFtZXJpY2FuIENvbW11bml0eSBTdXJ2ZXkgKEFDUykuIFRvZ2V0aGVyLCB0aGVzZSBkYXRhc2V0cyBmb3JtIHRoZSBmb3VuZGF0aW9uIGZvciBjb25zdHJ1Y3RpbmcgYSBsb25naXR1ZGluYWwgcGFuZWwgb2YgaG91c2luZyBtYXJrZXQgY29uZGl0aW9ucyBhY3Jvc3MgVS5TLiBtZXRyb3BvbGl0YW4gYXJlYXMuDQoNClRoaXMgbm90ZWJvb2sgZm9jdXNlcyBleGNsdXNpdmVseSBvbiAqKmRhdGEgYWNxdWlzaXRpb24gYW5kIGluaXRpYWwgdmFsaWRhdGlvbioqLiBJdCBwZXJmb3JtcyBubyBmZWF0dXJlIGVuZ2luZWVyaW5nIG9yIGFuYWx5dGljYWwgdHJhbnNmb3JtYXRpb25zIGJleW9uZCB0aG9zZSByZXF1aXJlZCB0byBzdGFuZGFyZGl6ZSB2YXJpYWJsZSBuYW1lcywgZGF0ZSBmb3JtYXRzLCBhbmQgZ2VvZ3JhcGhpYyBpZGVudGlmaWVycy4gRWFjaCBpbmdlc3Rpb24gc3RlcCBpbmNsdWRlcyBiYXNpYyB2ZXJpZmljYXRpb24gY2hlY2tzIHRvIGNvbmZpcm0gZGF0YSBjb21wbGV0ZW5lc3MgYW5kIGV4cGVjdGVkIHJhbmdlcyBiZWZvcmUgd3JpdGluZyBvdXRwdXRzIHRvIGRpc2suDQoNCkFsbCByYXcgZGF0YSBnZW5lcmF0ZWQgYnkgdGhpcyBub3RlYm9vayBhcmUgc2F2ZWQgdG8gdGhlIGBkYXRhL3Jhdy9gIGRpcmVjdG9yeSBhbmQgYXJlIGludGVudGlvbmFsbHkgZXhjbHVkZWQgZnJvbSB2ZXJzaW9uIGNvbnRyb2wuIFJlcHJvZHVjaWJpbGl0eSBpcyBlbnN1cmVkIHRocm91Z2ggdGhlIHVzZSBvZiBBUEktYmFzZWQgcmV0cmlldmFsLCBlbnZpcm9ubWVudC1tYW5hZ2VkIGRlcGVuZGVuY2llcywgYW5kIGV4cGxpY2l0IGRvY3VtZW50YXRpb24gb2YgZGF0YSBzb3VyY2VzIGFuZCBwYXJhbWV0ZXJzLiBEb3duc3RyZWFtIG5vdGVib29rcyByZWx5IG9uIHRoZXNlIHJhdyBmaWxlcyBhcyBpbW11dGFibGUgaW5wdXRzIGZvciB0cmFuc2Zvcm1hdGlvbiBhbmQgYW5hbHlzaXMuDQoNCmBgYHtyfQ0KbGlicmFyeSh0aWR5dmVyc2UpDQpsaWJyYXJ5KGx1YnJpZGF0ZSkNCmxpYnJhcnkoYnJvb20pDQpsaWJyYXJ5KGNvcnJwbG90KQ0KbGlicmFyeShmcmVkcikNCg0KYGBgDQoNCmBgYHtyfQ0Kc2V0LnNlZWQoNDIpDQpgYGANCg0KLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tDQoNCiMjIERhdGEgSW5nZXN0aW9uDQoNCmBgYHtyfQ0KbGlicmFyeSh0aWR5Y2Vuc3VzKQ0KY2Vuc3VzX2FwaV9rZXkoU3lzLmdldGVudigiQ0VOU1VTX0FQSV9LRVkiKSkNCg0KDQpgYGANCg0KYGBge3J9DQpsaWJyYXJ5KGZyZWRyKQ0KZnJlZHJfc2V0X2tleShTeXMuZ2V0ZW52KCJGUkVEX0FQSV9LRVkiKSkNCg0KYGBgDQoNCmBgYHtyfQ0KIyAtLS0gSGVscGVyOiBwdWxsIGEgRlJFRCBzZXJpZXMgaW50byBhIHRpZHkgdGliYmxlIC0tLQ0KZ2V0X2ZyZWRfc2VyaWVzIDwtIGZ1bmN0aW9uKHNlcmllc19pZCwgc3RhcnRfZGF0ZSA9IGFzLkRhdGUoIjIwMDAtMDEtMDEiKSkgew0KICBmcmVkcjo6ZnJlZHIoDQogICAgc2VyaWVzX2lkID0gc2VyaWVzX2lkLA0KICAgIG9ic2VydmF0aW9uX3N0YXJ0ID0gc3RhcnRfZGF0ZQ0KICApICU+JQ0KICAgIHNlbGVjdChkYXRlLCB2YWx1ZSkgJT4lDQogICAgcmVuYW1lKCEhc2VyaWVzX2lkIDo9IHZhbHVlKQ0KfQ0KDQojIC0tLSBDaG9vc2Ugc2VyaWVzIHRvIGluZ2VzdCAtLS0NCiMgQ29tbW9ubHkgdXNlZnVsIGZvciBob3VzaW5nIGFmZm9yZGFiaWxpdHk6DQpzZXJpZXNfaWRzIDwtIGMoDQogICJNT1JUR0FHRTMwVVMiLCAgIyAzMC15ZWFyIGZpeGVkIG1vcnRnYWdlIHJhdGUgKHdlZWtseSkNCiAgIkNTVVNIUElTQSIsICAgICAjIENhc2UtU2hpbGxlciBVUyBIUEkgKG1vbnRobHkpDQogICJDUElBVUNTTCIgICAgICAgIyBDUEkgKG1vbnRobHkpDQopDQoNCnN0YXJ0X2RhdGUgPC0gYXMuRGF0ZSgiMjAwMC0wMS0wMSIpDQoNCnZlcmlmeV9mcmVkX2RmIDwtIGZ1bmN0aW9uKGRmKSB7DQogICNiYXNpYyBpbmdlc3Rpb24gY2hlY2tzDQogIHN0b3BpZm5vdCgiZGF0ZSIgJWluJSBuYW1lcyhkZikpDQogIHN0b3BpZm5vdChucm93KGRmKSA+IDApDQogIHN0b3BpZm5vdChpcy5EYXRlKGRmJGRhdGUpKQ0KICANCiAgdmFsdWVfY29sIDwtIHNldGRpZmYobmFtZXMoZGYpLCAiZGF0ZSIpDQogIHN0b3BpZm5vdChsZW5ndGgodmFsdWVfY29sKSA9PSAxKQ0KICANCiAgdGliYmxlKA0KICAgIHNlcmllc19pZCAgID0gdmFsdWVfY29sLA0KICAgIG5fcm93cyAgICAgID0gbnJvdyhkZiksDQogICAgc3RhcnRfZGF0ZSAgPSBtaW4oZGYkZGF0ZSwgbmEucm0gPSBUUlVFKSwNCiAgICBlbmRfZGF0ZSAgICA9IG1heChkZiRkYXRlLCBuYS5ybSA9IFRSVUUpLA0KICAgIG5fbWlzc2luZyAgID0gc3VtKGlzLm5hKGRmW1t2YWx1ZV9jb2xdXSkpLA0KICAgIG5fZHVwX2RhdGVzID0gc3VtKGR1cGxpY2F0ZWQoZGYkZGF0ZSkpDQogICkNCn0NCg0KaW5nZXN0aW9uX3JlcG9ydCA8LSBtYXBfZGZyKHNlcmllc19pZHMsIGZ1bmN0aW9uKHNpZCl7DQogIGRmIDwtIGdldF9mcmVkX3NlcmllcyhzaWQsc3RhcnRfZGF0ZSA9IHN0YXJ0X2RhdGUpDQogIA0KICAjdmVyaWZpY2F0aW9uIHN1bW1hcnkNCiAgcmVwb3J0IDwtIHZlcmlmeV9mcmVkX2RmKGRmKQ0KICANCiAgI3F1aWNrIHBlZWsgKG9wdGlvbmFsKQ0KICBtZXNzYWdlKCJcbi0tIixzaWQsIiAtLS0iKQ0KICBwcmludChoZWFkKGRmLDMpKQ0KICBwcmludCh0YWlsKGRmLDMpKQ0KICBwcmludChyZXBvcnQpDQogIG91dF9wYXRoIDwtIGZpbGUucGF0aCgiLi9kYXRhL3JhdyIsIHBhc3RlMCgiZnJlZF8iLCBzaWQsIl8iLGZvcm1hdChzdGFydF9kYXRlLCAiJVklbSVkIiksIi5jc3YiKSkNCiAgd3JpdGVfY3N2KGRmLCBvdXRfcGF0aCkNCiAgcmVwb3J0ICU+JSBtdXRhdGUoc2F2ZWRfdG8gPSBvdXRfcGF0aCkNCn0pDQpgYGANCg0KYGBge3J9DQoNCmluZ2VzdGlvbl9yZXBvcnQNCg0KYGBgDQoNCiMjIyMgQ2Vuc3VzIERhdGEgSW5nZXN0aW9uDQoNCmBgYHtyfQ0KIyAtLS0gSGVscGVyOiBBQ1MgbWVkaWFuIGhvdXNlaG9sZCBpbmNvbWUgKEIxOTAxMykgYnkgTWV0cm8gKENCU0EpIC0tLQ0KZ2V0X2Fjc19tZWRpYW5faGhfaW5jb21lX21ldHJvIDwtIGZ1bmN0aW9uKHllYXIgPSAyMDIzKSB7DQogIGRmIDwtIGdldF9hY3MoDQogICAgZ2VvZ3JhcGh5ID0gIm1ldHJvcG9saXRhbiBzdGF0aXN0aWNhbCBhcmVhL21pY3JvcG9saXRhbiBzdGF0aXN0aWNhbCBhcmVhIiwNCiAgICB0YWJsZSA9ICJCMTkwMTMiLA0KICAgIHllYXIgPSB5ZWFyLA0KICAgIHN1cnZleSA9ICJhY3M1IiwNCiAgICBjYWNoZV90YWJsZSA9IFRSVUUNCiAgKSAlPiUNCiAgICB0cmFuc211dGUoDQogICAgICB5ZWFyID0geWVhciwNCiAgICAgIGdlb19pZCA9IEdFT0lELA0KICAgICAgcmVnaW9uID0gTkFNRSwNCiAgICAgIGluY29tZV9tZWRpYW5faGggPSBlc3RpbWF0ZSwNCiAgICAgIG1vZSA9IG1vZQ0KICAgICkNCg0KICBkZg0KfQ0KDQojLS0tUHVsbCBkYXRhIC0tLQ0KWUVBUiA8LSAyMDIzDQppbmNvbWVfbWV0cm8gPC0gZ2V0X2Fjc19tZWRpYW5faGhfaW5jb21lX21ldHJvKHllYXIgPSBZRUFSKQ0KDQojLS0tVmVyaWZpY2F0aW9uLS0tDQpzdG9waWZub3QobnJvdyhpbmNvbWVfbWV0cm8pID4gMCkNCnN0b3BpZm5vdChhbGwoYygiZ2VvX2lkIiwicmVnaW9uIiwiaW5jb21lX21lZGlhbl9oaCIpICVpbiUgbmFtZXMoaW5jb21lX21ldHJvKSkpDQoNCnZlcmlmaWNhdGlvbiA8LSBpbmNvbWVfbWV0cm8gJT4lDQogIHN1bW1hcmlzZSgNCiAgICB5ZWFyICAgPSBmaXJzdCh5ZWFyKSwNCiAgICBuX3Jvd3MgPSBuKCksDQogICAgbl9taXNzaW5nX2luY29tZSA9IHN1bShpcy5uYShpbmNvbWVfbWVkaWFuX2hoKSksDQogICAgbWluX2luY29tZSA9IG1pbihpbmNvbWVfbWVkaWFuX2hoLCBuYS5ybSA9IFRSVUUpLA0KICAgIG1heF9pbmNvbWUgPSBtYXgoaW5jb21lX21lZGlhbl9oaCwgbmEucm0gPSBUUlVFKQ0KICApDQoNCnByaW50KGhlYWQoaW5jb21lX21ldHJvLCA1KSkNCnByaW50KHZlcmlmaWNhdGlvbikNCg0KIy0tLVNhdmUgdG8gcmF3LS0tDQpvdXRfcGF0aCA8LSBmaWxlLnBhdGgoIi4vZGF0YS9yYXciLHBhc3RlMCgiYWM1X21lZGlhbl9oaF9pbmNvbWVfbWV0cm9fIixZRUFSLCIuY3N2IikpDQp3cml0ZV9jc3YoaW5jb21lX21ldHJvLCBvdXRfcGF0aCkNCg0KbWVzc2FnZSgiU2F2ZWQ6Iiwgb3V0X3BhdGgpDQoNCmBgYA0KDQojIyBVcGRhdGVkIGluZ2VzdGlvbiAobXVsdGkteWVhciBBQ1M1IGluY29tZSBieSBtZXRybykNCg0KYGBge3J9DQpsaWJyYXJ5KHRpZHl2ZXJzZSkNCmxpYnJhcnkodGlkeWNlbnN1cykNCmxpYnJhcnkocmVhZHIpDQoNCiMgLS0tIEhlbHBlcjogQUNTIG1lZGlhbiBob3VzZWhvbGQgaW5jb21lIChCMTkwMTMpIGJ5IE1ldHJvIChDQlNBKSwgbXVsdGkteWVhciAtLS0NCmdldF9hY3NfbWVkaWFuX2hoX2luY29tZV9tZXRybyA8LSBmdW5jdGlvbih5ZWFycyA9IDIwMDk6MjAyMykgew0KICBzdG9waWZub3QobGVuZ3RoKHllYXJzKSA+IDApDQoNCiAgZGYgPC0gcHVycnI6Om1hcF9kZnIoeWVhcnMsIGZ1bmN0aW9uKHkpIHsNCiAgICBnZXRfYWNzKA0KICAgICAgZ2VvZ3JhcGh5ID0gIm1ldHJvcG9saXRhbiBzdGF0aXN0aWNhbCBhcmVhL21pY3JvcG9saXRhbiBzdGF0aXN0aWNhbCBhcmVhIiwNCiAgICAgIHRhYmxlID0gIkIxOTAxMyIsDQogICAgICB5ZWFyID0geSwNCiAgICAgIHN1cnZleSA9ICJhY3M1IiwNCiAgICAgIGNhY2hlX3RhYmxlID0gVFJVRQ0KICAgICkgJT4lDQogICAgICB0cmFuc211dGUoDQogICAgICAgIHllYXIgPSB5LA0KICAgICAgICBnZW9faWQgPSBHRU9JRCwNCiAgICAgICAgcmVnaW9uID0gTkFNRSwNCiAgICAgICAgaW5jb21lX21lZGlhbl9oaCA9IGVzdGltYXRlLA0KICAgICAgICBtb2UgPSBtb2UNCiAgICAgICkNCiAgfSkNCg0KICBkZg0KfQ0KDQojIC0tLSBQdWxsIGRhdGEgLS0tDQpZRUFSUyA8LSAyMDA5OjIwMjMNCmluY29tZV9tZXRybyA8LSBnZXRfYWNzX21lZGlhbl9oaF9pbmNvbWVfbWV0cm8oeWVhcnMgPSBZRUFSUykNCg0KIyAtLS0gVmVyaWZpY2F0aW9uIC0tLQ0Kc3RvcGlmbm90KG5yb3coaW5jb21lX21ldHJvKSA+IDApDQpzdG9waWZub3QoYWxsKGMoInllYXIiLCAiZ2VvX2lkIiwgInJlZ2lvbiIsICJpbmNvbWVfbWVkaWFuX2hoIikgJWluJSBuYW1lcyhpbmNvbWVfbWV0cm8pKSkNCg0KdmVyaWZpY2F0aW9uIDwtIGluY29tZV9tZXRybyAlPiUNCiAgc3VtbWFyaXNlKA0KICAgIHllYXJfbWluID0gbWluKHllYXIpLA0KICAgIHllYXJfbWF4ID0gbWF4KHllYXIpLA0KICAgIG5fcm93cyA9IG4oKSwNCiAgICBuX3JlZ2lvbnMgPSBuX2Rpc3RpbmN0KHJlZ2lvbiksDQogICAgbl9taXNzaW5nX2luY29tZSA9IHN1bShpcy5uYShpbmNvbWVfbWVkaWFuX2hoKSksDQogICAgbWluX2luY29tZSA9IG1pbihpbmNvbWVfbWVkaWFuX2hoLCBuYS5ybSA9IFRSVUUpLA0KICAgIG1heF9pbmNvbWUgPSBtYXgoaW5jb21lX21lZGlhbl9oaCwgbmEucm0gPSBUUlVFKQ0KICApDQoNCnByaW50KGhlYWQoaW5jb21lX21ldHJvLCA1KSkNCnByaW50KHZlcmlmaWNhdGlvbikNCg0KIyBPcHRpb25hbDogdmVyaWZ5IGNvdmVyYWdlIGJ5IHllYXINCmNvdmVyYWdlX2J5X3llYXIgPC0gaW5jb21lX21ldHJvICU+JQ0KICBncm91cF9ieSh5ZWFyKSAlPiUNCiAgc3VtbWFyaXNlKA0KICAgIG5fcmVnaW9ucyA9IG5fZGlzdGluY3QocmVnaW9uKSwNCiAgICBwY3RfbWlzc2luZ19pbmNvbWUgPSBtZWFuKGlzLm5hKGluY29tZV9tZWRpYW5faGgpKSwNCiAgICAuZ3JvdXBzID0gImRyb3AiDQogICkNCg0KcHJpbnQoaGVhZChjb3ZlcmFnZV9ieV95ZWFyLCAxMCkpDQoNCiMgLS0tIFNhdmUgdG8gcmF3IC0tLQ0KZGlyLmNyZWF0ZSgiZGF0YS9yYXciLCByZWN1cnNpdmUgPSBUUlVFLCBzaG93V2FybmluZ3MgPSBGQUxTRSkNCg0Kb3V0X3BhdGggPC0gZmlsZS5wYXRoKA0KICAiZGF0YS9yYXciLA0KICBwYXN0ZTAoImFjczVfbWVkaWFuX2hoX2luY29tZV9tZXRyb18iLCBtaW4oWUVBUlMpLCAiXyIsIG1heChZRUFSUyksICIuY3N2IikNCikNCg0Kd3JpdGVfY3N2KGluY29tZV9tZXRybywgb3V0X3BhdGgpDQptZXNzYWdlKCJTYXZlZDogIiwgb3V0X3BhdGgpDQoNCmBgYA0K