diff --git a/.github/workflows/pr-compare.yml b/.github/workflows/pr-compare.yml new file mode 100644 index 000000000..f72005358 --- /dev/null +++ b/.github/workflows/pr-compare.yml @@ -0,0 +1,25 @@ +name: PR Compare +on: + pull_request: + types: [opened, synchronize, reopened] + branches: ['main'] + paths: + - models/** + - seeds/** + - tests/** + +jobs: + piperider-compare: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - uses: actions/checkout@v3 + + - name: PipeRider Compare + uses: InfuseAI/piperider-compare-action@v1 + with: + cloud_api_token: ${{ secrets.PIPERIDER_CLOUD_TOKEN_ID }} + cloud_project: ${{ secrets.PIPERIDER_API_PROJECT }} + upload: true + share: true diff --git a/.github/workflows/pr-comparison-job-slim-ci.yml b/.github/workflows/pr-comparison-job-slim-ci.yml new file mode 100644 index 000000000..e9c3cb1a0 --- /dev/null +++ b/.github/workflows/pr-comparison-job-slim-ci.yml @@ -0,0 +1,81 @@ +name: PR Transformation and Comparison (Slim CI) + +on: + pull_request: + types: [opened, synchronize, reopened] + # Trigger when PR is created and/or head of branch is updated. + branches: [ "main" ] + # Only PR target is `main` + paths: + - models/** + - seeds/** + - tests/** + +env: + SNOWFLAKE_ACCOUNT: ${{ secrets.SNOWFLAKE_ACCOUNT }} + SNOWFLAKE_USER: ${{ secrets.SNOWFLAKE_USER }} + SNOWFLAKE_PASSWORD: ${{ secrets.SNOWFLAKE_PASSWORD }} + SNOWFLAKE_ROLE: ${{ secrets.SNOWFLAKE_ROLE }} + SNOWFLAKE_DATABASE: ${{ secrets.SNOWFLAKE_DATABASE }} + SNOWFLAKE_WAREHOUSE: ${{ secrets.SNOWFLAKE_WAREHOUSE }} + SNOWFLAKE_SCHEMA: ${{ secrets.SNOWFLAKE_SCHEMA }} + DBT_CLOUD_API_TOKEN: ${{ secrets.DBT_CLOUD_API_TOKEN }} + DBT_CLOUD_ACCOUNT_ID: ${{ secrets.DBT_CLOUD_ACCOUNT_ID }} + DBT_CLOUD_JOB_ID: 173664 + PIPERIDER_CLOUD_TOKEN_ID: ${{ secrets.PIPERIDER_CLOUD_TOKEN_ID }} + PIPERIDER_CLOUD_PROJECT_ID: ${{ secrets.PIPERIDER_CLOUD_PROJECT_ID }} + PIPERIDER_API_PROJECT: ${{ secrets.PIPERIDER_API_PROJECT }} + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + + - name: Setup Piperider + run: | + pip install dbt-cloud-cli + pip install dbt-snowflake + pip install 'piperider-nightly[snowflake]' + + - name: Trigger DBT Cloud Job (PR Job) + run: | + dbt-cloud job run \ + --cause "GitHub PR Job" \ + --git-branch ${{ github.head_ref }} \ + --schema-override ${SNOWFLAKE_SCHEMA} \ + --wait \ + --file response.json + echo "run_id=$(cat response.json | jq -r '.data.id')" >> $GITHUB_ENV + + - name: Piperider Cloud Login + run: piperider cloud login --token $PIPERIDER_CLOUD_TOKEN_ID --no-interaction + + - name: Profile on PR environment + run: | + mkdir -p state/pr + dbt-cloud run get-artifact --run-id ${{ env.run_id }} --path manifest.json -f state/pr/manifest.json + dbt-cloud run get-artifact --run-id ${{ env.run_id }} --path run_results.json -f state/pr/run_results.json + piperider run --datasource jaffle_shop_pr --dbt-state state/pr --dbt-run-results -o /tmp/pipe-target + piperider cloud upload-report --run .piperider/outputs/latest/run.json + + - name: Generate Comparison Summary + run: | + piperider cloud compare-reports \ + --base datasource:jaffle_shop \ + --target datasource:jaffle_shop_pr \ + --summary-file summary.md \ + --tables-from target-only + + - name: Create PR Comment + uses: peter-evans/create-or-update-comment@v2.1.0 + with: + issue-number: ${{ github.event.number }} + body-file: summary.md + diff --git a/.github/workflows/pr-comparison-job.yml b/.github/workflows/pr-comparison-job.yml new file mode 100644 index 000000000..4a01248e7 --- /dev/null +++ b/.github/workflows/pr-comparison-job.yml @@ -0,0 +1,74 @@ +name: PR Transformation and Comparison +on: + pull_request: + types: [opened, synchronize, reopened] + branches: ['main'] + paths: + - models/** + - seeds/** + - tests/** + +env: + SNOWFLAKE_ACCOUNT: ${{ secrets.SNOWFLAKE_ACCOUNT }} + SNOWFLAKE_USER: ${{ secrets.SNOWFLAKE_USER }} + SNOWFLAKE_PASSWORD: ${{ secrets.SNOWFLAKE_PASSWORD }} + SNOWFLAKE_ROLE: ${{ secrets.SNOWFLAKE_ROLE }} + SNOWFLAKE_DATABASE: ${{ secrets.SNOWFLAKE_DATABASE }} + SNOWFLAKE_WAREHOUSE: ${{ secrets.SNOWFLAKE_WAREHOUSE }} + SNOWFLAKE_SCHEMA: ${{ secrets.SNOWFLAKE_SCHEMA }} + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + + - name: Setup DBT + Piperider + run: | + pip install dbt-snowflake + pip install piperider['snowflake'] + + - name: Run DBT on PR environment + run: dbt build --target jaffle_shop_pr + + - name: Profile on PR environment + run: piperider run --datasource jaffle_shop_pr --dbt-state target -o /tmp/pr-report/ + + - name: Profile on PROD environment + run: piperider run --datasource jaffle_shop -o /tmp/prod-report/ + + - name: Compare and Prepare + run: | + piperider compare-reports \ + --debug \ + --base /tmp/prod-report/run.json \ + --target /tmp/pr-report/run.json \ + --output comparison-report/ \ + --tables-from target-only + zip -r comparison-report.zip comparison-report/ + + - uses: actions/upload-artifact@v3 + with: + name: comparison-report-artifact + path: comparison-report.zip + + - name: Prepare Comparison Summary + run: | + echo "# :bar_chart: Piperider Comparison Summary" > summary.md + cat comparison-report/summary.md >> summary.md + echo -e "\n" >> summary.md + echo "## :paperclip: Generated Comparison Report ZIP" >> summary.md + echo "Find it in the [Github Action Runs Page](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" >> summary.md + + - name: Create PR Comment + uses: peter-evans/create-or-update-comment@v2.1.0 + with: + issue-number: ${{ github.event.number }} + body-file: summary.md + diff --git a/.github/workflows/production-daily-job-slim-ci.yml b/.github/workflows/production-daily-job-slim-ci.yml new file mode 100644 index 000000000..66feab19c --- /dev/null +++ b/.github/workflows/production-daily-job-slim-ci.yml @@ -0,0 +1,69 @@ +name: Daily Production Transformation (Slim CI) +on: + schedule: + - cron: '0 18 * * *' # run at 2 AM (UTC + 8) every day + workflow_dispatch: + pull_request: + types: + - closed + branches: ['main'] + paths: + - models/** + - seeds/** + - tests/** + +env: + SNOWFLAKE_ACCOUNT: ${{ secrets.SNOWFLAKE_ACCOUNT }} + SNOWFLAKE_USER: ${{ secrets.SNOWFLAKE_USER }} + SNOWFLAKE_PASSWORD: ${{ secrets.SNOWFLAKE_PASSWORD }} + SNOWFLAKE_ROLE: ${{ secrets.SNOWFLAKE_ROLE }} + SNOWFLAKE_DATABASE: ${{ secrets.SNOWFLAKE_DATABASE }} + SNOWFLAKE_WAREHOUSE: ${{ secrets.SNOWFLAKE_WAREHOUSE }} + DBT_CLOUD_API_TOKEN: ${{ secrets.DBT_CLOUD_API_TOKEN }} + DBT_CLOUD_ACCOUNT_ID: ${{ secrets.DBT_CLOUD_ACCOUNT_ID }} + DBT_CLOUD_JOB_ID: 173663 + PIPERIDER_CLOUD_TOKEN_ID: ${{ secrets.PIPERIDER_CLOUD_TOKEN_ID }} + PIPERIDER_API_PROJECT: ${{ secrets. PIPERIDER_API_PROJECT }} + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + + - name: Setup DBT + Piperider + run: | + pip install dbt-cloud-cli + pip install dbt-snowflake + pip install piperider['snowflake'] + + - name: Run DBT on PROD environment + run: | + dbt-cloud job run --wait --file response.json + echo "run_id=$(cat response.json | jq -r '.data.id')" >> $GITHUB_ENV + + - name: Piperider Cloud Login + run: piperider cloud login --token $PIPERIDER_CLOUD_TOKEN_ID --no-interaction + + - name: Profile on PROD environment + run: | + mkdir -p state/prod + dbt-cloud run get-artifact --run-id ${{ env.run_id }} --path manifest.json -f state/prod/manifest.json + dbt-cloud run get-artifact --run-id ${{ env.run_id }} --path run_results.json -f state/prod/run_results.json + piperider run --datasource jaffle_shop --dbt-state state/prod -o report/ + piperider cloud upload-report --run .piperider/outputs/latest/run.json + + - name: Create run artifact + run: zip -r run-report.zip report/ + + - name: Upload profiling result + uses: actions/upload-artifact@v3 + with: + name: run-report-artifact + path: run-report.zip diff --git a/.github/workflows/production-daily-job.yml b/.github/workflows/production-daily-job.yml new file mode 100644 index 000000000..1e37c1364 --- /dev/null +++ b/.github/workflows/production-daily-job.yml @@ -0,0 +1,54 @@ +name: Daily Production Transformation +on: + schedule: + - cron: '0 18 * * *' # run at 2 AM (UTC + 8) every day + workflow_dispatch: + pull_request: + types: + - closed + branches: ['main'] + paths: + - models/** + - seeds/** + - tests/** + +env: + SNOWFLAKE_ACCOUNT: ${{ secrets.SNOWFLAKE_ACCOUNT }} + SNOWFLAKE_USER: ${{ secrets.SNOWFLAKE_USER }} + SNOWFLAKE_PASSWORD: ${{ secrets.SNOWFLAKE_PASSWORD }} + SNOWFLAKE_ROLE: ${{ secrets.SNOWFLAKE_ROLE }} + SNOWFLAKE_DATABASE: ${{ secrets.SNOWFLAKE_DATABASE }} + SNOWFLAKE_WAREHOUSE: ${{ secrets.SNOWFLAKE_WAREHOUSE }} + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + + - name: Setup DBT + Piperider + run: | + pip install dbt-snowflake + pip install piperider['snowflake'] + + - name: Run DBT on PROD environment + run: dbt build --target jaffle_shop + + - name: Profile on PROD environment + run: piperider run --datasource jaffle_shop --dbt-state target -o report/ + + - name: Create run artifact + run: zip -r run-report.zip report/ + + - name: Upload profiling result + uses: actions/upload-artifact@v3 + with: + name: run-report-artifact + path: run-report.zip + diff --git a/.gitignore b/.gitignore index 716442207..94bdc7656 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,7 @@ target/ dbt_modules/ logs/ +venv/ **/.DS_Store +*.duckdb + diff --git a/.piperider/.gitignore b/.piperider/.gitignore new file mode 100644 index 000000000..e015b8d38 --- /dev/null +++ b/.piperider/.gitignore @@ -0,0 +1,8 @@ +**.log +__pycache__/ +logs/ +outputs/ +reports/ +comparisons/ +credentials.yml +.unsend_events.json \ No newline at end of file diff --git a/.piperider/compare/default.yml b/.piperider/compare/default.yml new file mode 100644 index 000000000..c798e0419 --- /dev/null +++ b/.piperider/compare/default.yml @@ -0,0 +1,15 @@ +base: + branch: main + dbt: + commands: + - dbt deps + - dbt build + piperider: + command: piperider run +target: + dbt: + commands: + - dbt deps + - dbt build + piperider: + command: piperider run diff --git a/.piperider/config.yml b/.piperider/config.yml new file mode 100644 index 000000000..f20bfaab9 --- /dev/null +++ b/.piperider/config.yml @@ -0,0 +1,6 @@ +dataSources: [] +dbt: + projectDir: . + +telemetry: + id: 2e470b721f5a4114aa7cf9b73eca760d diff --git a/dbt_project.yml b/dbt_project.yml index acdce4c57..8d69027de 100644 --- a/dbt_project.yml +++ b/dbt_project.yml @@ -22,5 +22,6 @@ require-dbt-version: [">=1.0.0", "<2.0.0"] models: jaffle_shop: materialized: table - staging: + +tags: piperider + staging: materialized: view diff --git a/models/customers.sql b/models/int_customer_order_history_joined.sql similarity index 96% rename from models/customers.sql rename to models/int_customer_order_history_joined.sql index 016a004fe..9aedd70a2 100644 --- a/models/customers.sql +++ b/models/int_customer_order_history_joined.sql @@ -34,7 +34,7 @@ customer_payments as ( select orders.customer_id, - sum(amount) as total_amount + sum(amount)::bigint as total_amount from payments diff --git a/models/orders.sql b/models/int_order_payments_pivoted.sql similarity index 89% rename from models/orders.sql rename to models/int_order_payments_pivoted.sql index cbb293491..681a7729e 100644 --- a/models/orders.sql +++ b/models/int_order_payments_pivoted.sql @@ -18,10 +18,10 @@ order_payments as ( order_id, {% for payment_method in payment_methods -%} - sum(case when payment_method = '{{ payment_method }}' then amount else 0 end) as {{ payment_method }}_amount, + sum(case when payment_method = '{{ payment_method }}' then amount else 0 end)::bigint as {{ payment_method }}_amount, {% endfor -%} - sum(amount) as total_amount + sum(amount)::bigint as total_amount from payments diff --git a/models/docs.md b/models/marts/docs.md similarity index 100% rename from models/docs.md rename to models/marts/docs.md diff --git a/models/marts/orders.sql b/models/marts/orders.sql new file mode 100644 index 000000000..0b5ae3888 --- /dev/null +++ b/models/marts/orders.sql @@ -0,0 +1,23 @@ +with orders as ( + + select * from {{ ref('int_order_payments_pivoted') }} + +) +, +customers as ( + + select * from {{ ref('int_customer_order_history_joined') }} + +) +, +final as ( + + select + * + from orders + left join customers using (customer_id) + +) + +select * from final + diff --git a/models/marts/orders.yml b/models/marts/orders.yml new file mode 100644 index 000000000..59a0f74ba --- /dev/null +++ b/models/marts/orders.yml @@ -0,0 +1,146 @@ +version: 2 + +models: + - name: orders + description: This table has basic information about orders, as well as some derived facts based on payments + + columns: + - name: order_id + tests: + - unique + - not_null + description: This is a unique identifier for an order + + - name: customer_id + description: Foreign key to the customers table + tests: + - not_null + + - name: order_date + description: Date (UTC) that the order was placed + + - name: status + description: '{{ doc("orders_status") }}' + tests: + - accepted_values: + values: ['placed', 'shipped', 'completed', 'return_pending', 'returned'] + + - name: amount + description: Total amount (AUD) of the order + tests: + - not_null + + - name: credit_card_amount + description: Amount of the order (AUD) paid for by credit card + tests: + - not_null + + - name: coupon_amount + description: Amount of the order (AUD) paid for by coupon + tests: + - not_null + + - name: bank_transfer_amount + description: Amount of the order (AUD) paid for by bank transfer + tests: + - not_null + + - name: gift_card_amount + description: Amount of the order (AUD) paid for by gift card + tests: + - not_null + + - name: customer_id + description: This is a unique identifier for a customer + + - name: first_name + description: Customer's first name. PII. + + - name: last_name + description: Customer's last name. PII. + + - name: first_order + description: Date (UTC) of a customer's first order + + - name: most_recent_order + description: Date (UTC) of a customer's most recent order + + - name: number_of_orders + description: Count of the number of orders a customer has placed + + - name: total_order_amount + description: Total value (AUD) of a customer's orders + +semantic_models: + - name: orders + defaults: + agg_time_dimension: order_date + description: | + Order fact table. This table is at the order grain with one row per order. + model: ref('orders') + entities: + - name: order_id + type: primary + - name: customer + type: foreign + expr: customer_id + dimensions: + - name: order_date + expr: order_date + type: time + type_params: + time_granularity: day + - name: status + type: categorical + measures: + - name: expense + description: "The total expenses of our jaffle business" + agg: sum + expr: amount / 4 + - name: revenue + description: "The total revenue of our jaffle business" + agg: sum + expr: amount + - name: average_order_amount + description: "The average size of a jaffle order" + agg: average + expr: amount + + +metrics: + - name: expenses + description: "The total expenses of our jaffle business" + type: simple + label: Expenses + type_params: + measure: expense + filter: | + {{ Dimension('order_id__status') }} = 'completed' + - name: revenue + description: "The total revenue of our jaffle business" + type: simple + label: Revenue + type_params: + measure: revenue + filter: | + {{ Dimension('order_id__status') }} = 'completed' + - name: average_order_amount + description: "The average size of a jaffle order" + type: simple + label: Average Order Amount + type_params: + measure: average_order_amount + - name: profit + description: "The total money we get to take home from our jaffle business" + type: derived + label: Profit + type_params: + expr: revenue - expenses + metrics: + - name: revenue + filter: | + {{ Dimension('order_id__status') }} = 'completed' + - name: expenses + filter: | + {{ Dimension('order_id__status') }} = 'completed' + diff --git a/models/metricflow_time_spine.sql b/models/metricflow_time_spine.sql new file mode 100644 index 000000000..af185aa10 --- /dev/null +++ b/models/metricflow_time_spine.sql @@ -0,0 +1,24 @@ +{{ + config( + materialized = 'table', + ) +}} + +with days as ( + + {{ + dbt_utils.date_spine( + 'day', + "strptime('01/01/2000','%m/%d/%Y')", + "strptime('01/01/2027','%m/%d/%Y')" + ) + }} + +), + +final as ( + select cast(date_day as date) as date_day + from days +) + +select * from final diff --git a/models/schema.yml b/models/schema.yml index 381349cfd..f7dcdad0c 100644 --- a/models/schema.yml +++ b/models/schema.yml @@ -1,7 +1,7 @@ version: 2 models: - - name: customers + - name: int_customer_order_history_joined description: This table has basic information about a customer, as well as some derived facts based on a customer's orders columns: @@ -29,7 +29,7 @@ models: - name: total_order_amount description: Total value (AUD) of a customer's orders - - name: orders + - name: int_order_payments_pivoted description: This table has basic information about orders, as well as some derived facts based on payments columns: @@ -44,7 +44,7 @@ models: tests: - not_null - relationships: - to: ref('customers') + to: ref('int_customer_order_history_joined') field: customer_id - name: order_date diff --git a/models/staging/stg_customers.sql b/models/staging/stg_customers.sql index cad047269..5b8b77a80 100644 --- a/models/staging/stg_customers.sql +++ b/models/staging/stg_customers.sql @@ -4,14 +4,14 @@ with source as ( Normally we would select from the table here, but we are using seeds to load our data in this project #} - select * from {{ ref('raw_customers') }} + select * from {{ ref('raw_customers_v2') }} ), renamed as ( select - id as customer_id, + customer_id, first_name, last_name diff --git a/models/staging/stg_orders.sql b/models/staging/stg_orders.sql index a654dcb94..46cbb2d98 100644 --- a/models/staging/stg_orders.sql +++ b/models/staging/stg_orders.sql @@ -18,6 +18,18 @@ renamed as ( from source +), + +-- Shift the order_date by the number of days since 2018-04-09 (the max order_date in the raw data) +shift_date as ( + + select + order_id, + customer_id, + (order_date + datediff('day', date '2018-04-09', CURRENT_DATE)::int) as order_date, + status + + from renamed ) -select * from renamed +select * from shift_date diff --git a/packages.yml b/packages.yml new file mode 100644 index 000000000..60f8dfb7a --- /dev/null +++ b/packages.yml @@ -0,0 +1,3 @@ +packages: + - package: dbt-labs/dbt_utils + version: 1.1.1 diff --git a/profiles.yml b/profiles.yml new file mode 100644 index 000000000..5fa75b6a0 --- /dev/null +++ b/profiles.yml @@ -0,0 +1,6 @@ +jaffle_shop: + target: dev + outputs: + dev: + type: duckdb + path: jaffle_shop.duckdb diff --git a/profiles.yml.snowflake b/profiles.yml.snowflake new file mode 100644 index 000000000..851bcdbf9 --- /dev/null +++ b/profiles.yml.snowflake @@ -0,0 +1,43 @@ +jaffle_shop: + target: dev + outputs: + dev: + type: snowflake + account: "{{ env_var('SNOWFLAKE_ACCOUNT') | as_text }}" + + # User/password auth + user: "{{ env_var('SNOWFLAKE_USER') | as_text }}" + password: "{{ env_var('SNOWFLAKE_PASSWORD') | as_text }}" + + role: "{{ env_var('SNOWFLAKE_ROLE') | as_text }}" + database: "{{ env_var('SNOWFLAKE_DATABASE') | as_text }}" + warehouse: "{{ env_var('SNOWFLAKE_WAREHOUSE') | as_text }}" + schema: PUBLIC + threads: 4 + jaffle_shop_pr: + type: snowflake + account: "{{ env_var('SNOWFLAKE_ACCOUNT') | as_text }}" + + # User/password auth + user: "{{ env_var('SNOWFLAKE_USER') | as_text }}" + password: "{{ env_var('SNOWFLAKE_PASSWORD') | as_text }}" + + role: "{{ env_var('SNOWFLAKE_ROLE') | as_text }}" + database: "{{ env_var('SNOWFLAKE_DATABASE') | as_text }}" + warehouse: "{{ env_var('SNOWFLAKE_WAREHOUSE') | as_text }}" + schema: "{{ env_var('SNOWFLAKE_SCHEMA') | as_text }}" + threads: 4 + jaffle_shop: + type: snowflake + account: "{{ env_var('SNOWFLAKE_ACCOUNT') | as_text }}" + + # User/password auth + user: "{{ env_var('SNOWFLAKE_USER') | as_text }}" + password: "{{ env_var('SNOWFLAKE_PASSWORD') | as_text }}" + + role: "{{ env_var('SNOWFLAKE_ROLE') | as_text }}" + database: "{{ env_var('SNOWFLAKE_DATABASE') | as_text }}" + warehouse: "{{ env_var('SNOWFLAKE_WAREHOUSE') | as_text }}" + schema: PUBLIC + threads: 4 + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..c9fa48cc7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +dbt-duckdb>=1.6 +piperider[duckdb] diff --git a/seeds/raw_customers_v2.csv b/seeds/raw_customers_v2.csv new file mode 100644 index 000000000..7f94e6f66 --- /dev/null +++ b/seeds/raw_customers_v2.csv @@ -0,0 +1,102 @@ +customer_id,first_name,last_name +1,Michael,P. +2,Shawn,M. +3,Kathleen,P. +4,Jimmy,C. +5,Katherine,R. +6,Sarah,R. +7,Martin,M. +8,Frank,R. +9,Jennifer,F. +10,Henry,W. +11,Fred,S. +12,Amy,D. +13,Kathleen,M. +14,Steve,F. +15,Teresa,H. +16,Amanda,H. +17,Kimberly,R. +18,Johnny,K. +19,Virginia,F. +20,Anna,A. +21,Willie,H. +22,Sean,H. +23,Mildred,A. +24,David,G. +25,Victor,H. +26,Aaron,R. +27,Benjamin,B. +28,Lisa,W. +29,Benjamin,K. +30,Christina,W. +31,Jane,G. +32,Thomas,O. +33,Katherine,M. +34,Jennifer,S. +35,Sara,T. +36,Harold,O. +37,Shirley,J. +38,Dennis,J. +39,Louise,W. +40,Maria,A. +41,Gloria,C. +42,Diana,S. +43,Kelly,N. +44,Jane,R. +45,Scott,B. +46,Norma,C. +47,Marie,P. +48,Lillian,C. +49,Judy,N. +50,Billy,L. +51,Howard,R. +52,Laura,F. +53,Anne,B. +54,Rose,M. +55,Nicholas,R. +56,Joshua,K. +57,Paul,W. +58,Kathryn,K. +59,Adam,A. +60,Norma,W. +61,Timothy,R. +62,Elizabeth,P. +63,Edward,G. +64,David,C. +65,Brenda,W. +66,Adam,W. +67,Michael,H. +68,Jesse,E. +69,Janet,P. +70,Helen,F. +71,Gerald,C. +72,Kathryn,O. +73,Alan,B. +74,Harry,A. +75,Andrea,H. +76,Barbara,W. +77,Anne,W. +78,Harry,H. +79,Jack,R. +80,Phillip,H. +81,Shirley,H. +82,Arthur,D. +83,Virginia,R. +84,Christina,R. +85,Theresa,M. +86,Jason,C. +87,Phillip,B. +88,Adam,T. +89,Margaret,J. +90,Paul,P. +91,Todd,W. +92,Willie,O. +93,Frances,R. +94,Gregory,H. +95,Lisa,P. +96,Jacqueline,A. +97,Shirley,D. +98,Nicole,M. +99,Mary,G. +100,Jean,M. +101,CL,K.