├── pytest.ini ├── resource └── wbryamlgenerator-0.1.tar.gz ├── src ├── web │ ├── static │ │ ├── workingbackwards_rgb_logo_wide_blue.png │ │ ├── icons │ │ │ ├── json.svg │ │ │ └── publish.svg │ │ ├── unit_test_wbr.html │ │ ├── demo_uploads │ │ │ ├── 2-wbr-sample-config-with-filter.yaml │ │ │ └── 1-wbr-sample-config.yaml │ │ ├── unit_test_wbr.js │ │ ├── wbr.css │ │ └── wbr.html │ └── templates │ │ └── login.html ├── unit_test_case │ ├── scenario_3 │ │ ├── config.yaml │ │ └── testconfig.yml │ ├── scenario_2 │ │ ├── config.yaml │ │ └── testconfig.yml │ ├── scenario_8 │ │ ├── config.yaml │ │ └── testconfig.yml │ ├── scenario_1 │ │ ├── config.yaml │ │ └── testconfig.yml │ ├── scenario_9 │ │ ├── testconfig.yml │ │ └── config.yaml │ ├── scenario_7 │ │ ├── config.yaml │ │ └── testconfig.yml │ ├── scenario_5 │ │ ├── config.yaml │ │ └── testconfig.yml │ ├── scenario_6 │ │ ├── testconfig.yml │ │ └── config.yaml │ └── scenario_4 │ │ ├── config.yaml │ │ └── testconfig.yml ├── validator.py ├── publish_utility.py └── controller.py ├── deploy.sh ├── requirements.txt ├── Dockerfile ├── .gitignore ├── docs └── API_DOCUMENTATION.md ├── README.md └── LICENSE.md /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | log_cli = true 3 | filterwarnings = 4 | ignore::DeprecationWarning 5 | ignore::FutureWarning 6 | -------------------------------------------------------------------------------- /resource/wbryamlgenerator-0.1.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/working-backwards/wbr-app/HEAD/resource/wbryamlgenerator-0.1.tar.gz -------------------------------------------------------------------------------- /src/web/static/workingbackwards_rgb_logo_wide_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/working-backwards/wbr-app/HEAD/src/web/static/workingbackwards_rgb_logo_wide_blue.png -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | git reset --hard 2 | git pull 3 | docker build -t wbr:dev . 4 | docker rm -f wbr || true 5 | docker run -d -v ~/.aws:/root/.aws/ --env environment='organisation' --name wbr -p 5001:5001 wbr:dev 6 | 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | python-dateutil==2.9.0 2 | pytz==2024.2 3 | PyYAML==6.0.2 4 | six==1.16.0 5 | pandas==2.2.3 6 | numpy==2.1.2 7 | matplotlib==3.9.2 8 | flask==3.0.3 9 | Werkzeug==3.0.6 10 | pytest==8.3.3 11 | setuptools==75.2.0 12 | flask_cors==5.0.0 13 | fiscalyear==0.4.0 14 | cryptography==44.0.1 15 | boto3==1.35.49 16 | google-cloud-storage==2.18.2 17 | azure-storage-blob==12.23.1 18 | azure-identity==1.19.0 19 | waitress==3.0.1 -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12.2-bookworm 2 | 3 | WORKDIR /python-docker 4 | 5 | COPY requirements.txt requirements.txt 6 | COPY resource/wbryamlgenerator-0.1.tar.gz resource/wbryamlgenerator-0.1.tar.gz 7 | RUN python -m pip install --upgrade pip 8 | RUN pip --no-cache-dir install -r requirements.txt 9 | RUN pip install resource/wbryamlgenerator-0.1.tar.gz 10 | 11 | COPY . . 12 | 13 | EXPOSE 5001 14 | 15 | CMD ["waitress-serve", "--port=5001", "src.controller:app"] 16 | -------------------------------------------------------------------------------- /src/web/static/icons/json.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/web/static/icons/publish.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/web/static/unit_test_wbr.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |
10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/unit_test_case/scenario_3/config.yaml: -------------------------------------------------------------------------------- 1 | setup: 2 | week_ending: 25-SEP-2021 3 | week_number: 38 4 | title: WBR Daily 5 | fiscal_year_end_month: MAY # set to last month of fiscal year. If no value is provided, DEC is used. 6 | block_starting_number: 1 # Default is 1. Use this if you need multiple yaml files for a single WBR deck. 7 | 8 | metrics: 9 | PageViews: 10 | column: "PageViews" 11 | aggf: sum 12 | 13 | deck: 14 | - block: 15 | ui_type: 6_12Graph 16 | title: PageViews 17 | x_axis_monthly_display: trailing_twelve_months 18 | metrics: 19 | PageViews: 20 | graph_prior_year_flag: true 21 | legend_name: Page Views -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # These are some examples of commonly ignored file patterns. 2 | # You should customize this list as applicable to your project. 3 | # Learn more about .gitignore: 4 | # https://www.atlassian.com/git/tutorials/saving-changes/gitignore 5 | 6 | # Node artifact files 7 | node_modules/ 8 | dist/ 9 | 10 | # Compiled Java class files 11 | *.class 12 | 13 | # Compiled Python bytecode 14 | *.py[cod] 15 | 16 | # Log files 17 | *.log 18 | 19 | # Package files 20 | *.jar 21 | 22 | # Maven 23 | target/ 24 | dist/ 25 | flask_session 26 | # JetBrains IDE 27 | .idea/ 28 | 29 | # Unit test reports 30 | TEST*.xml 31 | 32 | # Generated by MacOS 33 | .DS_Store 34 | 35 | # Generated by Windows 36 | Thumbs.db 37 | 38 | # Applications 39 | *.app 40 | *.exe 41 | *.war 42 | 43 | # Large media files 44 | *.mp4 45 | *.tiff 46 | *.avi 47 | *.flv 48 | *.mov 49 | *.wmv 50 | 51 | # flask monitoring db 52 | flask_monitoringdashboard.db 53 | 54 | venv/ 55 | .venv 56 | site/ 57 | 58 | /publish/ 59 | -------------------------------------------------------------------------------- /src/unit_test_case/scenario_2/config.yaml: -------------------------------------------------------------------------------- 1 | setup: 2 | week_ending: 09-JAN-2021 3 | week_number: 1 4 | title: WBR Daily 5 | fiscal_year_end_month: DEC # set to last month of fiscal year. If no value is provided, DEC is used. 6 | block_starting_number: 1 # Default is 1. Use this if you need multiple yaml files for a single WBR deck. 7 | 8 | metrics: 9 | PageViewsFiscal: 10 | column: "PageViews" 11 | aggf: sum 12 | PageViewsTrailing: 13 | column: "PageViews" 14 | aggf: sum 15 | 16 | deck: 17 | - block: 18 | ui_type: 6_12Graph 19 | title: PageViewsFiscal 20 | x_axis_monthly_display: fiscal_year 21 | metrics: 22 | PageViewsFiscal: 23 | graph_prior_year_flag: true 24 | legend_name: Page Views 25 | - block: 26 | ui_type: 6_12Graph 27 | title: PageViewsTrailing 28 | x_axis_monthly_display: trailing_twelve_months 29 | metrics: 30 | PageViewsTrailing: 31 | graph_prior_year_flag: true 32 | legend_name: Page Views -------------------------------------------------------------------------------- /src/unit_test_case/scenario_8/config.yaml: -------------------------------------------------------------------------------- 1 | # There are three sections in a WBR yaml file: setup, metrics, and deck. 2 | # 3 | # The setup: section defines the information you need to start building the WBR deck. 4 | # The only two required values are week_ending and week_number. 5 | 6 | setup: 7 | week_ending: 25-SEP-2021 8 | week_number: 38 9 | title: WBR Daily 10 | fiscal_year_end_month: DEC # set to last month of fiscal year. If no value is provided, DEC is used. 11 | block_starting_number: 1 # Default is 1. Use this if you need multiple yaml files for a single WBR deck. 12 | x_axis_monthly_display: fiscal_year 13 | 14 | metrics: 15 | PageViews: 16 | column: "PageViews" 17 | aggf: sum 18 | PageViewsFiscalInSetup: 19 | column: "PageViews" 20 | aggf: sum 21 | 22 | deck: 23 | - block: 24 | ui_type: 6_12Graph 25 | title: PageViewsFiscalInSetup 26 | metrics: 27 | PageViewsFiscalInSetup: 28 | line_style: primary 29 | graph_prior_year_flag: true 30 | - block: 31 | ui_type: 6_12Graph 32 | title: PageViews 33 | x_axis_monthly_display: "trailing_twelve_months" 34 | metrics: 35 | PageViews: 36 | line_style: primary 37 | graph_prior_year_flag: true -------------------------------------------------------------------------------- /src/unit_test_case/scenario_1/config.yaml: -------------------------------------------------------------------------------- 1 | # There are three sections in a WBR yaml file: setup, metrics, and deck. 2 | # 3 | # The setup: section defines the information you need to start building the WBR deck. 4 | # The only two required values are week_ending and week_number. 5 | 6 | setup: 7 | week_ending: 25-SEP-2021 8 | week_number: 38 9 | title: WBR Daily 10 | fiscal_year_end_month: DEC # set to last month of fiscal year. If no value is provided, DEC is used. 11 | block_starting_number: 1 # Default is 1. Use this if you need multiple yaml files for a single WBR deck. 12 | 13 | metrics: 14 | PageViewsFiscal: 15 | column: "PageViews" 16 | aggf: sum 17 | PageViewsTrailing: 18 | column: "PageViews" 19 | aggf: sum 20 | 21 | 22 | deck: 23 | - block: 24 | ui_type: 6_12Graph 25 | title: PageViewsFiscal 26 | x_axis_monthly_display: fiscal_year 27 | metrics: 28 | PageViewsFiscal: 29 | graph_prior_year_flag: true 30 | legend_name: Page Views 31 | - block: 32 | ui_type: 6_12Graph 33 | title: PageViewsTrailing 34 | x_axis_monthly_display: trailing_twelve_months 35 | metrics: 36 | PageViewsTrailing: 37 | graph_prior_year_flag: true 38 | legend_name: Page Views -------------------------------------------------------------------------------- /src/unit_test_case/scenario_3/testconfig.yml: -------------------------------------------------------------------------------- 1 | tests: 2 | - test: 3 | test_case_no : 1 4 | x_axis_monthly_display : "trailing_twelve_months" 5 | week_ending: 25-SEP-2021 6 | fiscal_year_end_month: MAY 7 | metric_name : "PageViews" 8 | 9 | # 1 10 | cy_6_weeks : [496725868, 499671126, 464148871, 457477195, 460207741, 470759324] 11 | 12 | # 2 13 | py_6_weeks : [429282793, 439825733, 453443686, 402629589, 383456304, 383306638] 14 | 15 | # 3 16 | cy_monthly : [1708890199, 1807907694, 1820822982, 1922478407, 2318880564, 1969227848, 2117874905, 2036021000, 17 | 2057839090, 2051219932, 2274329968, 2207577942] 18 | 19 | # 4 test 20 | py_monthly : [!!float .nan, !!float .nan, !!float .nan, !!float .nan, 1487877710, 1508255989, 1665665379, 1542530033, 21 | 1546099414, 1467102281, 1615446881,1953063065] 22 | 23 | # 5 test 24 | x_axis: [ "wk 33", "wk 34", "wk 35", "wk 36", "wk 37", "wk 38", "Sep", "Oct", "Nov", "Dec", "Jan", 25 | "Feb", "Mar", "Apr", "May" , "Jun", "Jul", "Aug"] 26 | 27 | # 6 test 28 | box_totals : [470759324.0, 2.29278694379893, 22.82, 1632139558.0, 15.35, 1632139558, 29 | 15.35, 8165267400, 26.58191843] 30 | 31 | # 7 test 32 | cy_monthly_data_frame_length : 17 33 | # 8 test 34 | py_monthly_data_frame_length : 21 35 | -------------------------------------------------------------------------------- /src/unit_test_case/scenario_8/testconfig.yml: -------------------------------------------------------------------------------- 1 | tests: 2 | - test: 3 | test_case_no : 1 4 | x_axis_monthly_display: "trailing_twelve_months" 5 | week_ending: 25-SEP-2021 6 | fiscal_year_end_month: DEC 7 | metric_name: "PageViews" 8 | 9 | # 1 test 10 | cy_6_weeks: [ 496725868, 499671126, 464148871, 457477195, 460207741, 470759324 ] 11 | 12 | # 2 13 | py_6_weeks: [ 429282793, 439825733, 453443686, 402629589, 383456304, 383306638 ] 14 | 15 | cy_monthly: [ 1708890199, 1807907694, 1820822982, 1922478407, 2318880564, 1969227848, 2117874905, 2036021000, 2057839090, 16 | 2051219932, 2274329968, 2207577942 ] 17 | 18 | # 4 19 | py_monthly: [ !!float .nan, !!float .nan, !!float .nan, !!float .nan, 20 | 1487877710, 1508255989, 1665665379, 1542530033.0, 1546099414.0, 1467102281.0, 1615446881.0, 1953063065.0 ] 21 | 22 | # 5 23 | x_axis: [ "wk 33", "wk 34", "wk 35", "wk 36", "wk 37", "wk 38", "Sep", "Oct", "Nov", "Dec", "Jan", 24 | "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug" ] 25 | 26 | # 6 27 | box_totals: [ 470759324.0, 2.29278694379893, 22.81533303, 1632139558.0, 15.34821198459484, 6114047468.0, 28 | 22.686371061643484, 18665110807.0, 31.435111159660067 ] 29 | 30 | # 7 31 | cy_monthly_data_frame_length: 16 32 | # 8 33 | py_monthly_data_frame_length: 16 34 | 35 | - test: 36 | test_case_no : 2 37 | x_axis_monthly_display: "fiscal_year" 38 | week_ending: 25-SEP-2021 39 | fiscal_year_end_month: JAN 40 | metric_name: "PageViewsFiscalInSetup" 41 | 42 | # 1 test 43 | cy_6_weeks: [ 496725868, 499671126, 464148871, 457477195, 460207741, 470759324 ] 44 | 45 | # 2 46 | py_6_weeks: [ 429282793, 439825733, 453443686, 402629589, 383456304, 383306638 ] 47 | 48 | cy_monthly: [ 2318880564.0,1969227848.0,2117874905.0,2036021000.0,2057839090.0,2051219932.0,2274329968.0,2207577942.0, !!float .nan, !!float .nan, !!float .nan, !!float .nan ] 49 | 50 | # 4 51 | py_monthly: [ 1487877710.0,1508255989.0,1665665379.0,1542530033.0,1546099414.0,1467102281.0,1615446881.0,1953063065.0,1708890199.0,1807907694.0,1820822982.0,1922478407.0 ] 52 | 53 | # 5 54 | x_axis: [ "wk 33", "wk 34", "wk 35", "wk 36", "wk 37", "wk 38", "Jan", 55 | "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ] 56 | 57 | # 6 58 | box_totals: [ 470759324.0, 2.29278694379893, 22.81533303, 1632139558.0, 15.34821198459484, 6114047468.0, 59 | 22.686371061643484, 18665110807.0, 31.435111159660067 ] 60 | 61 | # 7 62 | cy_monthly_data_frame_length: 16 63 | # 8 64 | py_monthly_data_frame_length: 16 65 | 66 | -------------------------------------------------------------------------------- /src/unit_test_case/scenario_1/testconfig.yml: -------------------------------------------------------------------------------- 1 | tests: 2 | - test: 3 | test_case_no : 1 4 | x_axis_monthly_display: "fiscal_year" 5 | week_ending: 25-SEP-2021 6 | fiscal_year_end_month: DEC 7 | metric_name: "PageViewsFiscal" 8 | 9 | # 1 test 10 | cy_6_weeks: [ 496725868, 499671126, 464148871, 457477195, 460207741, 470759324 ] 11 | 12 | # 2 test 13 | py_6_weeks: [ 429282793, 439825733, 453443686, 402629589, 383456304, 383306638 ] 14 | 15 | # 3 test 16 | cy_monthly: [ 2318880564, 1969227848, 2117874905, 2036021000, 2057839090, 2051219932, 2274329968, 2207577942, 17 | !!float .nan, !!float .nan, !!float .nan, !!float .nan ] 18 | 19 | # 4 test 20 | py_monthly: [ 1487877710, 1508255989, 1665665379, 1542530033, 1546099414, 1467102281, 1615446881, 1953063065, 21 | 1708890199, 1807907694, 1820822982, 1922478407 ] 22 | 23 | # 5 test 24 | x_axis: [ "wk 33", "wk 34", "wk 35", "wk 36", "wk 37", "wk 38", "Jan", 25 | "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ] 26 | 27 | # 6 test 28 | box_totals: [ 470759324.0, 2.29278694379893, 22.81533303, 1632139558.0, 15.34821198459484, 6114047468.0, 29 | 22.686371061643484, 18665110807.0, 31.435111159660067 ] 30 | 31 | # 7 test 32 | cy_monthly_data_frame_length: 16 33 | # 8 test 34 | py_monthly_data_frame_length: 16 35 | 36 | - test: 37 | test_case_no : 2 38 | x_axis_monthly_display: "trailing_twelve_months" 39 | week_ending: 25-SEP-2021 40 | fiscal_year_end_month: DEC 41 | metric_name: "PageViewsTrailing" 42 | 43 | # 1 test 44 | cy_6_weeks: [ 496725868, 499671126, 464148871, 457477195, 460207741, 470759324 ] 45 | 46 | # 2 test 47 | py_6_weeks: [ 429282793, 439825733, 453443686, 402629589, 383456304, 383306638 ] 48 | 49 | cy_monthly: [ 1708890199, 1807907694, 1820822982, 1922478407, 2318880564, 1969227848, 2117874905, 2036021000, 2057839090, 50 | 2051219932, 2274329968, 2207577942 ] 51 | 52 | # 4 test 53 | py_monthly: [ !!float .nan, !!float .nan, !!float .nan, !!float .nan, 54 | 1487877710, 1508255989, 1665665379, 1542530033.0, 1546099414.0, 1467102281.0, 1615446881.0, 1953063065.0 ] 55 | 56 | # 5 test 57 | x_axis: [ "wk 33", "wk 34", "wk 35", "wk 36", "wk 37", "wk 38", "Sep", "Oct", "Nov", "Dec", "Jan", 58 | "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug" ] 59 | 60 | # 6 test 61 | box_totals: [ 470759324.0, 2.29278694379893, 22.81533303, 1632139558.0, 15.34821198459484, 6114047468.0, 62 | 22.686371061643484, 18665110807.0, 31.435111159660067 ] 63 | 64 | # 7 test 65 | cy_monthly_data_frame_length: 16 66 | # 8 test 67 | py_monthly_data_frame_length: 16 68 | 69 | -------------------------------------------------------------------------------- /src/unit_test_case/scenario_2/testconfig.yml: -------------------------------------------------------------------------------- 1 | tests: 2 | - test: 3 | test_case_no : 1 4 | x_axis_monthly_display: "fiscal_year" 5 | week_ending: 09-JAN-2021 6 | fiscal_year_end_month: DEC 7 | metric_name: "PageViewsFiscal" 8 | 9 | # 1 test 10 | cy_6_weeks: [ 443483878, 431021307, 435436953, 421784733, 467715742, 532485819 ] 11 | 12 | # 2 test 13 | py_6_weeks: [ !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, 395917585 ] 14 | 15 | # 3 test 16 | cy_monthly: [ !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, 17 | !!float .nan, !!float .nan, !!float .nan, !!float .nan ] 18 | 19 | # 4 test 20 | py_monthly: [ 1487877710, 1508255989, 1665665379, 1542530033, 1546099414, 1467102281, 1615446881, 1953063065, 21 | 1708890199, 1807907694, 1820822982, 1922478407 ] 22 | 23 | # 5 test 24 | x_axis: [ "wk 48", "wk 49", "wk 50", "wk 51", "wk 52", "wk 1", "Jan", "Feb", "Mar", "Apr", "May", 25 | "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ] 26 | 27 | # 6 test 28 | box_totals: [ 532485819, 13.84817127, 34.49410665, 678567325, 143.24, 678567325,143.24, 678567325, 143.24 ] 29 | 30 | # 7 test 31 | cy_monthly_data_frame_length: 24 32 | # 8 test 33 | py_monthly_data_frame_length: 24 34 | 35 | - test: 36 | test_case_no : 2 37 | x_axis_monthly_display: "trailing_twelve_months" 38 | week_ending: 09-JAN-2021 39 | fiscal_year_end_month: DEC 40 | metric_name: "PageViewsTrailing" 41 | 42 | # 1 test 43 | cy_6_weeks: [ 443483878, 431021307, 435436953, 421784733, 467715742, 532485819 ] 44 | 45 | # 2 test 46 | py_6_weeks: [ !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, 395917585 ] 47 | 48 | # 3 test 49 | cy_monthly: [ 1487877710, 1508255989, 1665665379, 1542530033, 1546099414, 1467102281, 1615446881, 1953063065, 1708890199, 50 | 1807907694, 1820822982, 1922478407 ] 51 | 52 | # 4 test 53 | py_monthly: [ !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, 54 | !!float .nan, !!float .nan, !!float .nan, !!float .nan ] 55 | 56 | # 5 test 57 | x_axis: [ "wk 48", "wk 49", "wk 50", "wk 51", "wk 52", "wk 1", "Jan", "Feb", "Mar", "Apr", "May", 58 | "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ] 59 | 60 | # 6 test 61 | box_totals: [ 532485819, 13.84817127, 34.49410665, 678567325, 143.24, 678567325, 143.24 , 678567325, 143.24 ] 62 | 63 | # 7 test 64 | cy_monthly_data_frame_length: 24 65 | 66 | # 8 test 67 | py_monthly_data_frame_length: 24 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/unit_test_case/scenario_9/testconfig.yml: -------------------------------------------------------------------------------- 1 | tests: 2 | - test: 3 | test_case_no : 1 4 | x_axis_monthly_display: "trailing_twelve_months" 5 | week_ending: 25-SEP-2021 6 | fiscal_year_end_month: DEC 7 | metric_name: "test_table" 8 | 9 | # 5 test 10 | headers: [ "Sep", "Oct", "Nov", "Dec", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug" ] 11 | Weekly404sDivideFatals: [83437.83003759582,5805.144281897814,3118.9691173195843,3701.3161373420303,24301.32008341892,1473.5284551142133,2561.5137105923277,6382.510971786834,1835.9711556905727,3426.7642017547964,4348.3821489821785, 3855.182861064639] 12 | MobilePage_Views: [818251974,855133532,878565210,910327727,1111020166,956422153,1039784713,1012715808,1030311924,996729053,1063231761,1053610475] 13 | Desktop_Pct: [0.5223333333333333,0.5274193548387097,0.517,0.5267741935483872,0.5212903225806451,0.5157142857142858,0.51,0.503,0.49935483870967745,0.5133333333333333,0.5329032258064516,0.5229032258064517] 14 | PageViewsYOY: [!!float .nan,!!float .nan,!!float .nan,!!float .nan,0.5585155610671795,0.3056323743197151,0.2714888186434474,0.3199230850891317,0.3309875622267069,0.3981437821784697,0.40786428495385474, 0.13031574943024182] 15 | PageViewsMOM: [-0.12502047188117804,0.057942572938824544,0.007143776224230125,0.055829383748408734,0.20619329484099636,-0.15078513375301206,0.07548494561001151,-0.03864907450707056,0.010716043695030653,-0.0032165576172430432,0.10876943643115888,-0.02935019409637396] 16 | MobileAndDesktopPageViews: [1708890197,1807907694,1820822981,1922478407,2318880565,1969227848,2117874905,2036021000,2057839090,2051219930,2274329967,2207577941] 17 | DesktopPageViews: [890638223,952774162,942257771,1012150680,1207860399,1012805695,1078090192,1023305192,1027527166,1054490877,1211098206,1153967466] 18 | DesktopPageViewsYOY: [!!float .nan,!!float .nan,!!float .nan,!!float .nan,0.40661289348191043,0.1874167173072585,0.14032480847866458,0.17510871331099365,0.16818696239908304,0.2186799231312231,0.24776436066503527,0.11097162643458947] 19 | DesktopPageViewsMOM: [-0.14254619447726902,0.06976563254909851,-0.0110376534329234,0.07417599636861993,0.19336026035174925,-0.16148778796083374,0.06445905401430418,-0.05081671311596536,0.004125820950588999,0.02624136070773231,0.148514636224776,-0.04717267329516628] 20 | MobilePage_ViewsYOY: [!!float .nan,!!float .nan,!!float .nan,!!float .nan,0.7658326778123006,0.4595020579685012,0.4436610082556478,0.5076624403842662,0.5458356835925258,0.6561658692012646,0.6488498799147582,0.152290374558685] 21 | MobilePage_ViewsMOM: [-0.1051115227343925,0.04507359489730978,0.02740119188777168,0.036152714264658936,0.22046174476238933,-0.13914960117834618,0.08716084182964345,-0.02603318231319296,0.017375176590509023,-0.03259485813734986,0.06672094868694467,-0.009049095740848556] 22 | 23 | # 7 test 24 | cy_monthly_data_frame_length: 16 25 | # 8 test 26 | py_monthly_data_frame_length: 16 27 | 28 | -------------------------------------------------------------------------------- /src/validator.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | import pandas as pd 4 | 5 | week_ending_date_format = '%d-%b-%Y' 6 | 7 | 8 | def check_params(config): 9 | return 'function' not in config and \ 10 | ("aggf" in config and ('column' in config or 'filter' in config)) 11 | 12 | 13 | class WBRValidator: 14 | def __init__(self, csv, cfg): 15 | self.daily_df = pd.read_csv(csv, parse_dates=['Date'], thousands=',').sort_values(by='Date') 16 | self.cfg = cfg 17 | 18 | def validate_yaml(self): 19 | self.check_week_ending() 20 | self.validate_aggf() 21 | 22 | def check_week_ending(self): 23 | """ 24 | Checks the week ending date in the configuration. 25 | 26 | Raises: 27 | Exception: If the 'week_ending' key is missing in the 'setup' section of the configuration. 28 | ValueError: If the 'week_ending' date is not in the correct format. 29 | """ 30 | if 'week_ending' not in self.cfg['setup']: 31 | raise Exception(f"Error in SETUP section for week_ending at line {self.cfg['setup']['__line__']}") 32 | try: 33 | # Validate the week ending date format 34 | datetime.strptime(self.cfg['setup']['week_ending'], week_ending_date_format) 35 | except ValueError: 36 | raise ValueError(f"week_ending is in an invalid format, example of correct format: 25-SEP-2012, at line: " 37 | f"{self.cfg['setup']['__line__']}") 38 | 39 | def validate_aggf(self): 40 | """ 41 | Validates the aggregate function (aggf) configuration for metrics. 42 | 43 | This method checks each metric's configuration for required parameters and validates 44 | the metric comparison method. 45 | 46 | Raises: 47 | KeyError: If required parameters for metric configuration are missing or if an invalid comparison method is 48 | found. 49 | """ 50 | for metric, config in self.cfg['metrics'].items(): 51 | # Skip the line indicator key 52 | if metric == '__line__': 53 | continue 54 | 55 | # Check for required metric config parameters 56 | if 'function' not in config and ( 57 | "aggf" not in config or ('column' not in config and 'filter' not in config)): 58 | raise KeyError( 59 | f"One of the required metric config parameters from the list [aggf, column, filter] is missing for" 60 | f" the metric {metric} at line: {config['__line__']}") 61 | 62 | # Validate the metric comparison method 63 | if 'metric_comparison_method' in config and config['metric_comparison_method'] != 'bps': 64 | raise KeyError( 65 | f"Invalid value provided for metric_comparison_method {config['metric_comparison_method']} for" 66 | f" the metric {metric} at line: {config['__line__']}") -------------------------------------------------------------------------------- /src/unit_test_case/scenario_7/config.yaml: -------------------------------------------------------------------------------- 1 | setup: 2 | week_ending: 25-MAR-2023 3 | week_number: 13 4 | title: PracticePlus WBR 5 | fiscal_year_end_month: DEC 6 | block_starting_number: 1 7 | tooltip: true 8 | 9 | metrics: 10 | Booked_Sessionsv2: 11 | column: Booked_Sessionsv2 12 | aggf: sum 13 | 14 | Perfect_Sessions: 15 | column: Perfect_Sessions 16 | aggf: sum 17 | 18 | Perfect_Sessions_percentFiscal: 19 | box_total_scaling: "bps" 20 | function: 21 | divide: 22 | - metric: 23 | name: Perfect_Sessions 24 | - metric: 25 | name: Booked_Sessionsv2 26 | 27 | Perfect_Sessions_percentTrailing: 28 | box_total_scaling: "bps" 29 | function: 30 | divide: 31 | - metric: 32 | name: Perfect_Sessions 33 | - metric: 34 | name: Booked_Sessionsv2 35 | 36 | Perfect_Sessions_percent_non_bpsFiscal: 37 | function: 38 | divide: 39 | - metric: 40 | name: Perfect_Sessions 41 | - metric: 42 | name: Booked_Sessionsv2 43 | Perfect_Sessions_percent_non_bpsTrailing: 44 | function: 45 | divide: 46 | - metric: 47 | name: Perfect_Sessions 48 | - metric: 49 | name: Booked_Sessionsv2 50 | 51 | deck: 52 | - block: 53 | ui_type: 6_12Graph 54 | x_axis_monthly_display: "trailing_twelve_months" 55 | title: Perfect_Sessions_percentTrailing 56 | metrics: 57 | Perfect_Sessions_percentTrailing: 58 | line_style: primary 59 | graph_prior_year_flag: true 60 | - block: 61 | ui_type: 6_12Graph 62 | title: Perfect_Sessions_percentFiscal 63 | x_axis_monthly_display: "fiscal_year" 64 | metrics: 65 | Perfect_Sessions_percentFiscal: 66 | line_style: primary 67 | graph_prior_year_flag: true 68 | - block: 69 | ui_type: 6_12Graph 70 | title: Perfect_Sessions_percent_non_bpsTrailing 71 | x_axis_monthly_display: "trailing_twelve_months" 72 | metrics: 73 | Perfect_Sessions_percent_non_bpsTrailing: 74 | line_style: primary 75 | graph_prior_year_flag: true 76 | - block: 77 | ui_type: 6_12Graph 78 | title: Perfect_Sessions_percent_non_bpsFiscal 79 | x_axis_monthly_display: "fiscal_year" 80 | metrics: 81 | Perfect_Sessions_percent_non_bpsFiscal: 82 | line_style: primary 83 | graph_prior_year_flag: true 84 | - block: 85 | ui_type: 6_12Graph 86 | title: Perfect_Sessions_percentTrailingWOW 87 | x_axis_monthly_display: "trailing_twelve_months" 88 | metrics: 89 | Perfect_Sessions_percentTrailingWOW: 90 | line_style: primary 91 | graph_prior_year_flag: false 92 | - block: 93 | ui_type: 6_12Graph 94 | title: Perfect_Sessions_percentTrailingYOY 95 | x_axis_monthly_display: "trailing_twelve_months" 96 | metrics: 97 | Perfect_Sessions_percentTrailingYOY: 98 | line_style: primary 99 | graph_prior_year_flag: false -------------------------------------------------------------------------------- /src/unit_test_case/scenario_9/config.yaml: -------------------------------------------------------------------------------- 1 | 2 | setup: 3 | week_ending: 25-SEP-2021 4 | week_number: 38 5 | title: WBR Daily 6 | fiscal_year_end_month: DEC 7 | block_starting_number: 1 8 | 9 | metrics: 10 | PageViews: 11 | column: PageViews 12 | aggf: sum 13 | MobilePage_Views: 14 | column: MobilePageViews 15 | aggf: sum 16 | DesktopPageViews: 17 | column: DesktopPageViews 18 | aggf: sum 19 | MobileAndDesktopPageViews: 20 | function: 21 | sum: 22 | - metric: 23 | name: MobilePage_Views 24 | - metric: 25 | name: DesktopPageViews 26 | Fatals: 27 | column: Fatals 28 | aggf: sum 29 | Desktop_Pct: 30 | column: Desktop Pct 31 | aggf: mean 32 | metric_comparison_method: bps 33 | Weekly404sDivideFatals: 34 | function: 35 | divide: 36 | - metric: 37 | name: MobileAndDesktopPageViews 38 | - metric: 39 | name: Fatals 40 | 41 | 42 | deck: 43 | - block: 44 | ui_type: 12_MonthsTable 45 | title: "test_table" 46 | rows: 47 | - row: 48 | header: "Weekly404sDivideFatals" 49 | metric: Weekly404sDivideFatals 50 | style: "font-weight:bold; text-align:left;" 51 | y_scaling: "##MM" 52 | - row: 53 | header: "MobilePage_Views" 54 | metric: MobilePage_Views 55 | style: "text-align:center;" 56 | y_scaling: "##MM" 57 | - row: 58 | header: "Desktop_Pct" 59 | metric: Desktop_Pct 60 | style: "text-align:center;" 61 | y_scaling: "##%" 62 | - row: 63 | header: "PageViewsYOY" 64 | metric: PageViewsYOY 65 | style: "text-align:center;" 66 | y_scaling: "##%" 67 | - row: 68 | header: "PageViewsMOM" 69 | metric: PageViewsMOM 70 | style: "text-align:center;" 71 | y_scaling: "##%" 72 | - row: 73 | header: "MobileAndDesktopPageViews" 74 | metric: MobileAndDesktopPageViews 75 | style: "font-weight:bold; text-align:left;" 76 | y_scaling: "##MM" 77 | - row: 78 | header: "DesktopPageViews" 79 | metric: DesktopPageViews 80 | style: "font-weight:bold; text-align:left;" 81 | y_scaling: "##MM" 82 | - row: 83 | header: "DesktopPageViewsYOY" 84 | metric: DesktopPageViewsYOY 85 | style: "text-align:center;" 86 | y_scaling: "##%" 87 | - row: 88 | header: "DesktopPageViewsMOM" 89 | metric: DesktopPageViewsMOM 90 | style: "text-align:center;" 91 | y_scaling: "##%" 92 | - row: 93 | header: "MobilePage_ViewsYOY" 94 | metric: MobilePage_ViewsYOY 95 | style: "text-align:center;" 96 | y_scaling: "##%" 97 | - row: 98 | header: "MobilePage_ViewsMOM" 99 | metric: MobilePage_ViewsMOM 100 | style: "text-align:center;" 101 | y_scaling: "##%" -------------------------------------------------------------------------------- /src/web/static/demo_uploads/2-wbr-sample-config-with-filter.yaml: -------------------------------------------------------------------------------- 1 | setup: 2 | title: Recruitment Deck 3 | week_ending: 26-FEB-2022 4 | week_number: 38 5 | block_starting_number: 9 6 | 7 | # Here's an example of a filtering a column in the csv file A simple query 8 | # with a single where clause evaluation is supported. More complex data filtering should be 9 | # handled upstream in the system that houses the source data. Supported filters are 10 | # 11 | # == (equal) 12 | # != (not equal) 13 | # > (greater than) 14 | # < (less than) 15 | # >= (greater than or equal) 16 | # <= (less than or equal) 17 | 18 | metrics: 19 | ApplicantsEngineering: 20 | filter: 21 | base_column: "Applicants" 22 | query: "Department == 'Engineering'" 23 | aggf: sum 24 | ApplicantsSales: 25 | filter: 26 | base_column: "Applicants" 27 | query: "Department == 'Sales'" 28 | aggf: sum 29 | ApplicantsOther: 30 | filter: 31 | base_column: "Applicants" 32 | query: "Department == 'Other'" 33 | aggf: sum 34 | 35 | deck: 36 | - block: 37 | ui_type: 6_12Graph 38 | title: Engineering Job Applications 39 | y_scaling: "##" 40 | metrics: 41 | ApplicantsEngineering: 42 | line_style: primary 43 | graph_prior_year_flag: true 44 | 45 | - block: 46 | ui_type: 6_12Graph 47 | title: Sales Job Applications 48 | y_scaling: "##" 49 | metrics: 50 | ApplicantsSales: 51 | line_style: primary 52 | graph_prior_year_flag: true 53 | 54 | - block: 55 | ui_type: 6_12Graph 56 | title: Other Job Applications 57 | y_scaling: "##" 58 | metrics: 59 | ApplicantsOther: 60 | line_style: primary 61 | graph_prior_year_flag: true 62 | 63 | - block: 64 | ui_type: section 65 | title: "Similar data, but in a table" 66 | 67 | - block: 68 | ui_type: 6_WeeksTable 69 | title: "Inbound Job Summary" 70 | rows: 71 | - row: 72 | header: "Job Applications by Role" 73 | style: "font-weight: bold; background-color: white;text-align:left;" 74 | - row: 75 | header: "Engineering" 76 | metric: ApplicantsEngineering 77 | style: "text-align:right;" 78 | y_scaling: "##" 79 | - row: 80 | header: "YOY" 81 | metric: ApplicantsEngineeringYOY 82 | style: "font-style: italic; text-align:right;" 83 | y_scaling: "##%" 84 | - row: 85 | header: "Sales" 86 | metric: ApplicantsSales 87 | style: "text-align:right;" 88 | y_scaling: "##" 89 | - row: 90 | header: "YOY" 91 | metric: ApplicantsSalesYOY 92 | style: "font-style: italic; text-align:right;" 93 | y_scaling: "##%" 94 | - row: 95 | header: "Other" 96 | metric: ApplicantsOther 97 | style: "text-align:right;" 98 | y_scaling: "##" 99 | - row: 100 | header: "YOY" 101 | metric: ApplicantsOtherYOY 102 | style: "font-style: italic; text-align:right;" 103 | y_scaling: "##%" 104 | 105 | - block: 106 | ui_type: 12_MonthsTable 107 | title: "Inbound Job Summary" 108 | rows: 109 | - row: 110 | header: "Job Applications by Role" 111 | style: "font-weight: bold; background-color: white;text-align:left;" 112 | - row: 113 | header: "Engineering" 114 | metric: ApplicantsEngineering 115 | style: "text-align:right;" 116 | - row: 117 | header: "YOY" 118 | metric: ApplicantsEngineeringYOY 119 | style: "font-style: italic; text-align:right;" 120 | y_scaling: "##%" 121 | - row: 122 | header: "Sales" 123 | metric: ApplicantsSales 124 | style: "text-align:right;" 125 | - row: 126 | header: "YOY" 127 | metric: ApplicantsSalesYOY 128 | style: "font-style: italic; text-align:right;" 129 | y_scaling: "##%" 130 | - row: 131 | header: "Other" 132 | metric: ApplicantsOther 133 | style: "text-align:right;" 134 | - row: 135 | header: "YOY" 136 | metric: ApplicantsOtherYOY 137 | style: "font-style: italic; text-align:right;" 138 | y_scaling: "##%" 139 | -------------------------------------------------------------------------------- /src/unit_test_case/scenario_5/config.yaml: -------------------------------------------------------------------------------- 1 | setup: 2 | week_ending: 30-APR-2022 3 | fiscal_year_end_month: FEB 4 | week_number: 16 5 | title: WBR Test Cases 6 | 7 | metrics: 8 | CumulativePaidCustomers: 9 | column: "cumulative_paid_customers" 10 | aggf: mean 11 | PaidCustomers: 12 | column: "new_paid_customers" 13 | aggf: sum 14 | ChurnedCustomers: 15 | column: "new_churned_customers" 16 | aggf: sum 17 | CAC: 18 | column: "cac_usd" 19 | aggf: sum 20 | FreeTrialStarts: 21 | column: "free_trial_starts" 22 | aggf: sum 23 | TestDivisionMetric: 24 | function: 25 | divide: 26 | - metric: 27 | name: ChurnedCustomers 28 | - metric: 29 | name: CumulativePaidCustomers 30 | TestProductMetric: 31 | function: 32 | product: 33 | - metric: 34 | name: CAC 35 | - metric: 36 | name: FreeTrialStarts 37 | TestDifferenceMetric: 38 | function: 39 | difference: 40 | - metric: 41 | name: PaidCustomers 42 | - metric: 43 | name: ChurnedCustomers 44 | CumulativeMRR: 45 | column: "cumulative_mrr_usd" 46 | aggf: mean 47 | NewLogoMRR: 48 | column: "new_logo_mrr_usd" 49 | aggf: sum 50 | UpsellMRR: 51 | column: "upsell_mrr_usd" 52 | aggf: sum 53 | TestSumMetrics: 54 | function: 55 | sum: 56 | - metric: 57 | name: NewLogoMRR 58 | - metric: 59 | name: UpsellMRR 60 | TestSumMetricColumn: 61 | function: 62 | sum: 63 | - metric: 64 | name: NewLogoMRR 65 | - metric: 66 | name: UpsellMRR 67 | TestMaxMetric: 68 | column: "latency" 69 | aggf: max 70 | TestMinMetric: 71 | column: "latency" 72 | aggf: min 73 | TestSumDoubleMetric: 74 | column: "latency" 75 | aggf: sum 76 | TestFilterMetricUS: 77 | filter: 78 | base_column: "latency" 79 | query: "latency_country == 'US'" 80 | aggf: mean 81 | TestFilterMetricUK: 82 | filter: 83 | base_column: "latency" 84 | query: "latency_country == 'UK'" 85 | aggf: mean 86 | DownsellMRR: 87 | column: "downsell_mrr_usd" 88 | aggf: sum 89 | ChurnRevenueLost: 90 | column: "churn_revenue_lost_usd" 91 | aggf: sum 92 | CumulativeARR: 93 | column: "cumulative_arr_usd" 94 | aggf: mean 95 | NewARR: 96 | column: "new_arr_usd" 97 | aggf: sum 98 | FreeTrialConversionRate: 99 | column: "free_trial_conversion_rate" 100 | aggf: mean 101 | ChurnRate: 102 | column: "churn_rate" 103 | aggf: mean 104 | 105 | DAU: 106 | column: "daily_active_users" 107 | aggf: mean 108 | DailyActiveSubscribers: 109 | column: "daily_active_subcribers" 110 | aggf: mean 111 | NumberOfSeats: 112 | column: "new_seats" 113 | aggf: sum 114 | PercentActiveSubscribers: 115 | function: 116 | divide: 117 | - metric: 118 | name: DailyActiveSubscribers 119 | - metric: 120 | name: CumulativePaidCustomers 121 | 122 | deck: 123 | - block: 124 | ui_type: 6_12Graph 125 | title: CumulativePaidCustomers 126 | x_axis_monthly_display: trailing_twelve_months 127 | metrics: 128 | CumulativePaidCustomers: 129 | graph_prior_year_flag: true 130 | legend_name: CumulativePaidCustomers 131 | - block: 132 | ui_type: 6_12Graph 133 | title: PaidCustomers 134 | x_axis_monthly_display: trailing_twelve_months 135 | metrics: 136 | PaidCustomers: 137 | graph_prior_year_flag: true 138 | legend_name: PaidCustomers 139 | - block: 140 | ui_type: 6_12Graph 141 | title: TestMaxMetric 142 | x_axis_monthly_display: trailing_twelve_months 143 | metrics: 144 | TestMaxMetric: 145 | graph_prior_year_flag: true 146 | legend_name: TestMaxMetric 147 | - block: 148 | ui_type: 6_12Graph 149 | title: TestMinMetric 150 | x_axis_monthly_display: trailing_twelve_months 151 | metrics: 152 | TestMinMetric: 153 | graph_prior_year_flag: true 154 | legend_name: TestMinMetric 155 | - block: 156 | ui_type: 6_12Graph 157 | title: TestSumMetrics 158 | x_axis_monthly_display: trailing_twelve_months 159 | metrics: 160 | TestSumMetrics: 161 | graph_prior_year_flag: true 162 | legend_name: TestSumMetrics 163 | - block: 164 | ui_type: 6_12Graph 165 | title: TestSumMetricColumn 166 | x_axis_monthly_display: trailing_twelve_months 167 | metrics: 168 | TestSumMetricColumn: 169 | graph_prior_year_flag: true 170 | legend_name: TestSumMetricColumn 171 | - block: 172 | ui_type: 6_12Graph 173 | title: TestDifferenceMetric 174 | x_axis_monthly_display: trailing_twelve_months 175 | metrics: 176 | TestDifferenceMetric: 177 | graph_prior_year_flag: true 178 | legend_name: TestDifferenceMetric 179 | - block: 180 | ui_type: 6_12Graph 181 | title: TestSumDoubleMetric 182 | x_axis_monthly_display: trailing_twelve_months 183 | metrics: 184 | TestSumDoubleMetric: 185 | graph_prior_year_flag: true 186 | legend_name: TestSumDoubleMetric 187 | - block: 188 | ui_type: 6_12Graph 189 | title: TestFilterMetricUS 190 | x_axis_monthly_display: trailing_twelve_months 191 | metrics: 192 | TestFilterMetricUS: 193 | graph_prior_year_flag: true 194 | legend_name: TestFilterMetricUS 195 | - block: 196 | ui_type: 6_12Graph 197 | title: TestProductMetric 198 | x_axis_monthly_display: trailing_twelve_months 199 | metrics: 200 | TestProductMetric: 201 | graph_prior_year_flag: true 202 | legend_name: TestProductMetric 203 | - block: 204 | ui_type: 6_12Graph 205 | title: TestDivisionMetric 206 | x_axis_monthly_display: trailing_twelve_months 207 | metrics: 208 | TestDivisionMetric: 209 | graph_prior_year_flag: true 210 | legend_name: TestDivisionMetric 211 | -------------------------------------------------------------------------------- /src/unit_test_case/scenario_6/testconfig.yml: -------------------------------------------------------------------------------- 1 | tests: 2 | - test: 3 | test_case_no: 1 4 | x_axis_monthly_display: "trailing_twelve_months" 5 | week_ending: 25-MAR-2023 6 | fiscal_year_end_month: DEC 7 | metric_name: "Total_PMs" 8 | 9 | # 1 test 10 | cy_6_weeks: [ 56509.43122, 56666.43122, 57126.43122, 57791.43122, 58373.43122, 59012.43122 ] 11 | 12 | # 2 test 13 | py_6_weeks: [ 43832.43122, 44014.43122, 44162.43122, 44260.43122, 44392.43122, 44513.43122 ] 14 | 15 | # 3 test 16 | cy_monthly: [44606.43, 45499.43, 47317.43, 49007.43, 49986.43, 50486.43, 51458.43, 52488.43, 17 | 53014.43, 55062.43, 56165.43, 56725.43] 18 | 19 | # 4 test 20 | py_monthly: [35118.43, 36173.43, 37257.43, 38549.43, 38933.43, 39324.43, 39604.43, 21 | 40397.43, 41609.43, 42708.43, 43329.43, 44070.43] 22 | 23 | # 5 test 24 | x_axis: ['wk 8', 'wk 9', 'wk 10', 'wk 11', 'wk 12', 'wk 13', 'Mar', 'Apr', "May", "Jun", "Jul", "Aug", 25 | "Sep", "Oct", "Nov", "Dec", "Jan", "Feb"] 26 | 27 | 28 | # 6 test 29 | box_totals: [59012.43, 1.09, 32.57, 59012.43, 32.63, 59012.43, 32.63, 59012.43, 32.63] 30 | 31 | # 7 test 32 | cy_monthly_data_frame_length: 14 33 | # 8 test 34 | py_monthly_data_frame_length: 22 35 | 36 | - test: 37 | test_case_no: 2 38 | x_axis_monthly_display: "trailing_twelve_months" 39 | week_ending: 25-MAR-2023 40 | fiscal_year_end_month: DEC 41 | metric_name: "Total_PMsv2" 42 | 43 | # 1 test 44 | cy_6_weeks: [56509.0, 56666.0, 57126.0, 57791.0, 58373.0, 59012.0] 45 | 46 | # 2 test 47 | py_6_weeks: [43832.0, 44014.0, 44162.0, 44260.0, 44392.0, 44513.0] 48 | 49 | # 3 test 50 | cy_monthly: [44606.0, 45499.0, 47317.0, 49007.0, 49986.0, 50486.0, 51458.0, 52488.0, 53014.0, 55062.0, 51 | 56165.0, 56725.0] 52 | 53 | # 4 test 54 | py_monthly: [0.0, 0.0, 0.0, 38549.0, 38933.0, 39324.0, 39604.0, 40397.0, 41609.0, 42708.0, 43329.0, 44070.0] 55 | 56 | # 5 test 57 | x_axis: [ 'wk 8', 'wk 9', 'wk 10', 'wk 11', 'wk 12', 'wk 13', 'Mar', 'Apr', "May", "Jun", "Jul", "Aug", 58 | "Sep", "Oct", "Nov", "Dec", "Jan", "Feb" ] 59 | 60 | # 6 test 61 | box_totals: [59012.0, 1.09, 32.57, 59012.0, 32.63, 59012.0, 32.63, 59012.0, 32.63] 62 | 63 | # 7 test 64 | cy_monthly_data_frame_length: 14 65 | # 8 test 66 | py_monthly_data_frame_length: 22 67 | 68 | - test: 69 | test_case_no: 3 70 | x_axis_monthly_display: "trailing_twelve_months" 71 | week_ending: 25-MAR-2023 72 | fiscal_year_end_month: DEC 73 | metric_name: "MRR" 74 | 75 | # 1 test 76 | cy_6_weeks: [4012169.62, 4023316.62, 4113103.05, 4160983.05, 4202887.05, 4248895.05] 77 | 78 | # 2 test 79 | py_6_weeks: [3068270.19, 3081010.19, 3091370.19, 3098230.19, 3107470.19, 3115940.19] 80 | 81 | # 3 test 82 | cy_monthly: [3122450.19, 3184960.19, 3312220.19, 3430520.19, 3499050.19, 3534050.19, 3602090.19, 83 | 3674190.19, 3711010.19, 3854370.19, 3987745.62, 4027505.62] 84 | 85 | # 4 test 86 | py_monthly: [2454427.16, 2546247.82, 2622550.58, 2713494.46, 2729622.86, 2757035.87, 2776666.67, 2829840.06, 2914740.66, 2991725.61, 3033060.19, 3084930.19] 87 | 88 | # 5 test 89 | x_axis: [ 'wk 8', 'wk 9', 'wk 10', 'wk 11', 'wk 12', 'wk 13', 'Mar', 'Apr', "May", "Jun", "Jul", "Aug", 90 | "Sep", "Oct", "Nov", "Dec", "Jan", "Feb" ] 91 | 92 | 93 | # 6 test 94 | box_totals: [4248895.05, 1.09, 36.36, 4248895.05, 36.42, 4248895.05, 36.42, 4248895.05, 36.42] 95 | 96 | # 7 test 97 | cy_monthly_data_frame_length: 14 98 | # 8 test 99 | py_monthly_data_frame_length: 22 100 | 101 | - test: 102 | test_case_no: 4 103 | x_axis_monthly_display: "trailing_twelve_months" 104 | week_ending: 25-MAR-2023 105 | fiscal_year_end_month: DEC 106 | metric_name: "TotalPMs_TARGET" 107 | 108 | # 1 test 109 | cy_6_weeks: [57745.0, 58200.0, 58655.0, 59110.0, 59565.0, 60020.0] 110 | 111 | # 2 test 112 | py_6_weeks: [!!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan] 113 | 114 | # 3 test 115 | cy_monthly: [!!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, 116 | !!float .nan, !!float .nan, !!float .nan, 55015.0, 57030.0] 117 | 118 | # 4 test 119 | py_monthly: [!!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, 120 | !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan] 121 | 122 | # 5 test 123 | x_axis: [ 'wk 8', 'wk 9', 'wk 10', 'wk 11', 'wk 12', 'wk 13', 'Mar', 'Apr', "May", "Jun", "Jul", "Aug", 124 | "Sep", "Oct", "Nov", "Dec", "Jan", "Feb" ] 125 | 126 | # 6 test 127 | box_totals: [60020.0, 0.76, 0.0, 58850.0, 0.0, 55015.0, 0.0, 55015.0, 0.0] 128 | 129 | # 7 test 130 | cy_monthly_data_frame_length: 14 131 | # 8 test 132 | py_monthly_data_frame_length: 22 133 | 134 | - test: 135 | test_case_no: 5 136 | x_axis_monthly_display: "trailing_twelve_months" 137 | week_ending: 25-MAR-2023 138 | fiscal_year_end_month: DEC 139 | metric_name: "MRR_TARGET" 140 | 141 | # 1 test 142 | cy_6_weeks: [4072428.57, 4106428.57, 4138032.26, 4167838.71, 4197645.16, 4227451.61] 143 | 144 | # 2 test 145 | py_6_weeks: [!!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan] 146 | 147 | # 3 test 148 | cy_monthly: [!!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, 149 | !!float .nan, !!float .nan, !!float .nan, 3985000.0, 4121000.0] 150 | 151 | # 4 test 152 | py_monthly: [!!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, 153 | !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan] 154 | 155 | # 5 test 156 | x_axis: [ 'wk 8', 'wk 9', 'wk 10', 'wk 11', 'wk 12', 'wk 13', 'Mar', 'Apr', "May", "Jun", "Jul", "Aug", 157 | "Sep", "Oct", "Nov", "Dec", "Jan", "Feb" ] 158 | 159 | # 6 test 160 | box_totals: [4227451.61, 0.71, 0.0, 4227451.61, 0.0, 4227451.61, 0.0, 4227451.61, 0.0] 161 | 162 | # 7 test 163 | cy_monthly_data_frame_length: 14 164 | # 8 test 165 | py_monthly_data_frame_length: 22 166 | 167 | 168 | 169 | -------------------------------------------------------------------------------- /src/unit_test_case/scenario_4/config.yaml: -------------------------------------------------------------------------------- 1 | setup: 2 | week_ending: 30-APR-2022 3 | week_number: 16 4 | title: WBR Test Cases 5 | 6 | metrics: 7 | CumulativePaidCustomers: 8 | column: "cumulative_paid_customers" 9 | aggf: mean 10 | PaidCustomers: 11 | column: "new_paid_customers" 12 | aggf: sum 13 | ChurnedCustomers: 14 | column: "new_churned_customers" 15 | aggf: sum 16 | CAC: 17 | column: "cac_usd" 18 | aggf: sum 19 | FreeTrialStarts: 20 | column: "free_trial_starts" 21 | aggf: sum 22 | TestDivisionMetric: 23 | function: 24 | divide: 25 | - metric: 26 | name: ChurnedCustomers 27 | - metric: 28 | name: CumulativePaidCustomers 29 | TestProductMetric: 30 | function: 31 | product: 32 | - metric: 33 | name: CAC 34 | - metric: 35 | name: FreeTrialStarts 36 | TestProductMetricTwo: 37 | function: 38 | product: 39 | - metric: 40 | name: CumulativePaidCustomers 41 | - metric: 42 | name: ChurnedCustomers 43 | TestDifferenceMetric: 44 | function: 45 | difference: 46 | - metric: 47 | name: PaidCustomers 48 | - metric: 49 | name: ChurnedCustomers 50 | CumulativeMRR: 51 | column: "cumulative_mrr_usd" 52 | aggf: mean 53 | NewLogoMRR: 54 | column: "new_logo_mrr_usd" 55 | aggf: sum 56 | UpsellMRR: 57 | column: "upsell_mrr_usd" 58 | aggf: sum 59 | TestSumMetrics: 60 | function: 61 | sum: 62 | - metric: 63 | name: NewLogoMRR 64 | - metric: 65 | name: UpsellMRR 66 | TestSumMetricColumn: 67 | function: 68 | sum: 69 | - metric: 70 | name: NewLogoMRR 71 | - metric: 72 | name: UpsellMRR 73 | TestMaxMetric: 74 | column: "latency" 75 | aggf: max 76 | TestMinMetric: 77 | column: "latency" 78 | aggf: min 79 | TestSumDoubleMetric: 80 | column: "latency" 81 | aggf: sum 82 | TestFilterMetricUS: 83 | filter: 84 | base_column: "latency" 85 | query: "latency_country == 'US'" 86 | aggf: mean 87 | TestFilterMetricUK: 88 | filter: 89 | base_column: "latency" 90 | query: "latency_country == 'UK'" 91 | aggf: mean 92 | DownsellMRR: 93 | column: "downsell_mrr_usd" 94 | aggf: sum 95 | ChurnRevenueLost: 96 | column: "churn_revenue_lost_usd" 97 | aggf: sum 98 | CumulativeARR: 99 | column: "cumulative_arr_usd" 100 | aggf: mean 101 | NewARR: 102 | column: "new_arr_usd" 103 | aggf: sum 104 | FreeTrialConversionRate: 105 | column: "free_trial_conversion_rate" 106 | aggf: mean 107 | ChurnRate: 108 | column: "churn_rate" 109 | aggf: mean 110 | 111 | DAU: 112 | column: "daily_active_users" 113 | aggf: mean 114 | DailyActiveSubscribers: 115 | column: "daily_active_subcribers" 116 | aggf: mean 117 | NumberOfSeats: 118 | column: "new_seats" 119 | aggf: sum 120 | PercentActiveSubscribers: 121 | function: 122 | divide: 123 | - metric: 124 | name: DailyActiveSubscribers 125 | - metric: 126 | name: CumulativePaidCustomers 127 | 128 | deck: 129 | - block: 130 | ui_type: 6_12Graph 131 | title: CumulativePaidCustomers 132 | x_axis_monthly_display: trailing_twelve_months 133 | metrics: 134 | CumulativePaidCustomers: 135 | graph_prior_year_flag: true 136 | legend_name: CumulativePaidCustomers 137 | - block: 138 | ui_type: 6_12Graph 139 | title: PaidCustomers 140 | x_axis_monthly_display: trailing_twelve_months 141 | metrics: 142 | PaidCustomers: 143 | graph_prior_year_flag: true 144 | legend_name: PaidCustomers 145 | - block: 146 | ui_type: 6_12Graph 147 | title: TestMaxMetric 148 | x_axis_monthly_display: trailing_twelve_months 149 | metrics: 150 | TestMaxMetric: 151 | graph_prior_year_flag: true 152 | legend_name: TestMaxMetric 153 | - block: 154 | ui_type: 6_12Graph 155 | title: TestMinMetric 156 | x_axis_monthly_display: trailing_twelve_months 157 | metrics: 158 | TestMinMetric: 159 | graph_prior_year_flag: true 160 | legend_name: TestMinMetric 161 | - block: 162 | ui_type: 6_12Graph 163 | title: TestSumMetrics 164 | x_axis_monthly_display: trailing_twelve_months 165 | metrics: 166 | TestSumMetrics: 167 | graph_prior_year_flag: true 168 | legend_name: TestSumMetrics 169 | - block: 170 | ui_type: 6_12Graph 171 | title: TestSumMetricColumn 172 | x_axis_monthly_display: trailing_twelve_months 173 | metrics: 174 | TestSumMetricColumn: 175 | graph_prior_year_flag: true 176 | legend_name: TestSumMetricColumn 177 | - block: 178 | ui_type: 6_12Graph 179 | title: TestDifferenceMetric 180 | x_axis_monthly_display: trailing_twelve_months 181 | metrics: 182 | TestDifferenceMetric: 183 | graph_prior_year_flag: true 184 | legend_name: TestDifferenceMetric 185 | - block: 186 | ui_type: 6_12Graph 187 | title: TestSumDoubleMetric 188 | x_axis_monthly_display: trailing_twelve_months 189 | metrics: 190 | TestSumDoubleMetric: 191 | graph_prior_year_flag: true 192 | legend_name: TestSumDoubleMetric 193 | - block: 194 | ui_type: 6_12Graph 195 | title: TestFilterMetricUS 196 | x_axis_monthly_display: trailing_twelve_months 197 | metrics: 198 | TestFilterMetricUS: 199 | graph_prior_year_flag: true 200 | legend_name: TestFilterMetricUS 201 | - block: 202 | ui_type: 6_12Graph 203 | title: TestProductMetric 204 | x_axis_monthly_display: trailing_twelve_months 205 | metrics: 206 | TestProductMetric: 207 | graph_prior_year_flag: true 208 | legend_name: TestProductMetric 209 | - block: 210 | ui_type: 6_12Graph 211 | title: TestDivisionMetric 212 | x_axis_monthly_display: trailing_twelve_months 213 | metrics: 214 | TestDivisionMetric: 215 | graph_prior_year_flag: true 216 | legend_name: TestDivisionMetric 217 | - block: 218 | ui_type: 6_12Graph 219 | title: TestProductMetricTwo 220 | x_axis_monthly_display: trailing_twelve_months 221 | metrics: 222 | TestProductMetricTwo: 223 | graph_prior_year_flag: true 224 | legend_name: TestProductMetricTwo 225 | 226 | -------------------------------------------------------------------------------- /docs/API_DOCUMENTATION.md: -------------------------------------------------------------------------------- 1 | # API Documentation: 2 | 3 | ## **Endpoint** 4 | `POST /report` 5 | 6 | ## **Description** 7 | This API endpoint generates a report based on the provided CSV data and YAML configuration. It supports HTML, JSON, or a custom response format for the generated report. 8 | 9 | --- 10 | 11 | ## **Request Parameters** 12 | 13 | | Parameter | Location | Type | Required | Description | 14 | |-------------------------|-----------|---------|----------|------------------------------------------------------------------------------------------------| 15 | | `dataUrl` | Query | String | Optional | URL of the CSV file to be used for report generation. Either `dataUrl` or `dataFile` required. | 16 | | `dataFile` | Form-Data | File | Optional | CSV file to be uploaded directly. Either `dataUrl` or `dataFile` required. | 17 | | `configUrl` | Query | String | Optional | URL of the YAML configuration file. Either `configUrl` or `configFile` required. | 18 | | `configFile` | Form-Data | File | Optional | YAML configuration file to be uploaded directly. Either `configUrl` or `configFile` required. | 19 | | `outputType` | Query | String | Optional | Specifies the output format. Accepted values: `HTML` or `JSON`. Defaults to custom response. | 20 | | `week_ending` | Query | String | Optional | Specifies the week-ending date to override the YAML setup parameter. | 21 | | `week_number` | Query | String | Optional | Specifies the week number to override the YAML setup parameter. | 22 | | `title` | Query | String | Optional | Specifies the report title to override the YAML setup parameter. | 23 | | `fiscal_year_end_month` | Query | String | Optional | Specifies the fiscal year-end month to override the YAML setup parameter. | 24 | | `block_starting_number` | Query | Integer | Optional | Specifies the starting number for block numbering in the report. | 25 | | `tooltip` | Query | String | Optional | Specifies a tooltip to override the YAML setup parameter. | 26 | | `password` | Query | String | Optional | Password for your published report. | 27 | --- 28 | 29 | ## **Request Body** 30 | - **Optional**: YAML file content (if not using `configUrl`). 31 | - **Optional**: CSV file content (if not using `dataUrl`). 32 | 33 | --- 34 | 35 | ## **Response** 36 | 37 | ### **Success Responses** 38 | 1. **JSON Output** 39 | - **Status Code**: `200 OK` 40 | - **Body**: JSON representation of the generated report. 41 | 42 | 2. **HTML Output** 43 | - **Status Code**: `200 OK` 44 | - **Body**: Rendered HTML report. 45 | 46 | 3. **Custom Response** 47 | - **Status Code**: `200 OK` 48 | - **Body**: Published report URL or generated content, depending on implementation. 49 | 50 | ### **Error Responses** 51 | 1. **Missing CSV Data** 52 | - **Status Code**: `400 Bad Request` 53 | - **Body**: 54 | ```json 55 | { 56 | "error": "Either dataUrl or dataFile required!" 57 | } 58 | ``` 59 | 60 | 2. **Missing YAML Configuration** 61 | - **Status Code**: `400 Bad Request` 62 | - **Body**: 63 | ```json 64 | { 65 | "error": "Either configUrl or configFile required!" 66 | } 67 | ``` 68 | 69 | 3. **YAML Parsing Error** 70 | - **Status Code**: `500 Internal Server Error` 71 | - **Body**: 72 | ```json 73 | { 74 | "error": "Error description for YAML parsing issue." 75 | } 76 | ``` 77 | 78 | 4. **Validation Error** 79 | - **Status Code**: `500 Internal Server Error` 80 | - **Body**: 81 | ```json 82 | { 83 | "error": "Invalid configuration provided: " 84 | } 85 | ``` 86 | 87 | 5. **Report Generation Error** 88 | - **Status Code**: `500 Internal Server Error` 89 | - **Body**: 90 | ```json 91 | { 92 | "error": "Error while creating deck, caused by: " 93 | } 94 | ``` 95 | 96 | --- 97 | 98 | ## **Examples** 99 | 100 | ### **Request Example** 101 | Using `curl`: 102 | ```bash 103 | curl -X POST https:///report \ 104 | -H "Content-Type: multipart/form-data" \ 105 | -d "configUrl=https://put-your-config-url.yaml" \ 106 | -d "dataUrl=https://put-your-data-url.yaml" \ 107 | -d "week_ending=18-SEP-2021" \ 108 | -d "week_number=37" \ 109 | -d "outputType=JSON" \ 110 | ``` 111 | 112 | ### **Response Example (JSON Output)** 113 | ```json 114 | [ 115 | { 116 | "blocks": [ 117 | { 118 | "plotStyle": "6_12_chart", 119 | "title": "Total Page Views", 120 | "yLabel": "", 121 | "yScale": "##MM", 122 | "boxTotalScale": "%", 123 | "axes": 2, 124 | "xAxis": ["wk 33", "wk 34", "wk 35", "wk 36", "wk 37", "wk 38", " ", "Sep", "Oct", "Nov", "Dec", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug"], 125 | "yAxis": [ 126 | { 127 | "lineStyle": "primary", 128 | "legendName": "Page Views", 129 | "metric": { 130 | "current": [ 131 | { 132 | "primaryAxis": [496725868.0, 499671126.0, 464148871.0, 457477195.0, 460207741.0, 470759324.0, "", "", "", "", "", "", "", "", "", "", "", "", ""] 133 | }, 134 | { 135 | "secondaryAxis": ["", "", "", "", "", "", "", 1708890199.0, 1807907694.0, 1820822982.0, 1922478407.0, 2318880564.0, 1969227848.0, 2117874905.0, 2036021000.0, 2057839090.0, 2051219932.0, 2274329968.0, 2207577942.0] 136 | } 137 | ] 138 | } 139 | } 140 | ] 141 | } 142 | ], 143 | "title": "WBR Daily", 144 | "weekEnding": "25 September 2021", 145 | "blockStartingNumber": 2, 146 | "xAxisMonthlyDisplay": null, 147 | "eventErrors": null 148 | } 149 | ] 150 | ``` 151 | 152 | ### **Response Example (HTML Output)** 153 | ```html 154 | 155 | 156 | 157 | 158 | 159 |
160 |
161 |
162 | 163 | 164 | ``` 165 | 166 | ### **Response Example (URL Output)** 167 | ```json 168 | { 169 | "path": "https:///build-wbr/publish?file=" 170 | } 171 | ``` 172 | 173 | ## Notes 174 | - Ensure that either csvUrl or csvfile is provided for the data source. 175 | - YAML configuration can be supplied via yamlUrl or configfile. 176 | - Errors during YAML validation or report generation will return detailed error messages in the response. -------------------------------------------------------------------------------- /src/web/static/unit_test_wbr.js: -------------------------------------------------------------------------------- 1 | var totalTests; 2 | var totalPassed; 3 | var totalFailed; 4 | var unit_test_result; 5 | 6 | const testInput = document.getElementById('execute-tests'); 7 | 8 | testInput.addEventListener('click', () => { 9 | page_loader_div = document.createElement( 'div' ); 10 | page_loader_div.id = "page_loader_div"; 11 | page_loader_div.className = "loader"; 12 | document.getElementsByTagName('body')[0].appendChild(page_loader_div); 13 | 14 | fetchText(); 15 | }); 16 | 17 | async function fetchText() { 18 | var requestOptions = { 19 | method: 'GET', 20 | redirect: 'follow' 21 | }; 22 | var response = await fetch("/wbr-unit-test", requestOptions); 23 | if (response.status === 200) { 24 | document.getElementById("page_loader_div").remove(); 25 | var textArea = document.getElementById("test_result"); 26 | var data = await response.json(); 27 | totalTests = 0; 28 | totalPassed = 0; 29 | totalFailed = 0; 30 | unit_test_result = "Result of the Test\n\n"; 31 | unit_test_result += "---------------------------------------\n"; 32 | data.scenarios.forEach(function(scenario) { 33 | build_scenario(scenario); 34 | }); 35 | unit_test_result += "\nTotal Test cases: " + totalTests; 36 | unit_test_result += "\nTotal Failed: " + totalFailed; 37 | unit_test_result += "\nTotal Passed: " + totalPassed; 38 | document.getElementById('test_result').value = unit_test_result; 39 | } 40 | else { 41 | document.getElementById("page_loader_div").remove(); 42 | textArea.innerHTML = "Tests failed! below are the errors \n" + data.message + 43 | "\n Please report this bug with files uploaded and result at developer@workingbackwards.com"; 44 | } 45 | } 46 | 47 | function build_scenario(scenario) { 48 | unit_test_result += "SCENARIO: " + scenario.scenario + "\n"; 49 | unit_test_result += "Fiscal Month: " + scenario.fiscalMonth + "\n"; 50 | unit_test_result += "Week Ending: " + scenario.weekEnding + "\n"; 51 | unit_test_result += scenario.scenario + " test result -->\n\n" 52 | scenario.testCases.forEach(function(test) { 53 | each_scenario(test); 54 | }); 55 | unit_test_result += "---------------------------------------\n"; 56 | } 57 | 58 | function each_scenario(test) { 59 | unit_test_result += "Test: " + test.testNumber + "\n"; 60 | let testFailCount = 0; 61 | unit_test_result += "CY Data Frame length test result " + test.cyDataframeLength.result + "\n"; 62 | if (test.cyDataframeLength.result == "FAILED") { 63 | unit_test_result += "\nExpected: " + test.cyDataframeLength.expected + "\n"; 64 | unit_test_result += "Calculated: " + test.cyDataframeLength.calculated + "\n\n"; 65 | testFailCount += 1; 66 | } 67 | unit_test_result += "PY Data Frame length test result " + test.pyDataframeLength.result + "\n"; 68 | if (test.pyDataframeLength.result == "FAILED") { 69 | unit_test_result += "\nExpected: " + test.pyDataframeLength.expected + "\n"; 70 | unit_test_result += "Calculated: " + test.pyDataframeLength.calculated + "\n\n"; 71 | testFailCount += 1; 72 | } 73 | 74 | if (test.blockType == "SixTwelveChart") { 75 | sixTwelve(test) ? testFailCount += 1 : testFailCount; 76 | } 77 | 78 | if (test.blockType == "TrailingTable") { 79 | trailingTable(test); 80 | } 81 | 82 | if (testFailCount > 0) { 83 | totalFailed += 1; 84 | } else { 85 | totalPassed += 1; 86 | } 87 | 88 | totalTests += 1; 89 | unit_test_result += "\n"; 90 | } 91 | 92 | function sixTwelve(test) { 93 | let isFailed = false; 94 | unit_test_result += "CY Six week test result: " + test.cySixWeekTestResult.result + "\n"; 95 | if (test.cySixWeekTestResult.result == "FAILED") { 96 | unit_test_result += "\nExpected: " + test.cySixWeekTestResult.expected + "\n"; 97 | unit_test_result += "Calculated: " + test.cySixWeekTestResult.calculated + "\n\n"; 98 | isFailed = true; 99 | } 100 | unit_test_result += "CY Twelve months test result: " + test.cyTwelveMonthTestResult.result + "\n"; 101 | if (test.cyTwelveMonthTestResult.result == "FAILED") { 102 | unit_test_result += "\nExpected: " + test.cyTwelveMonthTestResult.expected + "\n"; 103 | unit_test_result += "Calculated: " + test.cyTwelveMonthTestResult.calculated + "\n\n"; 104 | isFailed = true; 105 | } 106 | if (test.pySixWeekTestResult !== undefined && test.pySixWeekTestResult !== null) { 107 | unit_test_result += "PY Six week test result: " + test.pySixWeekTestResult.result + "\n"; 108 | if (test.pySixWeekTestResult.result == "FAILED") { 109 | unit_test_result += "\nExpected: " + test.pySixWeekTestResult.expected + "\n"; 110 | unit_test_result += "Calculated: " + test.pySixWeekTestResult.calculated + "\n\n"; 111 | isFailed = true; 112 | } 113 | } 114 | if (test.pyTwelveMonthTestResult !== undefined && test.pyTwelveMonthTestResult !== null) { 115 | unit_test_result += "PY Twelve months test result: " + test.pyTwelveMonthTestResult.result + "\n"; 116 | if (test.pyTwelveMonthTestResult.result == "FAILED") { 117 | unit_test_result += "\nExpected: " + test.pyTwelveMonthTestResult.expected + "\n"; 118 | unit_test_result += "Calculated: " + test.pyTwelveMonthTestResult.calculated + "\n\n"; 119 | isFailed = true; 120 | } 121 | } 122 | unit_test_result += "Summary table test result: " + test.summaryResult.result + "\n"; 123 | if (test.summaryResult.result == "FAILED") { 124 | unit_test_result += "\nExpected: " + test.summaryResult.expected + "\n"; 125 | unit_test_result += "Calculated: " + test.summaryResult.calculated + "\n\n"; 126 | isFailed = true; 127 | } 128 | unit_test_result += "X-axis labels test result: " + test.xAxis.result + "\n"; 129 | if (test.xAxis.result == "FAILED") { 130 | unit_test_result += "\nExpected: " + test.xAxis.expected + "\n"; 131 | unit_test_result += "Calculated: " + test.xAxis.calculated + "\n\n"; 132 | isFailed = true; 133 | } 134 | return isFailed; 135 | } 136 | 137 | function trailingTable(test) { 138 | let isFailed = false; 139 | unit_test_result += "Header label test result: " + test.headerResult.result + "\n"; 140 | if (test.headerResult.result == "FAILED") { 141 | unit_test_result += "\nExpected: " + test.headerResult.expected + "\n"; 142 | unit_test_result += "Calculated: " + test.headerResult.calculated + "\n\n"; 143 | isFailed = true; 144 | } 145 | unit_test_result += "Row data test result: " + test.rowResult.result + "\n"; 146 | if (test.rowResult.result == "FAILED") { 147 | unit_test_result += "\nExpected: " + test.rowResult.expected + "\n"; 148 | unit_test_result += "Calculated: " + test.rowResult.calculated + "\n\n"; 149 | isFailed = true; 150 | } 151 | } 152 | 153 | -------------------------------------------------------------------------------- /src/web/templates/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Login Page 6 | 201 | 202 | 203 |
204 |
205 |

Login

206 | 207 |
208 | 209 |
210 |
211 |
212 | 213 |
214 |
215 |
216 | 217 | 218 |
219 |
220 |
221 | 222 |
223 | 224 |
225 | 226 | 259 | 260 | -------------------------------------------------------------------------------- /src/web/static/wbr.css: -------------------------------------------------------------------------------- 1 | @media screen { 2 | div.divFooter { 3 | display: none; 4 | } 5 | } 6 | @media print { 7 | div.divFooter { 8 | position: fixed; 9 | bottom: 0; 10 | } 11 | @page { 12 | margin-top: 0; 13 | margin-bottom: 0; 14 | } 15 | body { 16 | padding-top: 72px; 17 | padding-bottom: 72px ; 18 | } 19 | } 20 | @media print { 21 | a[href]:after { 22 | content: none !important; 23 | } 24 | } 25 | 26 | @media print { @page {size: auto !important} } 27 | #chartdiv { 28 | width: 980px; 29 | } 30 | 31 | @media print { 32 | .deckset { 33 | break-inside: avoid; 34 | } 35 | } 36 | 37 | .chartdiv { 38 | width: 48vw; 39 | height: 340px; 40 | display: block; 41 | padding-top: 10px 42 | } 43 | 44 | .blockTableDiv { 45 | width: 100%; 46 | height: 100%; 47 | display: block; 48 | } 49 | 50 | .tablediv { 51 | width: 100%; 52 | height: 52px; 53 | display: block; 54 | margin-top:10px; 55 | } 56 | 57 | .weekTableDiv { 58 | width: 100%; 59 | padding: 10px; 60 | border: 1px solid black; 61 | display: inline-block; 62 | margin: 5px; 63 | } 64 | 65 | table { 66 | font-size: 0.7vw 67 | } 68 | 69 | td { 70 | text-align:center; 71 | } 72 | .rowData { 73 | text-align: center; 74 | max-width: 60px; 75 | overflow: hidden; 76 | text-overflow: ellipsis; 77 | white-space: nowrap; 78 | } 79 | 80 | tr { 81 | text-align:center !important; 82 | } 83 | 84 | .counterdiv { 85 | position:relative; /* Stay in place */ 86 | z-index: 1; /* Sit on top */ 87 | } 88 | 89 | iframe { 90 | border: none; 91 | } 92 | 93 | .maindivTable { 94 | table { 95 | margin-bottom: 0px 96 | } 97 | width: 49%; 98 | padding-right: 0px; 99 | padding-left: 0px; 100 | padding-bottom: 0px; 101 | display: inline-grid; 102 | margin-left: 5px; 103 | margin-right: 5px; 104 | margin-top: 0px; 105 | margin-bottom: 0px; 106 | } 107 | 108 | .maindiv { 109 | width: 49%; 110 | padding-left: 0px; 111 | padding-right: 0px; 112 | padding-bottom: 10px; 113 | border: 1px solid black; 114 | display: inline-grid; 115 | margin-left: 5px; 116 | margin-right: 5px; 117 | margin-top: 0px; 118 | margin-bottom: 0px; 119 | } 120 | 121 | .sectionDiv{ 122 | padding: 10px; 123 | border: 1px solid black; 124 | margin-left: 5px; 125 | margin-right: 5px; 126 | margin-top: 10px; 127 | margin-bottom: 0px; 128 | text-align: center; 129 | width: 98.5%; 130 | } 131 | 132 | #container { 133 | height: 400px; 134 | } 135 | 136 | .contentHolder{ 137 | width=100%; 138 | display=inline-block; 139 | } 140 | 141 | .weekEnding { 142 | padding-top: 10px; 143 | text-align: right; 144 | font-family: "Lucida Grande", "Lucida Sans Unicode", Arial, Helvetica, sans-serif; 145 | position: relative; 146 | top: -20px; 147 | left: -27px; 148 | text-align: right; 149 | color: #FF0000; 150 | } 151 | 152 | .title { 153 | padding-top: 10px; 154 | text-align: center; 155 | font-family: "Lucida Grande", "Lucida Sans Unicode", Arial, Helvetica, sans-serif; 156 | } 157 | 158 | .blockTable { 159 | height: 26px; 160 | } 161 | 162 | .dashboard { 163 | top: -20px; 164 | } 165 | 166 | .container { 167 | overflow:hidden; 168 | height: 60px; 169 | } 170 | .one { 171 | position: relative; 172 | top: 0; 173 | z-index: 1; 174 | cursor:pointer; 175 | } 176 | .two { 177 | position: relative; 178 | top: -40px; 179 | z-index: -1; 180 | -webkit-transition: top 1s; 181 | -moz-transition: top 1s; 182 | -o-transition: top 1s; 183 | transition: top 1s; 184 | } 185 | 186 | .bs-canvas-overlay { 187 | opacity: 0; 188 | z-index: -1; 189 | } 190 | 191 | .bs-canvas-overlay.show { 192 | opacity: 0.85; 193 | z-index: 1100; 194 | } 195 | 196 | .bs-canvas { 197 | top: 0; 198 | width: 0; 199 | z-index: 1110; 200 | overflow-x: hidden; 201 | overflow-y: auto; 202 | } 203 | 204 | .bs-canvas-left { 205 | left: 0; 206 | } 207 | 208 | .bs-canvas-right { 209 | right: 0; 210 | } 211 | 212 | .bs-canvas-anim { 213 | transition: all .4s ease-out; 214 | -webkit-transition: all .4s ease-out; 215 | -moz-transition: all .4s ease-out; 216 | -ms-transition: all .4s ease-out; 217 | } 218 | 219 | .nav-bar-buttons { 220 | display: inline-block; 221 | font-weight: 400; 222 | text-align: center; 223 | white-space: nowrap; 224 | vertical-align: middle; 225 | -webkit-user-select: none; 226 | -moz-user-select: none; 227 | -ms-user-select: none; 228 | user-select: none; 229 | border: 1px solid transparent; 230 | padding: 0.375 rem 0.75 rem; 231 | font-size: 1rem; 232 | line-height: 1.5; 233 | border-radius: 0.25 rem; 234 | transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out; 235 | color: #f8f9fa 236 | } 237 | 238 | .loader { 239 | position: fixed; 240 | left: 44%; 241 | top: 32%; 242 | width: 50px; 243 | padding: 8px; 244 | aspect-ratio: 1; 245 | border-radius: 50%; 246 | background: #b59154; 247 | --_m: 248 | conic-gradient(#0000 10%,#000), 249 | linear-gradient(#000 0 0) content-box; 250 | -webkit-mask: var(--_m); 251 | mask: var(--_m); 252 | -webkit-mask-composite: source-out; 253 | mask-composite: subtract; 254 | animation: l3 1s infinite linear; 255 | } 256 | 257 | @keyframes l3 { 258 | to{transform: rotate(1turn)} 259 | } 260 | 261 | input[type=password], 262 | input[type=text] { 263 | border: 1px solid #ccc; 264 | border-radius: 5px; 265 | display: block; 266 | font-size: 18px; 267 | margin: 5px 0px 10px; 268 | padding: 8px; 269 | width: 250px; 270 | } 271 | label { 272 | vertical-align: middle; 273 | } 274 | 275 | .loader-hori{ 276 | width: 100%; 277 | height: 12px; 278 | display: none; 279 | background-color: #FFF; 280 | background-image: linear-gradient(45deg, rgba(0, 0, 0, 0.25) 25%, transparent 25%, transparent 50%, rgba(0, 0, 0, 0.25) 50%, rgba(0, 0, 0, 0.25) 75%, transparent 75%, transparent); 281 | font-size: 30px; 282 | background-size: 1em 1em; 283 | box-sizing: border-box; 284 | animation: barStripe 1s linear infinite; 285 | } 286 | 287 | @keyframes barStripe { 288 | 0% { 289 | background-position: 1em 0; 290 | } 291 | 100% { 292 | background-position: 0 0; 293 | } 294 | } 295 | 296 | .error-message { 297 | color: red; 298 | padding-top: 60px; 299 | text-align: center; 300 | font-family: 'Lucida Grande', 'Lucida Sans Unicode', Arial, Helvetica, sans-serif; 301 | } 302 | 303 | .dynamic-buttons { 304 | position: fixed; 305 | z-index: 999999; 306 | top: 25px; 307 | left: 75%; 308 | width: 300px; 309 | display: inline-flex; 310 | justify-content: space-around; 311 | } 312 | 313 | .json-download-button { 314 | margin: 0px 5px !important; 315 | font-size: 15px !important; 316 | background-color: #b59154 !important; 317 | border-color: #b59154 !important; 318 | outline: none !important; 319 | } 320 | 321 | .publish-button { 322 | margin: 0px 5px !important; 323 | font-size: 15px !important; 324 | background-color: #b59154 !important; 325 | border-color: #b59154 !important; 326 | outline: none !important; 327 | } 328 | 329 | .titleDiv { 330 | position: relative 331 | } 332 | 333 | .tableTitle { 334 | text-align: center; 335 | color: rgb(51, 51, 51); 336 | font-size: 18px; 337 | fill: rgb(51, 51, 51); 338 | border: 1px; 339 | } 340 | 341 | .card-body-bottom { 342 | flex: 1 1 auto; 343 | padding: 1.25rem; 344 | height: 200px; 345 | padding-left: 15px; 346 | } -------------------------------------------------------------------------------- /src/unit_test_case/scenario_7/testconfig.yml: -------------------------------------------------------------------------------- 1 | tests: 2 | - test: 3 | test_case_no: 1 4 | x_axis_monthly_display: "trailing_twelve_months" 5 | week_ending: 25-MAR-2023 6 | fiscal_year_end_month: DEC 7 | metric_name: "Perfect_Sessions_percentTrailing" 8 | 9 | # 1 test 10 | cy_6_weeks: [0.43, 0.42, 0.43, 0.43, 0.43, 0.42] 11 | 12 | # 2 test 13 | py_6_weeks: [0.41, 0.41, 0.41, 0.41, 0.4, 0.4] 14 | 15 | # 3 test 16 | cy_monthly: [0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.41, 0.42, 0.42, 0.42, 0.43] 17 | 18 | # 4 test 19 | py_monthly: [!!float .nan, !!float .nan ,!!float .nan , 0.26, 0.35, 0.37, 0.38, 0.39, 0.4, 0.4, 0.55, 0.41] 20 | 21 | # 5 test 22 | x_axis: [ 'wk 8', 'wk 9', 'wk 10', 'wk 11', 'wk 12', 'wk 13', 'Mar', 'Apr', "May", "Jun", "Jul", "Aug", 23 | "Sep", "Oct", "Nov", "Dec", "Jan", "Feb" ] 24 | 25 | 26 | # 6 test 27 | box_totals: [0.422906841391559, -1.163424697897475, 4.938076489300514, 0.42892078008261525, 6.032123562956304, 0.4241802606577491, -7.475212736793613, 28 | 0.4241802606577491, -7.475212736793613] 29 | 30 | # 7 test 31 | cy_monthly_data_frame_length: 14 32 | # 8 test 33 | py_monthly_data_frame_length: 22 34 | 35 | - test: 36 | test_case_no: 2 37 | x_axis_monthly_display: "fiscal_year" 38 | week_ending: 25-MAR-2023 39 | fiscal_year_end_month: DEC 40 | metric_name: "Perfect_Sessions_percentFiscal" 41 | 42 | # 1 test 43 | cy_6_weeks: [0.43, 0.42, 0.43, 0.43, 0.43, 0.42] 44 | 45 | # 2 test 46 | py_6_weeks: [0.41, 0.41, 0.41, 0.41, 0.4, 0.4] 47 | 48 | # 3 test 49 | cy_monthly: [0.42, 0.43, 0.43, 0.43, !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan] 50 | 51 | # 4 test 52 | py_monthly: [0.55, 0.41, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.41, 0.42, 0.42] 53 | 54 | # 5 test 55 | x_axis: [ 'wk 8', 'wk 9', 'wk 10', 'wk 11', 'wk 12', 'wk 13', 'Jan', 'Feb', 'Mar', 'Apr'] 56 | 57 | 58 | # 6 test 59 | box_totals: [0.422906841391559, -1.163424697897475, 4.938076489300514, 0.42892078008261525, 6.032123562956304, 0.4241802606577491, -7.475212736793613, 60 | 0.4241802606577491, -7.475212736793613] 61 | 62 | # 7 test 63 | cy_monthly_data_frame_length: 14 64 | # 8 test 65 | py_monthly_data_frame_length: 22 66 | 67 | - test: 68 | test_case_no: 3 69 | x_axis_monthly_display: "trailing_twelve_months" 70 | week_ending: 25-MAR-2023 71 | fiscal_year_end_month: DEC 72 | metric_name: "Perfect_Sessions_percent_non_bpsTrailing" 73 | 74 | # 1 test 75 | cy_6_weeks: [ 0.43, 0.42, 0.43, 0.43, 0.43, 0.42 ] 76 | 77 | # 2 test 78 | py_6_weeks: [ 0.41, 0.41, 0.41, 0.41, 0.4, 0.4 ] 79 | 80 | # 3 test 81 | cy_monthly: [ 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.41, 0.42, 0.42, 0.42, 0.43 ] 82 | 83 | # 4 test 84 | py_monthly: [ !!float .nan, !!float .nan ,!!float .nan , 0.26, 0.35, 0.37, 0.38, 0.39, 0.4, 0.4, 0.55, 0.41 ] 85 | 86 | # 5 test 87 | x_axis: [ 'wk 8', 'wk 9', 'wk 10', 'wk 11', 'wk 12', 'wk 13', 'Mar', 'Apr', "May", "Jun", "Jul", "Aug", 88 | "Sep", "Oct", "Nov", "Dec", "Jan", "Feb" ] 89 | 90 | 91 | # 6 test 92 | box_totals: [0.422906841391559, -1.163424697897475, 4.938076489300514, 0.42892078008261525, 6.032123562956304, 93 | 0.4241802606577491, -7.475212736793613, 0.4241802606577491, -7.475212736793613] 94 | 95 | # 7 test 96 | cy_monthly_data_frame_length: 14 97 | # 8 test 98 | py_monthly_data_frame_length: 22 99 | 100 | - test: 101 | test_case_no: 4 102 | x_axis_monthly_display: "fiscal_year" 103 | week_ending: 25-MAR-2023 104 | fiscal_year_end_month: DEC 105 | metric_name: "Perfect_Sessions_percent_non_bpsFiscal" 106 | 107 | # 1 test 108 | cy_6_weeks: [ 0.43, 0.42, 0.43, 0.43, 0.43, 0.42 ] 109 | 110 | # 2 test 111 | py_6_weeks: [ 0.41, 0.41, 0.41, 0.41, 0.4, 0.4 ] 112 | 113 | # 3 test 114 | cy_monthly: [ 0.42, 0.43, 0.43, 0.43, !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan] 115 | 116 | # 4 test 117 | py_monthly: [0.55, 0.41, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.41, 0.42, 0.42] 118 | 119 | # 5 test 120 | x_axis: ['wk 8', 'wk 9', 'wk 10', 'wk 11', 'wk 12', 'wk 13', 'Jan', 'Feb', 'Mar', 'Apr'] 121 | 122 | 123 | # 6 test 124 | box_totals: [0.422906841391559, -1.163424697897475, 4.938076489300514, 0.42892078008261525, 6.032123562956304, 125 | 0.4241802606577491, -7.475212736793613, 0.4241802606577491, -7.475212736793613] 126 | 127 | # 7 test 128 | cy_monthly_data_frame_length: 14 129 | # 8 test 130 | py_monthly_data_frame_length: 22 131 | 132 | - test: 133 | test_case_no: 5 134 | x_axis_monthly_display: "trailing_twelve_months" 135 | week_ending: 25-MAR-2023 136 | graph_prior_year_flag: false 137 | fiscal_year_end_month: DEC 138 | metric_name: "Perfect_Sessions_percentTrailingWOW" 139 | 140 | # 1 test 141 | cy_6_weeks: [-0.002224413630479316, -0.0035304127403590346, 0.015170602648350462, 0.002855308243237653, -0.009336112053825785, -0.01163424697897475 ] 142 | 143 | # 2 test 144 | py_6_weeks: [ 0.41, 0.41, 0.41, 0.41, 0.4, 0.4 ] 145 | 146 | # 3 test 147 | cy_monthly: [ !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan] 148 | 149 | # 4 test 150 | py_monthly: [ !!float .nan, !!float .nan ,!!float .nan , !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan ] 151 | 152 | # 5 test 153 | x_axis: [ 'wk 8', 'wk 9', 'wk 10', 'wk 11', 'wk 12', 'wk 13', 'Mar', 'Apr', "May", "Jun", "Jul", "Aug", 154 | "Sep", "Oct", "Nov", "Dec", "Jan", "Feb" ] 155 | 156 | 157 | # 6 test 158 | box_totals: [ !!float .nan, !!float .nan ,!!float .nan , !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan] 159 | 160 | # 7 test 161 | cy_monthly_data_frame_length: 22 162 | # 8 test 163 | py_monthly_data_frame_length: 22 164 | 165 | - test: 166 | test_case_no: 6 167 | x_axis_monthly_display: "trailing_twelve_months" 168 | week_ending: 25-MAR-2023 169 | graph_prior_year_flag: false 170 | fiscal_year_end_month: DEC 171 | metric_name: "Perfect_Sessions_percentTrailingYOY" 172 | 173 | # 1 test 174 | cy_6_weeks: [0.046639936623998235, 0.04695289060962482, 0.062129252011754454, 0.0663311598148606, 0.059046604089461274, 0.049380764893005136 ] 175 | 176 | # 2 test 177 | py_6_weeks: [ !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan ] 178 | 179 | # 3 test 180 | cy_monthly: [ !!float .nan, !!float .nan, !!float .nan, 0.5225036858650378, 0.15094552685358797, 0.09124432895293477, 0.04911373280044429, 0.07094277101038049, 0.05366881667802348, 0.04984166660472944, -0.24421533016068542, 0.04491317862155397] 181 | 182 | # 4 test 183 | py_monthly: [ !!float .nan, !!float .nan ,!!float .nan , !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan, !!float .nan ] 184 | 185 | # 5 test 186 | x_axis: [ 'wk 8', 'wk 9', 'wk 10', 'wk 11', 'wk 12', 'wk 13', 'Mar', 'Apr', "May", "Jun", "Jul", "Aug", 187 | "Sep", "Oct", "Nov", "Dec", "Jan", "Feb" ] 188 | 189 | 190 | # 6 test 191 | box_totals: [ 0.049380764893005136, -16.369847759257183, !!float .nan, 0.06032123562956304, !!float .nan, -0.07475212736793613, !!float .nan, -0.07475212736793613, !!float .nan] 192 | 193 | # 7 test 194 | cy_monthly_data_frame_length: 22 195 | # 8 test 196 | py_monthly_data_frame_length: 22 197 | -------------------------------------------------------------------------------- /src/publish_utility.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | from pathlib import Path 5 | 6 | import boto3 7 | from azure.identity import DefaultAzureCredential 8 | from azure.storage.blob import BlobServiceClient 9 | from google.cloud import storage 10 | 11 | 12 | class PublishWbr: 13 | """ 14 | Class to handle uploading and downloading data from various object storage services 15 | such as AWS S3, GCP, and Azure. If no storage option is provided, files are saved locally. 16 | 17 | Attributes: 18 | storage_option (str): The storage service to use ('s3', 'gcp', 'azure', or local). 19 | object_storage_bucket (str): The bucket or container name in the chosen storage service. 20 | s3_client (boto3.client): The client object for S3 interaction (used if storage_option is 's3'). 21 | gcp_client (google.cloud.storage.Client): The client object for GCP interaction (used if storage_option is 'gcp'). 22 | azure_client (azure.storage.blob.BlobServiceClient): The client object for Azure interaction (used if storage_option is 'azure'). 23 | """ 24 | 25 | def __init__(self, storage_option, object_storage_bucket): 26 | """ 27 | Initializes the PublishWbr class based on the chosen storage option. 28 | 29 | Args: 30 | storage_option (str): The storage service to use ('s3', 'gcp', 'azure', or local). 31 | object_storage_bucket (str): The bucket or container name in the chosen storage service. 32 | 33 | Raises: 34 | Warning: Logs a warning if no storage option is provided. 35 | """ 36 | self.s3_client = None 37 | self.gcp_client = None 38 | self.azure_client = None 39 | self.object_storage_bucket = object_storage_bucket 40 | self.storage_option = storage_option 41 | 42 | if storage_option == "s3": 43 | aws_access_key_id = os.environ.get("S3_STORAGE_KEY") or None 44 | s3config = { 45 | "region_name": os.environ.get("S3_REGION_NAME") or "", 46 | "aws_access_key_id": aws_access_key_id, 47 | "aws_secret_access_key": os.environ.get("S3_STORAGE_SECRET") or "" 48 | } 49 | if os.environ.get("S3_STORAGE_ENDPOINT"): 50 | s3config["endpoint_url"] = os.environ.get("S3_STORAGE_ENDPOINT") 51 | self.s3_client = boto3.client('s3', **s3config) if aws_access_key_id else boto3.client('s3') 52 | 53 | elif storage_option == "gcp": 54 | gcp_service_account_json_file = os.getenv("GCP_SERVICE_ACCOUNT_PATH") # JSON file path 55 | self.gcp_client = get_gcp_client_for_credentials(gcp_service_account_json_file) \ 56 | if gcp_service_account_json_file else get_gcp_client_for_iam() 57 | 58 | elif storage_option == "azure": 59 | azure_connection_string = os.getenv("AZURE_CONNECTION_STRING") 60 | self.azure_client = BlobServiceClient.from_connection_string(azure_connection_string) \ 61 | if azure_connection_string else get_azure_from_default_credentials() 62 | 63 | else: 64 | logging.warning("No OBJECT_STORAGE_OPTION is provided hence the published report will be saved locally") 65 | 66 | def upload(self, data, destination_file_path): 67 | """ 68 | Uploads data to the selected object storage or saves it locally if no storage option is selected. 69 | 70 | Args: 71 | data (list|dict): The data to upload. 72 | destination_file_path (str): The destination file path in the object storage or local directory. 73 | 74 | Raises: 75 | Exception: Raises exceptions specific to the storage service (if any occur). 76 | """ 77 | if self.storage_option == "s3": 78 | byte_data = bytes(json.dumps(data).encode('utf-8')) 79 | self.s3_client.put_object(Body=byte_data, Bucket=self.object_storage_bucket, Key=destination_file_path) 80 | 81 | elif self.storage_option == "gcp": 82 | byte_data = bytes(json.dumps(data).encode('utf-8')) 83 | bucket = self.gcp_client.bucket(self.object_storage_bucket) 84 | blob = bucket.blob(destination_file_path) 85 | blob.upload_from_string(byte_data, content_type='application/json') 86 | 87 | elif self.storage_option == "azure": 88 | byte_data = bytes(json.dumps(data).encode('utf-8')) 89 | blob_client = self.azure_client.get_blob_client(container=self.object_storage_bucket, 90 | blob=destination_file_path) 91 | blob_client.upload_blob(byte_data) 92 | 93 | else: 94 | path = str(Path(os.path.dirname(__file__)).parent) 95 | file_path = path + '/publish/' + destination_file_path 96 | os.makedirs(os.path.dirname(file_path), exist_ok=True) 97 | with open(file_path, 'w') as json_file: 98 | json.dump(data, json_file, indent=4) 99 | 100 | def download(self, path): 101 | """ 102 | Downloads data from the selected object storage or locally if no storage option is selected. 103 | 104 | Args: 105 | path (str): The file path in the object storage or local directory. 106 | 107 | Returns: 108 | dict: The data loaded from the storage or local file. 109 | 110 | Raises: 111 | Exception: Raises exceptions specific to the storage service (if any occur). 112 | """ 113 | if self.storage_option == "s3": 114 | response = self.s3_client.get_object(Bucket=self.object_storage_bucket, Key=path) 115 | json_file_content = response['Body'].read() 116 | return json.loads(json_file_content) 117 | 118 | elif self.storage_option == "gcp": 119 | bucket = self.gcp_client.bucket(self.object_storage_bucket) 120 | blob = bucket.blob(path) 121 | return json.loads(blob.download_as_string(client=None)) 122 | 123 | elif self.storage_option == "azure": 124 | blob_client = self.azure_client.get_blob_client(container=self.object_storage_bucket, 125 | blob=path) 126 | stream = blob_client.download_blob() 127 | return json.loads(stream.readall()) 128 | 129 | else: 130 | base_path = str(Path(os.path.dirname(__file__)).parent) 131 | file = base_path + '/publish/' + path 132 | current_file = open(file) 133 | return json.load(current_file) 134 | 135 | 136 | def get_gcp_client_for_iam(): 137 | """ 138 | Initializes a GCP storage client using IAM credentials. 139 | 140 | Returns: 141 | google.cloud.storage.Client: The GCP storage client initialized with IAM credentials. 142 | """ 143 | return storage.Client() 144 | 145 | 146 | def get_gcp_client_for_credentials(credentials_json_file): 147 | """ 148 | Initializes a GCP storage client using a service account JSON. 149 | 150 | Args: 151 | credentials_json_file (str): The JSON string containing the GCP service account credentials. 152 | 153 | Returns: 154 | google.cloud.storage.Client: The GCP storage client initialized with the provided service account JSON. 155 | 156 | Raises: 157 | Exception: If the client initialization fails, an exception is raised and logged. 158 | """ 159 | try: 160 | with open(credentials_json_file, mode="r") as credentials_json: 161 | # Initialize GCP client with the IAM credentials file 162 | return storage.Client.from_service_account_json(credentials_json.name) 163 | 164 | except Exception as e: 165 | logging.error(f"Failed to upload to GCP: {str(e)}") 166 | 167 | 168 | def get_azure_from_default_credentials(): 169 | """ 170 | Initializes an Azure BlobServiceClient using the DefaultAzureCredential for authentication. 171 | 172 | Returns: 173 | azure.storage.blob.BlobServiceClient: The Azure BlobServiceClient initialized with the default credentials. 174 | """ 175 | default_credential = DefaultAzureCredential() 176 | account_url = os.getenv("AZURE_ACCOUNT_URL") 177 | return BlobServiceClient(account_url, credential=default_credential) 178 | -------------------------------------------------------------------------------- /src/unit_test_case/scenario_6/config.yaml: -------------------------------------------------------------------------------- 1 | setup: 2 | week_ending: 25-MAR-2023 3 | week_number: 13 4 | title: PracticePlus WBR 5 | fiscal_year_end_month: DEC 6 | block_starting_number: 1 7 | 8 | metrics: 9 | #GRANULAR ACTUAL METRICS: 10 | Booked_Sessions: 11 | column: Booked_Sessions 12 | aggf: sum 13 | 14 | Total_PMs: 15 | column: Total_PMs 16 | aggf: last 17 | 18 | minTotal_PMs: 19 | column: Total_PMs 20 | aggf: first 21 | 22 | Total_PMsv2: 23 | column: Total_PMsv2 24 | aggf: last 25 | 26 | Booked_Sessionsv2: 27 | column: Booked_Sessionsv2 28 | aggf: sum 29 | 30 | Tele_Sessions: 31 | column: Tele_Sessions 32 | aggf: sum 33 | 34 | Tele_NoShows: 35 | column: Tele_NoShows 36 | aggf: sum 37 | 38 | Tele_OnTime: 39 | column: Tele_OnTime 40 | aggf: sum 41 | 42 | Tele_Shows: 43 | column: Tele_Shows 44 | aggf: sum 45 | 46 | Self_Serve: 47 | column: SSS_Sessions 48 | aggf: sum 49 | 50 | Billed_Sessions: 51 | column: Billed_Sessions 52 | aggf: sum 53 | 54 | Out_Of_Pocket_Sessions: 55 | column: OOP_Sessions 56 | aggf: sum 57 | 58 | PO_LeadTime: 59 | column: PO_LeadTime 60 | aggf: mean 61 | 62 | Perfect_Sessions: 63 | column: Perfect_Sessions 64 | aggf: sum 65 | 66 | TeleSuccess: 67 | column: TeleSuccess 68 | aggf: sum 69 | 70 | UniqueVisits: 71 | column: UniqueVisits 72 | aggf: sum 73 | 74 | FTSignUps: 75 | column: FTSignUps 76 | aggf: sum 77 | 78 | FTConversion: 79 | column: FTConversion 80 | aggf: mean 81 | 82 | Churns: 83 | column: Churns 84 | aggf: sum 85 | 86 | DAU: 87 | column: DAU 88 | aggf: mean 89 | 90 | PlatinumPMs: 91 | column: PlatinumPMs 92 | aggf: max 93 | 94 | MRR: 95 | column: MRR 96 | aggf: last 97 | 98 | 99 | #ACTUAL METRICS THAT REQUIRE CALCULATION: 100 | Booked_Sessions_Per_PM: 101 | function: 102 | divide: 103 | - metric: 104 | name: Booked_Sessions 105 | - metric: 106 | name: Total_PMs 107 | 108 | Tele_Sessions_Per_PM: 109 | function: 110 | divide: 111 | - metric: 112 | name: Tele_Sessions 113 | - metric: 114 | name: Total_PMsv2 115 | 116 | Tele_Sessions_percent: 117 | function: 118 | divide: 119 | - metric: 120 | name: Tele_Sessions 121 | - metric: 122 | name: Booked_Sessionsv2 123 | 124 | TeleSuccess_percent: 125 | function: 126 | divide: 127 | - metric: 128 | name: TeleSuccess 129 | - metric: 130 | name: Tele_Shows 131 | 132 | Tele_NoShow_percent: 133 | function: 134 | divide: 135 | - metric: 136 | name: Tele_NoShows 137 | - metric: 138 | name: Tele_Sessions 139 | 140 | Tele_OnTime_percent: 141 | function: 142 | divide: 143 | - metric: 144 | name: Tele_OnTime 145 | - metric: 146 | name: Tele_Shows 147 | 148 | SSS_Sessions_percent: 149 | function: 150 | divide: 151 | - metric: 152 | name: Self_Serve 153 | - metric: 154 | name: Booked_Sessions 155 | 156 | Billed_Session_percent: 157 | function: 158 | divide: 159 | - metric: 160 | name: Billed_Sessions 161 | - metric: 162 | name: Out_Of_Pocket_Sessions 163 | 164 | Perfect_Sessions_percent: 165 | function: 166 | divide: 167 | - metric: 168 | name: Perfect_Sessions 169 | - metric: 170 | name: Booked_Sessionsv2 171 | 172 | DAU_per_PM: 173 | function: 174 | divide: 175 | - metric: 176 | name: DAU 177 | - metric: 178 | name: minTotal_PMs 179 | 180 | ChurnRate: 181 | function: 182 | divide: 183 | - metric: 184 | name: Churns 185 | - metric: 186 | name: minTotal_PMs 187 | 188 | PlatinumPMs_percent: 189 | function: 190 | divide: 191 | - metric: 192 | name: PlatinumPMs 193 | - metric: 194 | name: Total_PMs 195 | 196 | #GRANULAR TARGET METRICS: 197 | TeleSession TARGET: 198 | column: TeleSession_Target 199 | aggf: mean 200 | 201 | TeleSuccess TARGET: 202 | column: TeleSuccess_Target 203 | aggf: mean 204 | 205 | PO_Target: 206 | column: PO_Target 207 | aggf: mean 208 | 209 | FTSignUps TARGET: 210 | column: FTSignUps_Target 211 | aggf: sum 212 | 213 | FTConversion TARGET: 214 | column: FTConversion_Target 215 | aggf: mean 216 | 217 | TotalPMs_TARGET: 218 | column: Total_PMs_Target 219 | aggf: first 220 | 221 | Churns TARGET: 222 | column: Churns_Target 223 | aggf: sum 224 | 225 | MRR_TARGET: 226 | column: MRR_Target 227 | aggf: last 228 | 229 | PerfectSessions TARGET: 230 | column: PerfectSessions_Target 231 | aggf: mean 232 | 233 | #TARGET METRICS THAT REQUIRE CALCULATION: 234 | ChurnRate TARGET: 235 | function: 236 | divide: 237 | - metric: 238 | name: Churns TARGET 239 | - metric: 240 | name: TotalPMs_TARGET 241 | 242 | deltaChurnRate: 243 | function: 244 | difference: 245 | - metric: 246 | name: ChurnRate 247 | - metric: 248 | name: ChurnRate TARGET 249 | 250 | vsChurnRate: 251 | function: 252 | divide: 253 | - metric: 254 | name: deltaChurnRate 255 | - metric: 256 | name: ChurnRate TARGET 257 | 258 | 259 | deltaPerfectSessions: 260 | function: 261 | difference: 262 | - metric: 263 | name: Perfect_Sessions_percent 264 | - metric: 265 | name: PerfectSessions TARGET 266 | 267 | vsPerfectSessions: 268 | function: 269 | divide: 270 | - metric: 271 | name: deltaPerfectSessions 272 | - metric: 273 | name: PerfectSessions TARGET 274 | 275 | deltaFTSignUps: 276 | function: 277 | difference: 278 | - metric: 279 | name: FTSignUps 280 | - metric: 281 | name: FTSignUps TARGET 282 | 283 | vsFTSignUps: 284 | function: 285 | divide: 286 | - metric: 287 | name: deltaFTSignUps 288 | - metric: 289 | name: FTSignUps TARGET 290 | 291 | deltaFTConversion: 292 | function: 293 | difference: 294 | - metric: 295 | name: FTConversion 296 | - metric: 297 | name: FTConversion TARGET 298 | 299 | vsFTConversion: 300 | function: 301 | divide: 302 | - metric: 303 | name: deltaFTConversion 304 | - metric: 305 | name: FTConversion TARGET 306 | 307 | deltaTotalPMs: 308 | function: 309 | difference: 310 | - metric: 311 | name: Total_PMs 312 | - metric: 313 | name: TotalPMs_TARGET 314 | 315 | vsTotalPMs: 316 | function: 317 | divide: 318 | - metric: 319 | name: deltaTotalPMs 320 | - metric: 321 | name: TotalPMs_TARGET 322 | 323 | deltaMRR: 324 | function: 325 | difference: 326 | - metric: 327 | name: MRR 328 | - metric: 329 | name: MRR_TARGET 330 | 331 | vsMRR: 332 | function: 333 | divide: 334 | - metric: 335 | name: deltaMRR 336 | - metric: 337 | name: MRR_TARGET 338 | 339 | #SLIDES LAYOUT: 340 | deck: 341 | - block: 342 | ui_type: 6_12Graph 343 | title: Total_PMs 344 | metrics: 345 | Total_PMs: 346 | line_style: primary 347 | graph_prior_year_flag: true 348 | - block: 349 | ui_type: 6_12Graph 350 | title: Total_PMsv2 351 | metrics: 352 | Total_PMsv2: 353 | line_style: primary 354 | graph_prior_year_flag: true 355 | - block: 356 | ui_type: 6_12Graph 357 | title: MRR 358 | metrics: 359 | MRR: 360 | line_style: primary 361 | graph_prior_year_flag: true 362 | - block: 363 | ui_type: 6_12Graph 364 | title: TotalPMs_TARGET 365 | metrics: 366 | TotalPMs_TARGET: 367 | line_style: primary 368 | graph_prior_year_flag: true 369 | - block: 370 | ui_type: 6_12Graph 371 | title: MRR_TARGET 372 | metrics: 373 | MRR_TARGET: 374 | line_style: primary 375 | graph_prior_year_flag: true 376 | - block: 377 | ui_type: 6_12Graph 378 | title: MRR_TARGET 379 | metrics: 380 | MRR_TARGET: 381 | line_style: primary 382 | graph_prior_year_flag: true 383 | -------------------------------------------------------------------------------- /src/web/static/demo_uploads/1-wbr-sample-config.yaml: -------------------------------------------------------------------------------- 1 | # There are three sections in a WBR yaml file: setup, metrics, and deck. 2 | # 3 | # The setup: section defines the information you need to start building the WBR deck. 4 | # The only two required values are week_ending and week_number. 5 | 6 | setup: 7 | week_ending: 25-SEP-2021 8 | week_number: 38 9 | title: Weekly Business Review Sample Report 10 | fiscal_year_end_month: DEC # set to last month of fiscal year. If no value is provided, DEC is used. 11 | block_starting_number: 1 # Default is 1. Use this if you need multiple yaml files for a single WBR deck. 12 | 13 | 14 | # The metrics: section defines all the metrics you'll use in the WBR deck. 15 | # The two key components of a metric are 1) the source data and 2) the aggregation function. 16 | # 17 | # 1) source data 18 | # A metric needs to be derived from source data. The source data can be based on columns in the csv file, 19 | # other metrics, or a combination of both. Valid metrics definitions instructions include 20 | # a) a one-to-one mapping with a column in the csv file 21 | # b) a function based on one or more columns in the csv file or metrics. The supported functions are 22 | # divide 23 | # sum 24 | # difference 25 | # product 26 | # c) an aggregation (filter) of a column in the csv file or a previously defined metric. A simple query 27 | # with a single where clause evaluation is supported. More complex data filtering should be handled 28 | # upstream in the system that houses the source data. 29 | # where "==" is a relation 30 | # possible relations -> 31 | # == (equal) 32 | # != (not equal) 33 | # > (greater than) 34 | # < (less than) 35 | # >= (greater than or equal) 36 | # <= (less than or equal) 37 | # 38 | # 2) aggregation function (aggf) 39 | # The aggf tells the system what function to use when resampling the daily data values to the weekly, 40 | # monthly, month-to-date, quarter-to-date and year-to-date transformations needed in a WBR deck. 41 | # The most common aggf functions are 42 | # sum 43 | # mean 44 | # min 45 | # max 46 | # 47 | # You can build your own custom functions. 48 | # 49 | # 50 | 51 | metrics: 52 | 53 | # Here's a simple example of a 1-1 mapping from a column in the csv file to a metric. 54 | # We will aggregate the metric by summing it. 55 | Impressions: 56 | column: Impressions 57 | aggf: sum 58 | 59 | Clicks: 60 | column: Clicks 61 | aggf: sum 62 | 63 | # Here's a simple example of a 1-1 mapping from a column in the csv file to a metric. 64 | # We will aggregate the metric by caluculating the mean. Mean is typically used for 65 | # metrics like Daily Active Users, Monthly Active Users,... 66 | 67 | Defects/Million: 68 | column: "Defects/Million" 69 | aggf: mean 70 | 71 | # Here's an example using a function from two previously defined metrics to define a new metric. 72 | # ClickThruRate is calcluated by summing the Clicks and dividing it by the sum of the 73 | # Impressions in each of the relevant time periods. 74 | 75 | ClickThruRate: 76 | function: 77 | divide: 78 | - metric: 79 | name: Clicks 80 | - metric: 81 | name: Impressions 82 | 83 | PageViews: 84 | column: "PageViews" 85 | aggf: sum 86 | 87 | # Some metrics you track will also have a plan (or target). A plan metric is treated the same 88 | # as any other type of metric. In the visualization section below, you'll see various ways 89 | # to display actual and plan metrics together. 90 | 91 | PageViews Target: 92 | column: "PageViews__Target" 93 | aggf: sum 94 | 95 | MobilePage_Views: 96 | column: "MobilePageViews" 97 | aggf: sum 98 | 99 | # VarianceToPlanPageViews is the difference between actual and plan (target) 100 | 101 | VarianceToPlanPageViews: 102 | function: 103 | difference: 104 | - metric: 105 | name: PageViews 106 | - metric: 107 | name: PageViews Target 108 | 109 | # PercentageVarianceToPlanPageViews is the % variance between actual and plan (target) 110 | 111 | PercentageVarianceToPlanPageViews: 112 | function: 113 | divide: 114 | - metric: 115 | name: VarianceToPlanPageViews 116 | - metric: 117 | name: PageViews Target 118 | 119 | PercentageCustomerGroupGreen: 120 | column: "pct_customer_group_1" 121 | aggf: mean 122 | PercentageCustomerGroupOrange: 123 | column: "pct_customer_group_2" 124 | aggf: mean 125 | PercentageCustomerGroupRed: 126 | column: "pct_customer_group_3" 127 | aggf: mean 128 | 129 | 130 | # The deck: section defines how each block in the deck will be rendered. The required elements needed to 131 | # render a block are the ui_type and one or more metrics defined in the metrics: section above. 132 | # Note, that for each metric that was defined, we also create three period-over-period comparisions 133 | # that are also available to display in the deck. For instance if you have a metric my_metric, we also create 134 | # my_metricYOY (a year-over-year percentage comparison) 135 | # my_metricMOM (a month-over-month percentage comparison) 136 | # my_metricWOW (a week-over-week percentage comparison) 137 | # 138 | # The supported ui_types for the WBR data are 139 | # 140 | # 6_12Graph - The most common WBR chart. It displays the trailing 6 weeks of data along with the 141 | # trailing 12 months of data on the same x-axis. It also has a summary table below the 142 | # chart that lists the last week, month-to-date, quarter-to-date, and year-to-date data 143 | # along with the relevent period-over-over-period comparisons expressed as a percentage. 144 | # 145 | # 6_WeeksTable - Displays the trailing 6 weeks of data along with the quarter-to-date and 146 | # year-to-date data for metrics. You will need to define each row in the table. 147 | # A row can contain metrics or text for headers and blank lines. 148 | # 149 | # 12_MonthsTable - Displays the trailing 12 months of data. The 6_WeeksTable and 12_MonthsTable 150 | # work best when displayed together side-by-side. 151 | # 152 | # section - used to display text such as titles, section headers, or blank lines for enhanced readability. 153 | # 154 | # Any CSS style can be passed and will be rendered for the header column in 6_WeeksTable and 12_MonthsTable. 155 | # 156 | # There are three line_styles for the 6_12Graph: primary, secondary, and target. The primary and secondary 157 | # line_styles graph both current year and prior year values by default (with different colors and line markers. 158 | # The target line_style plots line markers (no connecting lines) and only plots the current year values. 159 | # 160 | # See below for various y-axis scaling options. 161 | # 162 | 163 | deck: 164 | - block: 165 | ui_type: 6_12Graph 166 | title: Ad Impressions (Millions) 167 | y_scaling: "##.2MM" 168 | metrics: 169 | Impressions: 170 | line_style: primary 171 | graph_prior_year_flag: true 172 | 173 | - block: 174 | ui_type: 6_12Graph 175 | title: Clicks from Ads (Thousands) 176 | y_scaling: "##KK" 177 | metrics: 178 | Clicks: 179 | line_style: primary 180 | graph_prior_year_flag: true 181 | 182 | - block: 183 | ui_type: 6_12Graph 184 | title: Click Through Rate 185 | y_scaling: "##.1%" 186 | metrics: 187 | ClickThruRate: 188 | line_style: primary 189 | graph_prior_year_flag: true 190 | legend_name: CTR (%) 191 | 192 | - block: 193 | ui_type: 6_12Graph 194 | title: Defects/Million 195 | y_scaling: "##.1KK" 196 | metrics: 197 | Defects/Million: 198 | line_style: primary 199 | graph_prior_year_flag: false 200 | legend_name: Defects/Million 201 | 202 | # You can put more than one metric in a 6_12Graph. Here we are displaying 3 different metrics. 203 | # The target line style shows only the point markers with no line connecting data points. 204 | # Since it's a target, we choose not plot the prior year value. 205 | # The third metric is using a different line_style from the primary (default) line_style. 206 | 207 | - block: 208 | ui_type: 6_12Graph 209 | title: Total Page Views (Millions) 210 | y_scaling: "##MM" 211 | metrics: 212 | PageViews: 213 | graph_prior_year_flag: true 214 | legend_name: Page Views 215 | PageViews Target: 216 | line_style: target 217 | graph_prior_year_flag: false 218 | legend_name: Page Views - Target 219 | MobilePage_Views: 220 | line_style: secondary 221 | graph_prior_year_flag: true 222 | legend_name: Mobile Page Views 223 | 224 | - block: 225 | ui_type: section 226 | title: "" 227 | 228 | - block: 229 | ui_type: 6_WeeksTable 230 | title: "Page Views Actual vs Plan Summary" 231 | rows: 232 | - row: 233 | header: "Page Views" 234 | style: "font-weight: bold; background-color: LightGrey; text-align:left;" 235 | - row: 236 | header: "Actual" 237 | metric: PageViews 238 | style: "text-align:right;" 239 | y_scaling: "##MM" 240 | - row: 241 | header: "Plan" 242 | metric: PageViews Target 243 | style: "text-align:right;" 244 | y_scaling: "##MM" 245 | - row: 246 | header: "Variance to Plan" 247 | metric: VarianceToPlanPageViews 248 | style: "text-align:right;" 249 | y_scaling: "##MM" 250 | - row: 251 | header: "Variance to Plan (%)" 252 | metric: PercentageVarianceToPlanPageViews 253 | style: "font-style: italic; text-align:right;" 254 | y_scaling: "##.1%" 255 | - row: 256 | header: "YOY" 257 | metric: PageViewsYOY 258 | style: "font-style: italic; text-align:right;" 259 | y_scaling: "##.1%" 260 | - row: 261 | header: "WOW" 262 | metric: PageViewsWOW 263 | style: "font-style: italic; text-align:right;" 264 | y_scaling: "##.1%" 265 | - block: 266 | ui_type: 12_MonthsTable 267 | title: "Page Views Actual vs Plan Summary" 268 | rows: 269 | - row: 270 | header: "Page Views" 271 | style: "font-weight: bold; background-color: LightGrey; text-align:left;" 272 | - row: 273 | header: "Actual" 274 | metric: PageViews 275 | style: "text-align:right;" 276 | y_scaling: "##MM" 277 | - row: 278 | header: "Plan" 279 | metric: PageViews Target 280 | style: "text-align:right;" 281 | y_scaling: "##MM" 282 | - row: 283 | header: "Variance to Plan" 284 | metric: VarianceToPlanPageViews 285 | style: "text-align:right;" 286 | y_scaling: "##MM" 287 | - row: 288 | header: "Variance to Plan (%)" 289 | metric: PercentageVarianceToPlanPageViews 290 | style: "font-style: italic; text-align:right;" 291 | y_scaling: "##.1%" 292 | - row: 293 | header: "YOY" 294 | metric: PageViewsYOY 295 | style: "font-style: italic; text-align:right;" 296 | y_scaling: "##.1%" 297 | - row: 298 | header: "MOM" 299 | metric: PageViewsMOM 300 | style: "font-style: italic; text-align:right;" 301 | y_scaling: "##.1%" 302 | 303 | # This 6_12Graph displays the YOY growth for the metric PageViews. We don't calculate the 304 | # WOW and YOY percentages in the summary table for this type of derived metric, and the 305 | # WOW and YOY percentages are dispayed as 0. 306 | 307 | - block: 308 | ui_type: 6_12Graph 309 | title: Page View YOY Growth Rate 310 | y_scaling: "##.1%" 311 | metrics: 312 | PageViewsYOY: 313 | line_style: primary 314 | graph_prior_year_flag: false 315 | legend_name: PV YOY 316 | -------------------------------------------------------------------------------- /src/unit_test_case/scenario_5/testconfig.yml: -------------------------------------------------------------------------------- 1 | tests: 2 | #Test Case 1 3 | - test: 4 | test_case_no : 1 5 | x_axis_monthly_display: "trailing_twelve_months" 6 | week_ending: 30-APR-2022 7 | fiscal_year_end_month: FEB 8 | metric_name: "CumulativePaidCustomers" 9 | 10 | # 1 11 | cy_6_weeks: [ 30147.42857, 30360.14286, 30446.85714, 30716.14286, 30933, 31028.71429 ] 12 | 13 | # 2 test 14 | py_6_weeks: [ 20104.71429,20187.28571,20316.85714,20465.14286,20568,20752.28571 ] 15 | 16 | # 3 test 17 | cy_monthly: [ 21259.58065, 22207.56667, 23048.48387, 23830.51613,24663.6,25970.58065,26876.06667,27603.12903,28152.70968, 18 | 28959.85714,29950,30754.76667 ] 19 | 20 | # 4 test 21 | py_monthly: [ 16218.22581, 16399.93333, 16615.29032, 16840.25806, 17055.7, 17358.96774, 17815.16667, 22 | 18460.54839, 18998.19355, 19461.64286, 19961.58065, 20484.43333 ] 23 | 24 | # 5 test 25 | x_axis: [ "wk 11", "wk 12", "wk 13", "wk 14", "wk 15", "wk 16","May" , "Jun", "Jul", "Aug","Sep", "Oct", "Nov", "Dec", "Jan", 26 | "Feb", "Mar", "Apr" ] 27 | 28 | # 6 test 29 | box_totals: [ 31028.71429, 0.309424517, 49.51950215, 30754.76667, 50.1372587, 30345.78689, 50.08756695, 30345.78689, 50.08756695 ] 30 | 31 | # 7 test 32 | cy_monthly_data_frame_length: 12 33 | # 8 test 34 | py_monthly_data_frame_length: 22 35 | 36 | - test: 37 | #Test Case 2 38 | test_case_no : 2 39 | x_axis_monthly_display: "trailing_twelve_months" 40 | week_ending: 30-APR-2022 41 | fiscal_year_end_month: FEB 42 | metric_name: "PaidCustomers" 43 | 44 | # 1 test 45 | cy_6_weeks: [ 444, 305, 255, 508,304, 314 ] 46 | 47 | # 2 test 48 | py_6_weeks: [ 131, 222, 272, 182, 227, 331 ] 49 | 50 | # 3 test 51 | cy_monthly: [ 1273, 1248, 1201, 1071, 1543, 1555, 1258, 1336, 975, 1557, 1672, 1447 ] 52 | 53 | # 4 test 54 | py_monthly: [ 520, 487, 457, 657, 413, 715, 684, 990, 665, 799, 905, 1104 ] 55 | 56 | # 5 test 57 | x_axis: [ "wk 11", "wk 12", "wk 13", "wk 14", "wk 15", "wk 16","May" , "Jun", "Jul", "Aug","Sep", "Oct", "Nov", "Dec", "Jan", 58 | "Feb", "Mar", "Apr" ] 59 | 60 | # 6 test 61 | box_totals: [ 314, 3.289473684, -5.135951662, 1447, 31.06884058, 3119, 55.25136884, 3119, 55.25136884 ] 62 | 63 | # 7 test 64 | cy_monthly_data_frame_length: 12 65 | # 8 test 66 | py_monthly_data_frame_length: 22 67 | 68 | - test: 69 | #Test Case 3 70 | test_case_no : 3 71 | x_axis_monthly_display: "trailing_twelve_months" 72 | week_ending: 30-APR-2022 73 | fiscal_year_end_month: FEB 74 | metric_name: "TestMaxMetric" 75 | 76 | # 1 test 77 | cy_6_weeks: [ 98, 87, 92, 96, 93, 98 ] 78 | 79 | # 2 test 80 | py_6_weeks: [ 94, 98, 111, 94, 104, 97 ] 81 | 82 | # 3 test 83 | cy_monthly: [ 106, 104, 103, 109, 105, 103, 99, 110, 105, 95, 103, 98 ] 84 | 85 | # 4 test 86 | py_monthly: [ 103, 101, 103, 105, 105, 99, 96, 98, 115, 101, 98, 111 ] 87 | 88 | # 5 test 89 | x_axis: [ "wk 11", "wk 12", "wk 13", "wk 14", "wk 15", "wk 16","May" , "Jun", "Jul", "Aug","Sep", "Oct", "Nov", "Dec", "Jan", 90 | "Feb", "Mar", "Apr" ] 91 | 92 | # 6 test 93 | box_totals: [ 98, 5.376344086, 1.030927835, 98, -11.71171171, 103, -7.207207207, 103, -7.207207207 ] 94 | 95 | # 7 test 96 | cy_monthly_data_frame_length: 12 97 | # 8 test 98 | py_monthly_data_frame_length: 22 99 | 100 | - test: 101 | #Test Case 4 102 | test_case_no : 4 103 | x_axis_monthly_display: "trailing_twelve_months" 104 | week_ending: 30-APR-2022 105 | fiscal_year_end_month: FEB 106 | metric_name: "TestMinMetric" 107 | 108 | # 1 test 109 | cy_6_weeks: [60, 64, 68, 63, 67, 61] 110 | 111 | # 2 test 112 | py_6_weeks: [67, 69, 61, 67, 52, 64] 113 | 114 | # 3 test 115 | cy_monthly: [60, 53, 50, 58, 49, 55, 51, 58, 55, 54, 60, 61] 116 | 117 | # 4 test 118 | py_monthly: [58, 61, 58, 45, 55, 48, 56, 54, 49, 59, 58, 52] 119 | 120 | # 5 test 121 | x_axis: [ "wk 11", "wk 12", "wk 13", "wk 14", "wk 15", "wk 16","May" , "Jun", "Jul", "Aug","Sep", "Oct", "Nov", "Dec", "Jan", 122 | "Feb", "Mar", "Apr"] 123 | 124 | # 6 test 125 | box_totals: [61, -8.955223881, -4.6875, 61, 17.30769231, 60, 15.38461538, 60, 15.38461538] 126 | 127 | # 7 test 128 | cy_monthly_data_frame_length: 12 129 | # 8 test 130 | py_monthly_data_frame_length: 22 131 | 132 | - test: 133 | #Test Case 5 134 | test_case_no : 5 135 | x_axis_monthly_display: "trailing_twelve_months" 136 | week_ending: 30-APR-2022 137 | fiscal_year_end_month: FEB 138 | metric_name: "TestSumMetrics" 139 | 140 | # 1 test 141 | cy_6_weeks: [201267.255, 85864.74, 75772.485, 268613.655, 81676.305, 108304.5] 142 | 143 | # 2 test 144 | py_6_weeks: [19295.555, 61631.27, 88889.77, 35875.95, 62050.1, 123467.85] 145 | 146 | # 3 test 147 | cy_monthly: [599383.295, 548422.62, 540368, 450080.86, 694597.565, 546607.735, 398184.75, 471717.9, 148 | 303411.315, 709816.89, 646804.11, 541729.125] 149 | 150 | # 4 test 151 | py_monthly: [87050.97, 100455.285, 82165.08, 177660.915, 99726.915, 228500.015, 262013.94, 444731.19, 152 | 280387.77, 298792.975, 285780.23, 347115.81] 153 | 154 | # 5 test 155 | x_axis: [ "wk 11", "wk 12", "wk 13", "wk 14", "wk 15", "wk 16","May" , "Jun", "Jul", "Aug","Sep", "Oct", "Nov", "Dec", "Jan", 156 | "Feb", "Mar", "Apr"] 157 | 158 | # 6 test 159 | box_totals: [108304.5, 32.60210535, -12.28121329, 541729.125, 56.06581705, 1188533.235, 87.79280638, 160 | 1188533.235, 87.79280638] 161 | 162 | # 7 test 163 | cy_monthly_data_frame_length: 12 164 | # 8 test 165 | py_monthly_data_frame_length: 22 166 | 167 | - test: 168 | #Test Case 6 169 | test_case_no : 6 170 | x_axis_monthly_display: "trailing_twelve_months" 171 | week_ending: 30-APR-2022 172 | fiscal_year_end_month: FEB 173 | metric_name: "TestSumMetricColumn" 174 | 175 | # 1 test 176 | cy_6_weeks: [201267.255, 85864.74, 75772.485, 268613.655, 81676.305, 108304.5] 177 | 178 | # 2 test 179 | py_6_weeks: [19295.555, 61631.27, 88889.77, 35875.95, 62050.1, 123467.85] 180 | 181 | # 3 test 182 | cy_monthly: [599383.295, 548422.62, 540368, 450080.86, 694597.565, 546607.735, 398184.75, 471717.9, 183 | 303411.315, 709816.89, 646804.11, 541729.125] 184 | 185 | # 4 test 186 | py_monthly: [87050.97, 100455.285, 82165.08, 177660.915, 99726.915, 228500.015, 262013.94, 444731.19, 187 | 280387.77, 298792.975, 285780.23, 347115.81] 188 | 189 | # 5 test 190 | x_axis: [ "wk 11", "wk 12", "wk 13", "wk 14", "wk 15", "wk 16","May" , "Jun", "Jul", "Aug","Sep", "Oct", "Nov", "Dec", "Jan", 191 | "Feb", "Mar", "Apr"] 192 | 193 | # 6 test 194 | box_totals: [108304.5, 32.60210535, -12.28121329, 541729.125, 56.06581705, 1188533.235, 87.79280638, 195 | 1188533.235, 87.79280638] 196 | 197 | # 7 test 198 | cy_monthly_data_frame_length: 12 199 | # 8 test 200 | py_monthly_data_frame_length: 22 201 | 202 | - test: 203 | #Test Case 7 204 | test_case_no : 7 205 | x_axis_monthly_display: "trailing_twelve_months" 206 | week_ending: 30-APR-2022 207 | fiscal_year_end_month: FEB 208 | metric_name: "TestDifferenceMetric" 209 | 210 | # 1 test 211 | cy_6_weeks: [294, 124, 104, 355, 115, 149] 212 | 213 | # 2 test 214 | py_6_weeks: [31, 122, 171, 82, 124, 224] 215 | 216 | # 3 test 217 | cy_monthly: [893, 870, 837, 689, 1211, 1063, 769, 789, 463, 1013, 941, 733] 218 | 219 | # 4 test 220 | py_monthly: [183, 205, 166, 316, 184, 421, 435, 745, 443, 487, 468, 658] 221 | 222 | # 5 test 223 | x_axis: [ "wk 11", "wk 12", "wk 13", "wk 14", "wk 15", "wk 16","May" , "Jun", "Jul", "Aug","Sep", "Oct", "Nov", "Dec", "Jan", 224 | "Feb", "Mar", "Apr"] 225 | 226 | # 6 test 227 | box_totals: [149, 29.56521739, -33.48214286, 733, 11.39817629, 1674, 48.6678508, 1674, 48.6678508] 228 | 229 | # 7 test 230 | cy_monthly_data_frame_length: 12 231 | # 8 test 232 | py_monthly_data_frame_length: 22 233 | 234 | - test: 235 | #Test Case 8 236 | test_case_no : 8 237 | x_axis_monthly_display: "trailing_twelve_months" 238 | week_ending: 30-APR-2022 239 | fiscal_year_end_month: FEB 240 | metric_name: "TestSumDoubleMetric" 241 | 242 | # 1 test 243 | cy_6_weeks: [1054, 1120, 1081, 1126, 1154, 1098] 244 | 245 | # 2 test 246 | py_6_weeks: [1088, 1130, 1158, 1137, 1072, 1067] 247 | 248 | # 3 test 249 | cy_monthly: [5014, 4621, 4967, 5092, 4615, 4823, 4835, 4901, 5041, 4228, 4969, 4779] 250 | 251 | # 4 test 252 | py_monthly: [4767, 4732, 4773, 4886, 4766, 4744, 4640, 4911, 4563, 4409, 4806, 4721] 253 | 254 | # 5 test 255 | x_axis: [ "wk 11", "wk 12", "wk 13", "wk 14", "wk 15", "wk 16","May" , "Jun", "Jul", "Aug","Sep", "Oct", "Nov", "Dec", "Jan", 256 | "Feb", "Mar", "Apr"] 257 | 258 | # 6 test 259 | box_totals: [1098, -4.852686308, 2.905342081, 4779, 1.228553273, 9748, 2.319722893, 9748, 2.319722893] 260 | 261 | # 7 test 262 | cy_monthly_data_frame_length: 12 263 | # 8 test 264 | py_monthly_data_frame_length: 22 265 | 266 | - test: 267 | #Test Case 9 268 | test_case_no : 9 269 | x_axis_monthly_display: "trailing_twelve_months" 270 | week_ending: 30-APR-2022 271 | fiscal_year_end_month: FEB 272 | metric_name: "TestFilterMetricUS" 273 | 274 | # 1 test 275 | cy_6_weeks: [76.28571429, 80, 77.42857143, 80.57142857, 82.57142857, 78.14285714] 276 | 277 | # 2 test 278 | py_6_weeks: [76.85714286, 80.71428571, 83.4285714, 81.28571429, 77.42857143, 76.71428571] 279 | 280 | # 3 test 281 | cy_monthly: [81, 77, 80.19354839, 82.48387097, 77.1, 78.25806452, 80, 79.09677419, 81.5483871, 75.5, 80.70967742, 282 | 79.7] 283 | 284 | # 4 test 285 | py_monthly: [76.80645161, 79.13333333, 77.12903226, 79.64516129, 79.83333333, 76.70967742, 78.03333333, 286 | 79.51612903, 73.87096774, 78.96428571, 77.38709677, 79.3] 287 | 288 | # 5 test 289 | x_axis: [ "wk 11", "wk 12", "wk 13", "wk 14", "wk 15", "wk 16","May" , "Jun", "Jul", "Aug","Sep", "Oct", "Nov", "Dec", "Jan", 290 | "Feb", "Mar", "Apr"] 291 | 292 | # 6 test 293 | box_totals: [78.14285714, -5.363321799, 1.862197393, 79.7, 0.504413619, 80.21311475, 2.406864797, 80.21311475, 2.406864797] 294 | 295 | # 7 test 296 | cy_monthly_data_frame_length: 12 297 | # 8 test 298 | py_monthly_data_frame_length: 22 299 | 300 | - test: 301 | #Test Case 10 302 | test_case_no : 10 303 | x_axis_monthly_display: "trailing_twelve_months" 304 | week_ending: 30-APR-2022 305 | fiscal_year_end_month: FEB 306 | metric_name: "TestProductMetric" 307 | 308 | # 1 test 309 | cy_6_weeks: [1281928728, 761289243, 887745306, 1535720928, 877035132, 924282576] 310 | 311 | # 2 test 312 | py_6_weeks: [426367260, 926712228, 693976806, 518609000, 626809984, 1175951250] 313 | 314 | # 3 test 315 | cy_monthly: [16831379100, 19267299480, 17064862900, 11083768752, 19077113601, 17467114077, 13789406112, 316 | 16734973140, 14513631924, 21942751773, 20163274560, 19112863572] 317 | 318 | # 4 test 319 | py_monthly: [8041120656, 8224985856, 6491820390, 8848915056, 6270421248, 10463315360, 10831796100, 15778978464, 320 | 10972970610, 10606476958, 12643732292, 14594582340] 321 | 322 | # 5 test 323 | x_axis: [ "wk 11", "wk 12", "wk 13", "wk 14", "wk 15", "wk 16","May" , "Jun", "Jul", "Aug","Sep", "Oct", "Nov", "Dec", "Jan", 324 | "Feb", "Mar", "Apr"] 325 | 326 | # 6 test 327 | box_totals: [924282576, 5.387178036, -21.4, 19112863572, 30.95861962, 78865884828, 44.57646652, 78865884828, 44.57646652] 328 | 329 | # 7 test 330 | cy_monthly_data_frame_length: 12 331 | # 8 test 332 | py_monthly_data_frame_length: 22 333 | 334 | - test: 335 | #Test Case 11 336 | test_case_no : 11 337 | x_axis_monthly_display: "trailing_twelve_months" 338 | week_ending: 30-APR-2022 339 | fiscal_year_end_month: FEB 340 | metric_name: "TestDivisionMetric" 341 | 342 | # 1 test 343 | cy_6_weeks: [0.004975549, 0.005961764, 0.004959461, 0.004981094, 0.00610998, 0.005317655] 344 | 345 | # 2 test 346 | py_6_weeks: [0.004973958, 0.004953613, 0.004971241, 0.004886357, 0.005007779, 0.005156059] 347 | 348 | # 3 test 349 | cy_monthly: [0.017874294, 0.017021225, 0.015792796, 0.016029867, 0.013461133, 0.018944513, 0.018194627, 350 | 0.019816594, 0.018186526, 0.018784623, 0.024407346, 0.023215913] 351 | 352 | # 4 test 353 | py_monthly: [0.020779092, 0.017195192, 0.017513988, 0.020249096, 0.013426596, 0.016936491, 0.013976855, 0.013271545, 354 | 0.011685322, 0.016031535, 0.021892054, 0.021772631] 355 | 356 | # 5 test 357 | x_axis: [ "wk 11", "wk 12", "wk 13", "wk 14", "wk 15", "wk 16","May" , "Jun", "Jul", "Aug","Sep", "Oct", "Nov", "Dec", "Jan", 358 | "Feb", "Mar", "Apr"] 359 | 360 | # 6 test 361 | box_totals: [0.005317655, -12.96771194, 3.134109768, 0.023215913, 6.628885785, 0.047617813, 9.03412084, 0.047617813, 9.03412084] 362 | 363 | # 7 test 364 | cy_monthly_data_frame_length: 12 365 | # 8 test 366 | py_monthly_data_frame_length: 22 367 | 368 | -------------------------------------------------------------------------------- /src/unit_test_case/scenario_4/testconfig.yml: -------------------------------------------------------------------------------- 1 | tests: 2 | #Test Case 1 3 | - test: 4 | test_case_no : 1 5 | x_axis_monthly_display: "trailing_twelve_months" 6 | week_ending: 30-APR-2022 7 | fiscal_year_end_month: '' 8 | metric_name: "CumulativePaidCustomers" 9 | 10 | # 1 11 | cy_6_weeks: [ 30147.42857, 30360.14286, 30446.85714, 30716.14286, 30933, 31028.71429 ] 12 | 13 | # 2 test 14 | py_6_weeks: [ 20104.71429,20187.28571,20316.85714,20465.14286,20568,20752.28571 ] 15 | 16 | # 3 test 17 | cy_monthly: [ 21259.58065, 22207.56667, 23048.48387, 23830.51613,24663.6,25970.58065,26876.06667,27603.12903,28152.70968, 18 | 28959.85714,29950,30754.76667 ] 19 | 20 | # 4 test 21 | py_monthly: [ 16218.22581, 16399.93333, 16615.29032, 16840.25806, 17055.7, 17358.96774, 17815.16667, 22 | 18460.54839, 18998.19355, 19461.64286, 19961.58065, 20484.43333 ] 23 | 24 | # 5 test 25 | x_axis: [ "wk 11", "wk 12", "wk 13", "wk 14", "wk 15", "wk 16","May" , "Jun", "Jul", "Aug","Sep", "Oct", "Nov", "Dec", "Jan", 26 | "Feb", "Mar", "Apr" ] 27 | 28 | # 6 test 29 | box_totals: [ 31028.71429, 0.309424517, 49.51950215, 30754.76667, 50.1372587, 30754.76667, 50.1372587, 29455.85833, 49.31924137 ] 30 | 31 | # 7 test 32 | cy_monthly_data_frame_length: 12 33 | # 8 test 34 | py_monthly_data_frame_length: 20 35 | 36 | - test: 37 | #Test Case 2 38 | test_case_no : 2 39 | x_axis_monthly_display: "trailing_twelve_months" 40 | week_ending: 30-APR-2022 41 | fiscal_year_end_month: '' 42 | metric_name: "PaidCustomers" 43 | 44 | # 1 test 45 | cy_6_weeks: [ 444, 305, 255, 508,304, 314 ] 46 | 47 | # 2 test 48 | py_6_weeks: [ 131, 222, 272, 182, 227, 331 ] 49 | 50 | # 3 test 51 | cy_monthly: [ 1273, 1248, 1201, 1071, 1543, 1555, 1258, 1336, 975, 1557, 1672, 1447 ] 52 | 53 | # 4 test 54 | py_monthly: [ 520, 487, 457, 657, 413, 715, 684, 990, 665, 799, 905, 1104 ] 55 | 56 | # 5 test 57 | x_axis: [ "wk 11", "wk 12", "wk 13", "wk 14", "wk 15", "wk 16","May" , "Jun", "Jul", "Aug","Sep", "Oct", "Nov", "Dec", "Jan", 58 | "Feb", "Mar", "Apr" ] 59 | 60 | # 6 test 61 | box_totals: [ 314, 3.289473684, -5.135951662, 1447, 31.06884058, 1447, 31.06884058, 5651, 62.71235243 ] 62 | 63 | # 7 test 64 | cy_monthly_data_frame_length: 12 65 | # 8 test 66 | py_monthly_data_frame_length: 20 67 | 68 | - test: 69 | #Test Case 3 70 | test_case_no : 3 71 | x_axis_monthly_display: "trailing_twelve_months" 72 | week_ending: 30-APR-2022 73 | fiscal_year_end_month: '' 74 | metric_name: "TestMaxMetric" 75 | 76 | # 1 test 77 | cy_6_weeks: [ 98, 87, 92, 96, 93, 98 ] 78 | 79 | # 2 test 80 | py_6_weeks: [ 94, 98, 111, 94, 104, 97 ] 81 | 82 | # 3 test 83 | cy_monthly: [ 106, 104, 103, 109, 105, 103, 99, 110, 105, 95, 103, 98 ] 84 | 85 | # 4 test 86 | py_monthly: [ 103, 101, 103, 105, 105, 99, 96, 98, 115, 101, 98, 111 ] 87 | 88 | # 5 test 89 | x_axis: [ "wk 11", "wk 12", "wk 13", "wk 14", "wk 15", "wk 16","May" , "Jun", "Jul", "Aug","Sep", "Oct", "Nov", "Dec", "Jan", 90 | "Feb", "Mar", "Apr" ] 91 | 92 | # 6 test 93 | box_totals: [ 98, 5.376344086, 1.030927835, 98, -11.71171171, 98, -11.71171171, 105, -8.695652174 ] 94 | 95 | # 7 test 96 | cy_monthly_data_frame_length: 12 97 | # 8 test 98 | py_monthly_data_frame_length: 20 99 | 100 | - test: 101 | #Test Case 4 102 | test_case_no : 4 103 | x_axis_monthly_display: "trailing_twelve_months" 104 | week_ending: 30-APR-2022 105 | fiscal_year_end_month: '' 106 | metric_name: "TestMinMetric" 107 | 108 | # 1 test 109 | cy_6_weeks: [60, 64, 68, 63, 67, 61] 110 | 111 | # 2 test 112 | py_6_weeks: [67, 69, 61, 67, 52, 64] 113 | 114 | # 3 test 115 | cy_monthly: [60, 53, 50, 58, 49, 55, 51, 58, 55, 54, 60, 61] 116 | 117 | # 4 test 118 | py_monthly: [58, 61, 58, 45, 55, 48, 56, 54, 49, 59, 58, 52] 119 | 120 | # 5 test 121 | x_axis: [ "wk 11", "wk 12", "wk 13", "wk 14", "wk 15", "wk 16","May" , "Jun", "Jul", "Aug","Sep", "Oct", "Nov", "Dec", "Jan", 122 | "Feb", "Mar", "Apr"] 123 | 124 | # 6 test 125 | box_totals: [61, -8.955223881, -4.6875, 61, 17.30769231, 61, 17.30769231, 54, 10.20408163] 126 | 127 | # 7 test 128 | cy_monthly_data_frame_length: 12 129 | # 8 test 130 | py_monthly_data_frame_length: 20 131 | 132 | - test: 133 | #Test Case 5 134 | test_case_no : 5 135 | x_axis_monthly_display: "trailing_twelve_months" 136 | week_ending: 30-APR-2022 137 | fiscal_year_end_month: '' 138 | metric_name: "TestSumMetrics" 139 | 140 | # 1 test 141 | cy_6_weeks: [201267.255, 85864.74, 75772.485, 268613.655, 81676.305, 108304.5] 142 | 143 | # 2 test 144 | py_6_weeks: [19295.555, 61631.27, 88889.77, 35875.95, 62050.1, 123467.85] 145 | 146 | # 3 test 147 | cy_monthly: [599383.295, 548422.62, 540368, 450080.86, 694597.565, 546607.735, 398184.75, 471717.9, 148 | 303411.315, 709816.89, 646804.11, 541729.125] 149 | 150 | # 4 test 151 | py_monthly: [87050.97, 100455.285, 82165.08, 177660.915, 99726.915, 228500.015, 262013.94, 444731.19, 152 | 280387.77, 298792.975, 285780.23, 347115.81] 153 | 154 | # 5 test 155 | x_axis: [ "wk 11", "wk 12", "wk 13", "wk 14", "wk 15", "wk 16","May" , "Jun", "Jul", "Aug","Sep", "Oct", "Nov", "Dec", "Jan", 156 | "Feb", "Mar", "Apr"] 157 | 158 | # 6 test 159 | box_totals: [108304.5, 32.60210535, -12.28121329, 541729.125, 56.06581705, 541729.125, 56.06581705, 160 | 2201761.44, 81.65197678] 161 | 162 | # 7 test 163 | cy_monthly_data_frame_length: 12 164 | # 8 test 165 | py_monthly_data_frame_length: 20 166 | 167 | - test: 168 | #Test Case 6 169 | test_case_no : 6 170 | x_axis_monthly_display: "trailing_twelve_months" 171 | week_ending: 30-APR-2022 172 | fiscal_year_end_month: '' 173 | metric_name: "TestSumMetricColumn" 174 | 175 | # 1 test 176 | cy_6_weeks: [201267.255, 85864.74, 75772.485, 268613.655, 81676.305, 108304.5] 177 | 178 | # 2 test 179 | py_6_weeks: [19295.555, 61631.27, 88889.77, 35875.95, 62050.1, 123467.85] 180 | 181 | # 3 test 182 | cy_monthly: [599383.295, 548422.62, 540368, 450080.86, 694597.565, 546607.735, 398184.75, 471717.9, 183 | 303411.315, 709816.89, 646804.11, 541729.125] 184 | 185 | # 4 test 186 | py_monthly: [87050.97, 100455.285, 82165.08, 177660.915, 99726.915, 228500.015, 262013.94, 444731.19, 187 | 280387.77, 298792.975, 285780.23, 347115.81] 188 | 189 | # 5 test 190 | x_axis: [ "wk 11", "wk 12", "wk 13", "wk 14", "wk 15", "wk 16","May" , "Jun", "Jul", "Aug","Sep", "Oct", "Nov", "Dec", "Jan", 191 | "Feb", "Mar", "Apr"] 192 | 193 | # 6 test 194 | box_totals: [108304.5, 32.60210535, -12.28121329, 541729.125, 56.06581705, 541729.125, 56.06581705, 195 | 2201761.44, 81.65197678] 196 | 197 | # 7 test 198 | cy_monthly_data_frame_length: 12 199 | # 8 test 200 | py_monthly_data_frame_length: 20 201 | 202 | - test: 203 | #Test Case 7 204 | test_case_no : 7 205 | x_axis_monthly_display: "trailing_twelve_months" 206 | week_ending: 30-APR-2022 207 | fiscal_year_end_month: '' 208 | metric_name: "TestDifferenceMetric" 209 | 210 | # 1 test 211 | cy_6_weeks: [294, 124, 104, 355, 115, 149] 212 | 213 | # 2 test 214 | py_6_weeks: [31, 122, 171, 82, 124, 224] 215 | 216 | # 3 test 217 | cy_monthly: [893, 870, 837, 689, 1211, 1063, 769, 789, 463, 1013, 941, 733] 218 | 219 | # 4 test 220 | py_monthly: [183, 205, 166, 316, 184, 421, 435, 745, 443, 487, 468, 658] 221 | 222 | # 5 test 223 | x_axis: [ "wk 11", "wk 12", "wk 13", "wk 14", "wk 15", "wk 16","May" , "Jun", "Jul", "Aug","Sep", "Oct", "Nov", "Dec", "Jan", 224 | "Feb", "Mar", "Apr"] 225 | 226 | # 6 test 227 | box_totals: [149, 29.56521739, -33.48214286, 733, 11.39817629, 733, 11.39817629, 3150, 53.21011673] 228 | 229 | # 7 test 230 | cy_monthly_data_frame_length: 12 231 | # 8 test 232 | py_monthly_data_frame_length: 20 233 | 234 | - test: 235 | #Test Case 8 236 | test_case_no : 8 237 | x_axis_monthly_display: "trailing_twelve_months" 238 | week_ending: 30-APR-2022 239 | fiscal_year_end_month: '' 240 | metric_name: "TestSumDoubleMetric" 241 | 242 | # 1 test 243 | cy_6_weeks: [1054, 1120, 1081, 1126, 1154, 1098] 244 | 245 | # 2 test 246 | py_6_weeks: [1088, 1130, 1158, 1137, 1072, 1067] 247 | 248 | # 3 test 249 | cy_monthly: [5014, 4621, 4967, 5092, 4615, 4823, 4835, 4901, 5041, 4228, 4969, 4779] 250 | 251 | # 4 test 252 | py_monthly: [4767, 4732, 4773, 4886, 4766, 4744, 4640, 4911, 4563, 4409, 4806, 4721] 253 | 254 | # 5 test 255 | x_axis: [ "wk 11", "wk 12", "wk 13", "wk 14", "wk 15", "wk 16","May" , "Jun", "Jul", "Aug","Sep", "Oct", "Nov", "Dec", "Jan", 256 | "Feb", "Mar", "Apr"] 257 | 258 | # 6 test 259 | box_totals: [1098, -4.852686308, 2.905342081, 4779, 1.228553273, 4779, 1.228553273, 19017, 2.80015136] 260 | 261 | # 7 test 262 | cy_monthly_data_frame_length: 12 263 | # 8 test 264 | py_monthly_data_frame_length: 20 265 | 266 | - test: 267 | #Test Case 9 268 | test_case_no : 9 269 | x_axis_monthly_display: "trailing_twelve_months" 270 | week_ending: 30-APR-2022 271 | fiscal_year_end_month: '' 272 | metric_name: "TestFilterMetricUS" 273 | 274 | # 1 test 275 | cy_6_weeks: [76.28571429, 80, 77.42857143, 80.57142857, 82.57142857, 78.14285714] 276 | 277 | # 2 test 278 | py_6_weeks: [76.85714286, 80.71428571, 83.4285714, 81.28571429, 77.42857143, 76.71428571] 279 | 280 | # 3 test 281 | cy_monthly: [81, 77, 80.19354839, 82.48387097, 77.1, 78.25806452, 80, 79.09677419, 81.5483871, 75.5, 80.70967742, 282 | 79.7] 283 | 284 | # 4 test 285 | py_monthly: [76.80645161, 79.13333333, 77.12903226, 79.64516129, 79.83333333, 76.70967742, 78.03333333, 286 | 79.51612903, 73.87096774, 78.96428571, 77.38709677, 79.3] 287 | 288 | # 5 test 289 | x_axis: [ "wk 11", "wk 12", "wk 13", "wk 14", "wk 15", "wk 16","May" , "Jun", "Jul", "Aug","Sep", "Oct", "Nov", "Dec", "Jan", 290 | "Feb", "Mar", "Apr"] 291 | 292 | # 6 test 293 | box_totals: [78.14285714, -5.363321799, 1.862197393, 79.7, 0.504413619, 79.7, 0.504413619, 79.45833333, 2.758917987] 294 | 295 | # 7 test 296 | cy_monthly_data_frame_length: 12 297 | # 8 test 298 | py_monthly_data_frame_length: 20 299 | 300 | - test: 301 | #Test Case 10 302 | test_case_no : 10 303 | x_axis_monthly_display: "trailing_twelve_months" 304 | week_ending: 30-APR-2022 305 | fiscal_year_end_month: '' 306 | metric_name: "TestProductMetric" 307 | 308 | # 1 test 309 | cy_6_weeks: [1281928728, 761289243, 887745306, 1535720928, 877035132, 924282576] 310 | 311 | # 2 test 312 | py_6_weeks: [426367260, 926712228, 693976806, 518609000, 626809984, 1175951250] 313 | 314 | # 3 test 315 | cy_monthly: [16831379100, 19267299480, 17064862900, 11083768752, 19077113601, 17467114077, 13789406112, 316 | 16734973140, 14513631924, 21942751773, 20163274560, 19112863572] 317 | 318 | # 4 test 319 | py_monthly: [8041120656, 8224985856, 6491820390, 8848915056, 6270421248, 10463315360, 10831796100, 15778978464, 320 | 10972970610, 10606476958, 12643732292, 14594582340] 321 | 322 | # 5 test 323 | x_axis: [ "wk 11", "wk 12", "wk 13", "wk 14", "wk 15", "wk 16","May" , "Jun", "Jul", "Aug","Sep", "Oct", "Nov", "Dec", "Jan", 324 | "Feb", "Mar", "Apr"] 325 | 326 | # 6 test 327 | box_totals: [924282576, 5.387178036, -21.4, 19112863572, 30.95861962, 19112863572, 30.95861962, 308106095409, 56.69976088] 328 | 329 | # 7 test 330 | cy_monthly_data_frame_length: 12 331 | # 8 test 332 | py_monthly_data_frame_length: 20 333 | 334 | - test: 335 | #Test Case 11 336 | test_case_no : 11 337 | x_axis_monthly_display: "trailing_twelve_months" 338 | week_ending: 30-APR-2022 339 | fiscal_year_end_month: '' 340 | metric_name: "TestDivisionMetric" 341 | 342 | # 1 test 343 | cy_6_weeks: [0.004975549, 0.005961764, 0.004959461, 0.004981094, 0.00610998, 0.005317655] 344 | 345 | # 2 test 346 | py_6_weeks: [0.004973958, 0.004953613, 0.004971241, 0.004886357, 0.005007779, 0.005156059] 347 | 348 | # 3 test 349 | cy_monthly: [0.017874294, 0.017021225, 0.015792796, 0.016029867, 0.013461133, 0.018944513, 0.018194627, 350 | 0.019816594, 0.018186526, 0.018784623, 0.024407346, 0.023215913] 351 | 352 | # 4 test 353 | py_monthly: [0.020779092, 0.017195192, 0.017513988, 0.020249096, 0.013426596, 0.016936491, 0.013976855, 0.013271545, 354 | 0.011685322, 0.016031535, 0.021892054, 0.021772631] 355 | 356 | # 5 test 357 | x_axis: [ "wk 11", "wk 12", "wk 13", "wk 14", "wk 15", "wk 16","May" , "Jun", "Jul", "Aug","Sep", "Oct", "Nov", "Dec", "Jan", 358 | "Feb", "Mar", "Apr"] 359 | 360 | # 6 test 361 | box_totals: [0.005317655, -12.96771194, 3.134109768, 0.023215913, 6.628885785, 0.023215913, 6.628885785, 0.084906709, 18.20288231] 362 | 363 | # 7 test 364 | cy_monthly_data_frame_length: 12 365 | # 8 test 366 | py_monthly_data_frame_length: 20 367 | 368 | - test: 369 | #Test Case 12 370 | test_case_no : 12 371 | x_axis_monthly_display: "trailing_twelve_months" 372 | week_ending: 30-APR-2022 373 | fiscal_year_end_month: '' 374 | metric_name: "TestProductMetricTwo" 375 | 376 | # 1 test 377 | cy_6_weeks: [4522114.286, 5495185.857, 4597475.429, 4699569.857, 5846337, 5119737.857] 378 | 379 | # 2 test 380 | py_6_weeks: [2010471.429, 2018728.571, 2052002.571, 2046514.286, 2118504, 2220494.571] 381 | 382 | # 3 test 383 | cy_monthly: [8078640.645, 8394460.2 , 8389648.129, 9103257.161, 8188315.2, 12777525.68, 13142396.6, 15098911.58, 14414187.35, 384 | 15754162.29, 21893450,21958903.4] 385 | 386 | # 4 test 387 | py_monthly: [5465542.097, 4624781.2, 4835049.484, 5742528, 3905755.3, 5103536.516, 4435976.5, 4522834.355, 388 | 4217598.968, 6072032.571, 8723210.742, 9136057.267] 389 | 390 | # 5 test 391 | x_axis: [ "wk 11", "wk 12", "wk 13", "wk 14", "wk 15", "wk 16","May" , "Jun", "Jul", "Aug","Sep", "Oct", "Nov", "Dec", "Jan", 392 | "Feb", "Mar", "Apr"] 393 | 394 | # 6 test 395 | box_totals: [5119737.857, -12.42828018, 130.5674566, 21958903.4, 140.3542662, 21958903.4, 140.3542662, 73669101.69, 163.5479341] 396 | 397 | # 7 test 398 | cy_monthly_data_frame_length: 12 399 | # 8 test 400 | py_monthly_data_frame_length: 20 401 | 402 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Table of Contents 2 | - [Overview](#overview) 3 | - [Getting Started](#getting-started) 4 | - [Understanding the WBR Framework](#understanding-the-wbr-framework) 5 | - [Hardware and Software Requirements](#hardware-and-software-requirements) 6 | - [Hardware](#hardware) 7 | - [Software](#software) 8 | - [Set up the WBR App](#set-up-the-wbr-app) 9 | - [Code checkout](#code-checkout) 10 | - [Running the WBR App](#running-the-wbr-app) 11 | - [Using the WBR App](#using-the-wbr-app) 12 | - [Features](#features) 13 | - [Creating the WBR Report](#creating-the-wbr-report) 14 | - [Downloading the JSON file](#downloading-the-json-file) 15 | - [Creating the WBR Report from a JSON file](#creating-the-wbr-report-from-a-json-file) 16 | - [Publishing the WBR Report to a URL](#publishing-the-wbr-report-to-a-url) 17 | - [Generating a WBR config file](#generating-a-wbr-config-file) 18 | - [Generating a WBR config file using AI](#generating-a-wbr-config-file-using-ai) 19 | - [Testing](#testing) 20 | - [Additional Information](#additional-information) 21 | - [Toolchain used for developing WBR](#toolchain-used-for-developing-wbr) 22 | - [License](#license) 23 | - [Helpful Links](#helpful-links) 24 | 25 | 26 | ## Overview 27 | 28 | The WBR App is a web application that takes your organization's business data and builds an HTML-based report 29 | called a WBR Report, so that you can implement an Amazon-style WBR process in your organization. 30 | 31 | Most data visualization applications such as Tableau and Looker cannot generate an Amazon-style WBR Report 32 | out of the box. The WBR App was created to reduce the effort it takes to ingest, transform, and visualize 33 | your business data, so you can focus on improving your business performance each week. In addition to 34 | the HTML WBR Report, the WBR App also produces a JSON version of your WBR Report, so you can quickly import 35 | the transformed data without reprocessing it. 36 | 37 | The WBR Report allows you to quickly review hundreds of metrics and easily spot noteworthy variances 38 | in the data. A well-constructed WBR provides a comprehensive, data-driven answer to the following 39 | three questions each week: 40 | * How did the business do last week? 41 | * Are we on track to hit our targets? 42 | * What did our customers experience last week? 43 | 44 | This document explains how to install an instance of the WBR App by cloning the code repository and running it locally in your own environment. Additionally, you can customize the source code based on your needs and even contribute to the WBR App Git repository. 45 | 46 | 47 | ## Getting Started 48 | 49 | To get started, you'll need to install the necessary software and run the application on your computer. The following steps will guide you through this. 50 | 51 | ### Understanding the WBR Framework 52 | 53 | The WBR App requires the following input files: 54 | * A metrics file in **.csv** format 55 | * A configuration file in **.yaml** format 56 | 57 | The application generates the WBR Report in an HTML page. You can also download a JSON representation of your WBR Report. 58 | 59 | ### Hardware and Software Requirements 60 | #### Hardware 61 | * Processor - 4 Core Processor (Minimum) 62 | * RAM size - 4GB RAM (Minimum) 63 | 64 | #### Software 65 | * Python 3.12.x 66 | * git 2.41.0 67 | 68 | 69 | ## Set up the WBR App 70 | 71 | ### Code checkout 72 | #### Mac / Linux 73 | 1. Open your terminal. 74 | 2. Navigate to the directory where you want to store the project. 75 | 3. To get a copy of the application's code on your computer, run the following command to clone (download) the repository. 76 | ```bash 77 | git clone https://github.com/working-backwards/wbr-app.git 78 | ``` 79 | 4. Navigate into the project directory: 80 | ```bash 81 | cd wbr-app 82 | ``` 83 | Now, you have a local copy of the project on your machine! 84 | 85 | #### Windows 86 | 1. Open command prompt. 87 | 2. Navigate to the directory where you want to store the project. 88 | 3. Run the following command to clone the repository: 89 | ```bash 90 | git clone https://github.com/working-backwards/wbr-app.git 91 | ``` 92 | 4. Navigate into the project directory: 93 | ```bash 94 | cd wbr-app 95 | ``` 96 | Now, you have a local copy of the project on your machine! 97 | 98 | ### Running the WBR App 99 | #### Mac / Linux 100 | 1. **Set Up a Python Virtual Environment** 101 | Create a virtual environment in the `wbr-app` directory by running the following command: 102 | ```bash 103 | python3.12 -m venv "venv" 104 | ``` 105 | 2. **Activate the Virtual Environment** 106 | Activate the virtual environment by running: 107 | ```bash 108 | source venv/bin/activate 109 | ``` 110 | 3. **Install Dependencies** 111 | Once the virtual environment is active, install the required packages: 112 | ```bash 113 | pip install -r requirements.txt 114 | ``` 115 | 4. **Run the Application** 116 | Start the app with the following command: 117 | ```bash 118 | waitress-serve --port=5001 --call src.controller:start 119 | ``` 120 | After successfully running the command, you should see output similar to the following 121 | ``` 122 | INFO:waitress:Serving on http://0.0.0.0:5001 123 | ``` 124 | Note: you might encounter port conflicts, so feel free to change the port. 125 | 126 | Once the command is successful, the application will be running, and you can open your web browser and navigate to `http://localhost:5001/wbr.html` to view the WBR App. 127 | 128 | #### Windows 129 | 1. **Set Up a Python Virtual Environment** 130 | Create a virtual environment in the `wbr-app` directory by running the following command: 131 | ```bash 132 | py -m venv venv 133 | ``` 134 | 2. **Activate the Virtual Environment** 135 | To activate the virtual environment 136 | - Change to the venv\Scripts directory: 137 | ```bash 138 | cd venv\Scripts 139 | ``` 140 | - Type activate and press enter to activate the environment: 141 | ```bash 142 | activate 143 | ``` 144 | 3. **Return to the Project Directory** 145 | Navigate back to the `wbr-app` directory: 146 | ```bash 147 | cd ..\..\ 148 | ``` 149 | 4. **Install Dependencies** 150 | Once the virtual environment is active, install the required packages: 151 | ```bash 152 | pip install -r requirements.txt 153 | ``` 154 | 5. **Run the Application** 155 | Run the app with the following command: 156 | ```bash 157 | waitress-serve --port=5001 --call src.controller:start 158 | ``` 159 | After successfully running the command, you should see output similar to the following 160 | ``` 161 | INFO:waitress:Serving on http://0.0.0.0:5001 162 | ``` 163 | Note: you might encounter port conflicts, so feel free to change the port. 164 | 165 | Once the command is run successful, the application will be running, and you can open your web browser and navigate to `http://localhost:5001/wbr.html` to view the WBR App. 166 | 167 | 168 | ## Using the WBR App 169 | To access the WBR App route your browser to `http[s]:///wbr.html`. 170 | 171 | ### Features 172 | #### Creating the WBR Report 173 | 1. Click on the breadcrumb button which will open a side menu. 174 | 2. Upload the dataset csv file in the Weekly Data input section. 175 | 3. Upload the config yaml file in the Configuration input section. 176 | 4. Click on the `Generate Report` button to generate the WBR report. 177 | 178 | #### Downloading the JSON file 179 | To download the JSON file, follow the below steps, 180 | 181 | 1. When a WBR report is generated a JSON button will be displayed on the application. 182 | 2. Clicking on the button will download the report's JSON file on to your browser 183 | 184 | #### Creating the WBR Report from a JSON file 185 | This helps you create a WBR report on your browser without the data file and/or the config file. To accomplish this please follow the below steps. 186 | 187 | 1. To create a WBR report with a JSON file you should have a valid WBR supported JSON file. 188 | 2. In the side menu when clicked on `Upload JSON and generate report` link a form will be popped which will accept a valid JSON file. 189 | 3. After uploading the JSON file click the `Upload` button to generate a WBR report. 190 | 191 | #### Publishing the WBR Report to a URL 192 | This feature lets you publish a report, you can also publish the report with the password protection, follow the below steps to accomplish this, 193 | 194 | 1. When the report is generated, a `PUBLISH` button is displayed on the application. 195 | 2. When clicked on that button, a form will be popped which will let you publish the report to a URL. 196 | 3. If you want to add the password protection to your report then click on the checkbox and publish the report or else you can just publish the report. 197 | 4. When you click the `PUBLISH` button in the form the report will be persisted, and a URL will be generated for it. 198 | 5. To view the report, copy the published URL, paste it into your web browser, and the report will be displayed. 199 | 200 | NOTE: 201 | If you have published the URL with password protection, the application will ask you to enter the password before you render the report onto your browser. 202 | 203 | **The reports will be saved to the local project directory named `publish`, if you want to save the reports to the cloud storages like s3 or gcp or azure cloud you need to use the following environment variables** 204 | 205 | - **ENVIRONMENT**: 206 | *Optional*. Specifies the environment and saves the files within this particular environment directory. 207 | 208 | - **OBJECT_STORAGE_OPTION**: 209 | *Optional*. Specifies the cloud storage service for saving reports. Supported values are `s3`, `gcp`, `azure`. If not provided, reports will be saved to the local project directory. 210 | 211 | - **OBJECT_STORAGE_BUCKET**: 212 | This is required only if you have set the **OBJECT_STORAGE_OPTION** environment variable. If you are using OBJECT_STORAGE_OPTION as `s3` or `gcp` or `azure` you must first create your own storage bucket. Set OBJECT_STORAGE_BUCKET equal to either S3 bucket name or GCP bucket name or Azure storage container name. 213 | 214 | Depending on your cloud provider, define the following environment variables 215 | 216 | ###### **S3 (Amazon Web Services)** 217 | * **S3_STORAGE_KEY**: Your AWS Access Key ID. 218 | * **S3_STORAGE_SECRET**: Your AWS Secret Access Key. 219 | * **S3_REGION_NAME**: The AWS region where your bucket is hosted. 220 | * **S3_STORAGE_ENDPOINT**: [Optional] Specifies the endpoint where reports will be stored. If not provided, reports will be published to the `OBJECT_STORAGE_BUCKET`. 221 | *Alternatively*, you can use the IAM that have been set up for your local system instead of setting these environment variables. 222 | *NOTE*: You can also use the same environment variables for any S3 compatible storage. 223 | 224 | 225 | ###### **GCP (Google Cloud Platform)** 226 | * **GCP_SERVICE_ACCOUNT_PATH**: Provide the JSON token file path that you downloaded from the Google Cloud. 227 | * **GCLOUD_PROJECT**: If you are using IAM to authenticate then you need to set this environment variable with the project id for the cloud storage. 228 | 229 | ###### **Azure (Microsoft Azure)** 230 | * **AZURE_CONNECTION_STRING**: The connection string from your Azure Storage Account's access keys, while setting this environment variable please encompass the value within double quotes. 231 | * **AZURE_ACCOUNT_URL**: If you are using IAM to authenticate then you need to set this environment variable with the project id for the cloud storage. 232 | 233 | To set the environment variables on your system use the following syntax 234 | ```bash 235 | # Mac / linux: 236 | export VARIABLE_NAME=value 237 | # Windows: 238 | SET VARIABLE_NAME=value 239 | ``` 240 | 241 | After setting the above environment variables you need to rerun the application using the following command 242 | ```bash 243 | waitress-serve --port=5001 --call src.controller:start 244 | ``` 245 | 246 | #### Generating a WBR config file 247 | This feature will help you create a config file, considering all the numeric column to be a metric from your data CSV file and applying `sum` as the default aggregation method. 248 | To accomplish this follow the below steps, 249 | 250 | 1. Click on the `Generate YAML` button which will pop up a form. 251 | 2. Upload the CSV data file 252 | 3. Click on `Download` button, a `wbr_config.yaml` file will be downloaded on to your browser. 253 | 4. You will have to change the `week_ending` and `week_number` fields according to your needs. You can also make changes to other fields according to your needs 254 | 5. You will be able to generate a WBR report using this config file. 255 | 256 | #### Generating a WBR config file using AI 257 | We have a feature where you can install our AI plugin to generate the config file using the same instructions as above. 258 | To install the plugin you will have do the following. 259 | 260 | 1. Clone the repository https://github.com/working-backwards/wbr-ai-yaml-generator. 261 | 2. Follow the instruction to build and install the plugin from the README.md of the same repository. 262 | 3. Once you have completed the installation of the plugin add the environment variables OPENAI_API_KEY and ORGANISATION_ID. 263 | 4. Run the [controller.py](src%2Fcontroller.py) 264 | 265 | After this plugin is installed, the AI yaml generator replaces the default rules-based yaml generator. 266 | 267 | ##### To use this feature you need to set the following environment variables, 268 | - **OPENAI_API_KEY**: Your API key provided by OpenAI. 269 | - **ORGANISATION_ID**: Your organisation ID provided by OpenAI. 270 | 271 | To set the environment variables on your system use the following syntax 272 | ```bash 273 | # Mac / linux: 274 | export VARIABLE_NAME=value 275 | # Windows: 276 | SET VARIABLE_NAME=value 277 | ``` 278 | 279 | After setting the above environment variables you need to rerun the application using the following command 280 | ```bash 281 | waitress-serve --port=5001 --call src.controller:start 282 | ``` 283 | 284 | 285 | ## Testing 286 | Access our automated test suite, which scans test input files from the directory, `src/unit_test_case`. 287 | The test suite iterates through all scenarios from all the `scenario` folders, generates WBR reports, and compares results with the **testconfig.yml** file. 288 | 289 | **Note**: Each scenario folder must contain the **original.csv** (data), **config.yaml** (configuration), and a **testconfig.yml** (expected result) files. 290 | 291 | A web user interface is been developed to run the test cases, route your browser to `http[s]:///unit_test_wbr.html` and click `Run Unit Tests` button. 292 | 293 | 294 | ## Additional Information 295 | * For queries on customizing or building additional WBR metrics, contact [developers@workingbackwards.com](). 296 | 297 | ## API Documentation 298 | For detailed information on how to use the WBR App's API, please refer to the [API Documentation](docs/API_DOCUMENTATION.md). This document provides comprehensive details on the available endpoints, request parameters, and response formats. 299 | 300 | 301 | ## Toolchain used for developing WBR 302 | * [Python 3.12.x compiler](https://www.python.org/) 303 | * [PIP](https://pypi.org/project/pip/) Python Package Management System 304 | * [JetBrains PyCharm Community Edition](https://www.jetbrains.com/pycharm/) IDE 305 | 306 | 307 | ## License 308 | The WBR App is developed and maintained by the Working Backwards engineering team. 309 | The WBR App is released under the MIT License (https://opensource.org/licenses/MIT). 310 | For more information on licensing, refer to the **LICENSE.md** file. 311 | 312 | 313 | ## Helpful Links 314 | [workingbackwards.com](https://workingbackwards.com "Website about Amazon business processes including WBR.") -------------------------------------------------------------------------------- /src/controller.py: -------------------------------------------------------------------------------- 1 | import io 2 | import json 3 | import logging 4 | import os 5 | import tempfile 6 | import uuid 7 | from pathlib import Path 8 | 9 | import flask 10 | import pandas 11 | import requests 12 | from cryptography.fernet import Fernet 13 | from flask import Flask, request, send_file, render_template 14 | from flask_cors import CORS 15 | from werkzeug.utils import redirect 16 | 17 | import src.controller_utility as controller_util 18 | import src.test as test 19 | import src.validator as validator 20 | import src.wbr as wbr 21 | from src.publish_utility import PublishWbr 22 | 23 | app = Flask(__name__, 24 | static_url_path='', 25 | static_folder='web/static', 26 | template_folder='web/templates') 27 | 28 | cors = CORS(app, resources={r"/*": {"origins": "*"}}) 29 | 30 | key = Fernet.generate_key() 31 | 32 | which_env = os.environ.get("ENVIRONMENT") or 'qa' 33 | publisher = PublishWbr(os.getenv("OBJECT_STORAGE_OPTION"), os.environ.get("OBJECT_STORAGE_BUCKET")) 34 | 35 | 36 | @app.route('/get-wbr-metrics', methods=['POST']) 37 | def get_wbr_metrics(): 38 | """ 39 | A flask endpoint, build WBR for given data csv and config yaml file. 40 | :return: A json response for the frontend to render the data 41 | """ 42 | # Get the configuration file and CSV data file from the request 43 | config_file = request.files['configfile'] 44 | csv_data_file = request.files['csvfile'] 45 | 46 | try: 47 | cfg = controller_util.load_yaml_from_stream(config_file) 48 | except Exception as e: 49 | return app.response_class( 50 | response=json.dumps({"description": e.__str__()}), 51 | status=500 52 | ) 53 | 54 | try: 55 | deck = process_input(csv_data_file, cfg) 56 | except Exception as e: 57 | logging.error(e, exc_info=True) 58 | return app.response_class( 59 | response=json.dumps({"description": e.__str__()}), 60 | status=500 61 | ) 62 | 63 | # Return the WBR deck as a JSON response 64 | return app.response_class( 65 | response=json.dumps(deck, indent=4, cls=controller_util.Encoder), 66 | status=200, 67 | mimetype='application/json' 68 | ) 69 | 70 | 71 | def process_input(data, cfg): 72 | try: 73 | wbr_validator = validator.WBRValidator(data, cfg) 74 | wbr_validator.validate_yaml() 75 | except Exception as e: 76 | logging.error("Yaml validation failed", e, exc_info=True) 77 | raise Exception(f"Invalid configuration provided: {e.__str__()}") 78 | 79 | try: 80 | # Create a WBR object using the CSV data and configuration 81 | wbr1 = wbr.WBR(cfg, daily_df=wbr_validator.daily_df) 82 | except Exception as error: 83 | logging.error(error, exc_info=True) 84 | raise Exception(f"Could not create WBR metrics due to: {error.__str__()}") 85 | 86 | try: 87 | # Generate the WBR deck using the WBR object 88 | deck = controller_util.get_wbr_deck(wbr1) 89 | except Exception as err: 90 | logging.error(err, exc_info=True) 91 | raise Exception(f"Error while creating deck, caused by: {err.__str__()}") 92 | 93 | return deck 94 | 95 | 96 | 97 | @app.route('/download_yaml', methods=['POST']) 98 | def download_yaml_for_csv(): 99 | """ 100 | Downloads a YAML file based on the provided CSV file. 101 | 102 | Returns: 103 | The downloaded YAML file as an attachment. 104 | """ 105 | csv_data_file = request.files['csvfile'] 106 | csv_data = pandas.read_csv(csv_data_file, parse_dates=['Date'], thousands=',') 107 | 108 | temp_file = tempfile.NamedTemporaryFile(mode="a", dir='/tmp/') 109 | 110 | try: 111 | from wbryamlgenerator.yaml_generator import generate 112 | csv_data_string: str = csv_data.head(3).to_csv(index=False) 113 | generate(csv_data_string, temp_file) 114 | return send_file(temp_file.name, mimetype='application/x-yaml', as_attachment=True) 115 | except Exception as e: 116 | logging.error(e, exc_info=True) 117 | logging.info("Exception occurred! falling back to the default implementation") 118 | controller_util.generate_custom_yaml(temp_file, csv_data) 119 | return send_file(temp_file.name, mimetype='application/x-yaml', as_attachment=True) 120 | 121 | 122 | @app.route('/publish-wbr-report', methods=['POST']) 123 | def publish_report(url=None, deck=None): 124 | """ 125 | Fetch JSON file from the HTTP request, save the file to S3 bucket and publish the WBR to a public URL. 126 | 127 | Returns: 128 | A Flask response object with the URL to access the uploaded data. 129 | """ 130 | # Parse the JSON data from the request 131 | data = json.loads(deck or request.data) 132 | 133 | # Modify the base URL to use HTTPS instead of HTTP 134 | base_url = url or request.base_url.replace('/publish-wbr-report', '') 135 | if "localhost" not in base_url and "127.0.0.1" not in base_url: 136 | base_url = base_url.replace("http", "https") 137 | 138 | return publish_and_get(base_url, '/build-wbr/publish?file=', data) 139 | 140 | 141 | @app.route("/publish-protected-report", methods=['POST']) 142 | def publish_protected_wbr(url=None, deck=None): 143 | """ 144 | Saves the generated WBR report with a password 145 | :return: Redirect URL for the published report 146 | """ 147 | # Get the password from the request arguments 148 | password = request.args['password'] 149 | 150 | # Load the JSON data from the request body 151 | data = json.loads(deck or request.data) 152 | 153 | # Add the password to the JSON data 154 | protected_data = {"data": data, "password": password} 155 | 156 | # Get the base URL and replace 'http' with 'https' 157 | base_url = url or request.base_url.replace('/publish-protected-report', '') 158 | if "localhost" not in base_url and "127.0.0.1" not in base_url: 159 | base_url = base_url.replace("http", "https") 160 | return publish_and_get(base_url, '/build-wbr/publish/protected?file=', protected_data) 161 | 162 | 163 | def publish_and_get(base_url: str, trailing_url: str, data: list | dict): 164 | # Generate a unique filename for the JSON data 165 | filename = str(uuid.uuid4())[25:] 166 | # Upload the report to cloud storage 167 | try: 168 | publisher.upload(data, which_env + "/" + filename) 169 | # Create a response with the URL to access the uploaded data 170 | return app.response_class( 171 | response=json.dumps({'path': f"{base_url}{trailing_url}{filename}"}, indent=4, 172 | cls=controller_util.Encoder), 173 | status=200 174 | ) 175 | except Exception as e: 176 | logging.error("Error occurred while publishing the report", e, exc_info=True) 177 | return app.response_class( 178 | status=500 179 | ) 180 | 181 | 182 | @app.route('/build-wbr/publish', methods=['GET']) 183 | def build_wbr(): 184 | """ 185 | Builds unprotected WBR onto the web browser using the already saved WBR report 186 | :return: Rendered template of already generated report 187 | """ 188 | filename = request.args['file'] 189 | logging.info(f"Received request to download {filename}") 190 | try: 191 | data = publisher.download(which_env + "/" + filename) 192 | except Exception as e: 193 | logging.error(e, exc_info=True) 194 | return app.response_class( 195 | response=json.dumps({"message": "Failed to download your report!"}), 196 | status=500 197 | ) 198 | return flask.render_template('wbr_share.html', data=data) 199 | 200 | 201 | @app.route('/login', methods=["GET", "POST"]) 202 | def login(): 203 | """ 204 | A callback function when building a protected report onto web browser if successfully verify user redirected to 205 | build-wbr/publish/protected endpoint where protected WBR report will be rendered 206 | """ 207 | # Get the file name from the request arguments 208 | file_name = request.args['file'] 209 | 210 | if 'password' in request.args: 211 | # If password is provided in the request arguments 212 | auth_password = request.args['password'] 213 | try: 214 | # Retrieve the JSON file from S3 bucket 215 | protected_data = publisher.download(which_env + "/" + file_name) 216 | except Exception as e: 217 | # Log any exceptions that occur during file retrieval 218 | logging.error(e, exc_info=True) 219 | return e.__str__() 220 | 221 | if auth_password == protected_data['password']: 222 | # If the provided password matches the password in the JSON file 223 | file_name = request.args['file'] 224 | f = Fernet(key) 225 | # Encrypt the password and generate a token 226 | token = f.encrypt(bytes(auth_password, 'utf-8'))[:15] 227 | return redirect("/build-wbr/publish/protected?file=" + file_name + 228 | "&password=" + str(token)) 229 | else: 230 | # If the provided password does not match the password in the JSON file 231 | return app.response_class( 232 | response=json.dumps({"message": "Unauthorised"}), 233 | status=403 234 | ) 235 | else: 236 | # If password is not provided in the request arguments 237 | return render_template("login.html", fileName=file_name) 238 | 239 | 240 | @app.route('/build-wbr/publish/protected', methods=['GET']) 241 | def build_wbr_protected(): 242 | """ 243 | Builds the protected WBR report, if user is not authenticated to view report user is redirected to login page. 244 | :return: Rendered WBR html file 245 | """ 246 | if 'file' in request.args: 247 | auth_file_name = request.args['file'] 248 | if 'password' not in request.args: 249 | return redirect('/login?file=' + auth_file_name) 250 | else: 251 | protected_data = publisher.download(which_env + "/" + auth_file_name) 252 | return flask.render_template('wbr_share.html', data=protected_data["data"]) 253 | 254 | 255 | @app.route('/build-wbr/sample', methods=['GET']) 256 | def build_sample_wbr(): 257 | """ 258 | Builds sample WBR files. 259 | :return: Rendered sample WBR report html file 260 | """ 261 | filename = request.args['file'] 262 | base_path = str(Path(os.path.dirname(__file__)).parent) 263 | file = base_path + '/sample/' + filename 264 | current_file = open(file) 265 | data = json.load(current_file) 266 | return flask.render_template('wbr_share.html', data=data) 267 | 268 | 269 | @app.route("/get_file_name", methods=['GET']) 270 | def get_file_name(): 271 | """ 272 | Retrieve the sample reference files. 273 | :return: reference files 274 | """ 275 | data_folder = Path(os.path.dirname(__file__)) / 'web/static/demo_uploads' 276 | files = os.listdir(data_folder) 277 | files.sort() 278 | return app.response_class( 279 | response=json.dumps(files, indent=4, cls=controller_util.Encoder), 280 | status=200, 281 | mimetype='application/json' 282 | ) 283 | 284 | 285 | @app.route('/wbr-unit-test', methods=["GET"]) 286 | def run_unit_test(): 287 | """ 288 | Unit test endpoint 289 | :return: Test results 290 | """ 291 | test_result = test.test_wbr() 292 | return app.response_class( 293 | response=json.dumps(test_result, indent=4, cls=controller_util.Encoder), 294 | status=200, 295 | mimetype='application/json' 296 | ) 297 | 298 | 299 | @app.route('/report', methods=["POST"]) 300 | def build_report(): 301 | output_type = request.args["outputType"] if 'outputType' in request.args else None 302 | 303 | # Validate if data file or data file url is present in the request 304 | if 'dataUrl' not in request.args and 'dataFile' not in request.files: 305 | return app.response_class( 306 | response=json.dumps( 307 | {'error': 'Either dataUrl or dataFile required!'}, indent=4, 308 | cls=controller_util.Encoder 309 | ), 310 | status=400 311 | ) 312 | 313 | # Validate if config file or config file url is present in the request 314 | if 'configUrl' not in request.args and 'configFile' not in request.files: 315 | return app.response_class( 316 | response=json.dumps( 317 | {'error': 'Either configUrl or configFile required!'}, indent=4, 318 | cls=controller_util.Encoder 319 | ), 320 | status=400 321 | ) 322 | 323 | # Load config 324 | try: 325 | cfg = controller_util.load_yaml_from_url(request.args["configUrl"]) \ 326 | if 'configUrl' in request.args else controller_util.load_yaml_from_stream(request.files['configFile']) 327 | except Exception as e: 328 | logging.error(e, exc_info=True) 329 | return app.response_class( 330 | response=json.dumps({"error": f"Failed to load yaml, due to {e.__str__()}"}), 331 | status=500 332 | ) 333 | 334 | # Load data 335 | try: 336 | data = request.files['dataFile'] if 'dataFile' in request.files \ 337 | else io.StringIO(requests.get(request.args["dataUrl"]).content.decode('utf-8')) 338 | except Exception as e: 339 | logging.error(e, exc_info=True) 340 | return app.response_class( 341 | response=json.dumps({"error": f"Failed to load the data csv, due to {e.__str__()}"}), 342 | status=500 343 | ) 344 | 345 | # Load events data 346 | try: 347 | events_data = request.files['eventsFile'] if 'eventsFile' in request.files else ( 348 | io.StringIO(requests.get(request.args["eventsFileUrl"]).content.decode('utf-8')) 349 | if "eventsFileUrl" in request.args else None 350 | ) 351 | except Exception as e: 352 | logging.error(e, exc_info=True) 353 | return app.response_class( 354 | response=json.dumps({"error": f"Failed to load the events csv, due to {e.__str__()}"}), 355 | status=500 356 | ) 357 | 358 | # Override the config setup based on the url query parameters 359 | if 'week_ending' in request.args: 360 | cfg["setup"]["week_ending"] = request.args["week_ending"] 361 | if 'week_number' in request.args: 362 | cfg["setup"]["week_number"] = int(request.args["week_number"]) 363 | if 'title' in request.args: 364 | cfg["setup"]["title"] = request.args["title"] 365 | if 'fiscal_year_end_month' in request.args: 366 | cfg["setup"]["fiscal_year_end_month"] = request.args["fiscal_year_end_month"] 367 | if 'block_starting_number' in request.args: 368 | cfg["setup"]["block_starting_number"] = int(request.args["block_starting_number"]) 369 | if 'tooltip' in request.args: 370 | cfg["setup"]["tooltip"] = bool(request.args["tooltip"]) 371 | 372 | try: 373 | deck = process_input(data, cfg, events_data) 374 | except Exception as e: 375 | logging.error(e, exc_info=True) 376 | return app.response_class( 377 | response=json.dumps({"error": e.__str__()}), 378 | status=500 379 | ) 380 | 381 | if output_type == "JSON": 382 | # Return the WBR deck as a JSON response 383 | return app.response_class( 384 | response=json.dumps([deck], indent=4, cls=controller_util.Encoder), 385 | status=200, 386 | mimetype='application/json' 387 | ) 388 | elif output_type == "HTML": 389 | # Return the WBR deck as a JSON response 390 | return flask.render_template( 391 | 'wbr_share.html', 392 | data=json.loads(json.dumps([deck], indent=4, cls=controller_util.Encoder)) 393 | ) 394 | else: 395 | return publish_protected_wbr(request.base_url.replace('/report', ''), 396 | json.dumps([deck], indent=4, cls=controller_util.Encoder)) \ 397 | if "password" in request.args \ 398 | else publish_report(request.base_url.replace('/report', ''), 399 | json.dumps([deck], indent=4, cls=controller_util.Encoder)) 400 | 401 | 402 | def start(): 403 | return app 404 | 405 | 406 | if __name__ == "__main__": 407 | app.run(debug=False, port=5001, host='0.0.0.0') 408 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | **End User License Agreement** 2 | 3 | This End User License Agreement (this " **Agreement**") governs your use of the web-based downloadable software application commonly known as "_Weekly Business Review_" (the " **Software**") including, without limitation, all user manuals, technical manuals and any other materials provided by Licensor, in printed, electronic, or other form, that describe the Software or its use or specifications (collectively, the " **Documentation**") provided to you (" **you**" or " **your**", or " **End User** ," as applicable) by Working Backwards, LLC, a Washington limited liability company (" **Licensor**") for your use pursuant to and subject to this Agreement and subject to the terms and conditions of that certain Master Services Agreement and any Statement of Work thereto, as they may be amended from time to time (collectively, the " **MSA**") by and between Licensor and your employer or other person, business or entity who owns or otherwise lawfully controls the computer on which the Software is installed or through which the Software is accessed (" **Licensee**"). 4 | 5 | LICENSOR PROVIDES THE SOFTWARE SOLELY ON THE TERMS AND CONDITIONS SET FORTH IN THIS AGREEMENT AND ON THE CONDITION THAT LICENSEE ACCEPTS AND COMPLIES WITH THEM. BY DOWNLOADING, USING, OR OTHERWISE ACCESSING THE SOFTWARE (EITHER BY CLONING CODE OR OTHERWISE) YOU: (A) ACCEPT THIS AGREEMENT ON BEHALF LICENSEE AND AGREE THAT LICENSEE IS LEGALLY BOUND BY ITS TERMS; AND (B) REPRESENT AND WARRANT THAT: (I) YOU ARE 18 YEARS OF AGE OR OLDER/OF LEGAL AGE TO ENTER INTO A BINDING AGREEMENT; AND (II) IF LICENSEE IS A CORPORATION, GOVERNMENTAL ORGANIZATION, OR OTHER LEGAL ENTITY, YOU HAVE THE RIGHT, POWER, AND AUTHORITY TO ENTER INTO THIS AGREEMENT ON BEHALF OF LICENSEE AND BIND LICENSEE TO ITS TERMS. IF LICENSEE DOES NOT AGREE TO THE TERMS OF THIS AGREEMENT, LICENSOR WILL NOT AND DOES NOT LICENSE THE SOFTWARE TO LICENSEE AND YOU MUST NOT ACCESS, DOWNLOAD OR INSTALL THE SOFTWARE OR DOCUMENTATION. 6 | 7 | IF YOU DO NOT AGREE TO THIS AGREEMENT, DO NOT DOWNLOAD, USE, OR OTHERWISE ACCESS THE SOFTWARE (EITHER BY CLONING CODE OR OTHERWISE). IF YOU DO NOT ACCEPT THESE TERMS, YOU WILL HAVE NO LICENSE TO, AND MUST NOT ACCESS OR USE, THE SOFTWARE OR DOCUMENTATION. 8 | 9 | **1. License Grant.** Subject to your strict compliance with this Agreement, Licensor hereby grants you a non-exclusive, non-transferable, non-sublicensable, limited license to: (i) use the Software for Licensee's non-commercial internal business purposes only, as it may be: (A) accessed online at: [https://app.workingbackwards.com/wbr.html](https://app.workingbackwards.com/wbr.html%20) (the "**URL**"); or (B) downloaded, copied and installed on the Licensee Network (as defined below) in accordance with the Documentation; (ii) use and make a reasonable number of copies of the Documentation solely for Licensee's internal business purposes in connection with Licensee's use of the Software; and (iii) generate reports ("**Reports**") from the Software using Licensee data or inputs ("**Licensee Data**"), for internal business purposes only (the "**License**"). If the Software is downloaded, copied or installed to Licensee's Network, Licensee shall be permitted to download copies of the Software only on computers owned, leased or controlled by Licensee or its affiliates. The foregoing License will terminate immediately without any notice of any kind requiredon your ceasing to be authorized by Licensee to use the Software for any breach of this Agreement or an applicable MSA. 10 | 11 | **2. Use Restrictions.** You shall not, directly or indirectly, except as expressly permitted by this Agreement: 12 | 13 | (a) use, copy, modify, translate, adapt, or otherwise create derivative works or improvements, whether or not patentable, of the Software or Documentation or any part thereof for any commercial purpose except as expreslly permitted by Licensor; 14 | 15 | (b) combine the Software or any part thereof with, or incorporate the Software or any part thereof in, any other programs for any commercial purpose except as expressly permitted by Licensor; 16 | 17 | (c) reverse engineer, disassemble, decompile, decode, or otherwise attempt to derive or gain access to the source code of the Software or any part thereof for any commercial purposes except as expressly permitted by the License; 18 | 19 | (d) remove, delete, alter, or obscure any trademarks or any copyright, trademark, patent, or other intellectual property or proprietary rights notices included on or in the Software or Documentation, including any copy thereof; 20 | 21 | (e) rent, lease, lend, sell, sublicense, assign, distribute, publish, transfer, or otherwise provide any access to or use of the Software or any features or functionality of the Software, for any reason, to any other person, business or entity, including any subcontractor, independent contractor, affiliate (other than wholly-owned subsidiaries of Licensee), or service provider of Licensee, whether or not over a network and whether or not on a hosted basis, including in connection with the internet, web hosting, wide area network (WAN), virtual private network (VPN), virtualization, time-sharing, service bureau, software as a service, cloud, or other technology or service; 22 | 23 | (f) share or disseminate your password or login, as applicable to users of the URL, to any unauthorized user, or otherwise use the Software or Documentation in, or in association with, the design, construction, maintenance, or operation of any hazardous environments or systems; 24 | 25 | (g) use the Software or Documentation in violation of the MSA, this Agreement, or any law, regulation, order or rule; or 26 | 27 | (h) use the Software or Documentation for purposes of competitive analysis of the Software, the development of a competing software product or service, or any other purpose that is for a commercial purpose or to the Licensor's commercial disadvantage, in each case as determined by Licensor in its sole and exclusive discretion. 28 | 29 | **3. Compliance Measures.** The Software may contain technological copy protection or other security features designed to prevent unauthorized use of the Software, including features to protect against use of the Software: (a) beyond the scope of the License granted to pursuant to Section 1; or (b) prohibited under Section 2. You shall not, and shall not attempt to, remove, disable, circumvent, or otherwise create or implement any workaround to, any such copy protection or security features. 30 | 31 | **4. Collection and Use of Data.** Licensor may, from time to time, receive Licensee Data in connection with the provision of the Software hereunder. You acknowledge and agree that the Licensee Data is used solely to transform Licensee Data in memory and send it back to you for review, and such Licensee Data is not stored permanently by Licensor. You further acknowledge that Licensor does not assume and hereby disclaims any and all liability and responsibility of any kind for any Licensee Data or the transmission thereof, and Licensor shall have no obligation or duty to provide or employ any security measures related thereto. To the fullest extent permitted under applicable law, LICENSOR HAS NO OBLIGATION OR LIABILITY FOR ANY LOSS, ALTERATION, TRANSMISSION, DISSEMINATION, DESTRUCTION, DAMAGE, CORRUPTION, OR RECOVERY OF LICENSEE DATA.Licensee has and will retain sole responsibility for: (a) all Licensee Data, including its content and use; (b) all information, instructions, and materials provided by or on behalf of Licensee or you in connection with the Software; (c) Licensee's information technology infrastructure, including computers, software, databases, electronic systems (including database management systems), and networks, whether operated directly by Licensee or through the use of third-party services ("**Licensee Networks**"); (d) the security and use of Licensee's and your access credentials; and (e) all access to and use of the Software and Documentation directly or indirectly by or through the Licensee Network or its or your access credentials, with or without Licensee's knowledge or consent, including all results obtained from, and all conclusions, decisions, and actions based on, such access or use. 32 | 33 | **5. Intellectual Property Rights.** You acknowledge that the Software is provided under limited license, and not sold, to you or to Licensee. You and Licensee do not acquire any ownership interest in the Software under this Agreement, or any other rights to the Software other than to use the Software in accordance with the limited license granted under this Agreement, subject to all terms, conditions, and restrictions. Licensor reserves and shall retain its entire right, title, and interest in and to the Software and all Intellectual Property Rights arising out of or relating to the Software, subject to the License expressly granted to the Licensee in this Agreement. You shall safeguard all Software (including, without limitation, all copies thereof) from any infringement, misappropriation, theft, misuse, or unauthorized access.You shall promptly notify Licensor if you becomes aware of any infringement of the Licensor's Intellectual Property Rights in the Software and fully cooperate with Licensor in any legal action taken by Licensor to enforce its Intellectual Property Rights. 34 | 35 | " **Intellectual Property Rights**"means any and all registered and unregistered rights granted, applied for, or otherwise now or hereafter in existence under or related to any patent, copyright, trademark, trade secret, database protection, or other intellectual property rights laws, and all similar or equivalent rights or forms of protection, in any part of the world. 36 | 37 | **6. Disclaimer of Liability.** TO THE FULLEST EXTENT PERMITTED UNDER APPLICABLE LAW: 38 | 39 | (a) IN NO EVENT WILL LICENSOR OR ITS AFFILIATES, OR ANY OF ITS OR THEIR RESPECTIVE LICENSORS, MEMBERS, MANAGERS, SUCCESSORS, ASSIGNS OR SERVICE PROVIDERS, BE LIABLE TO LICENSEE, ANY END USER OR ANY THIRD PARTY FOR ANY USE, INTERRUPTION, DELAY, OR INABILITY TO USE THE SOFTWARE; LOST REVENUES OR PROFITS; DELAYS, INTERRUPTION, OR LOSS OF SERVICES, BUSINESS, OR GOODWILL; LOSS OR CORRUPTION OF DATA; LOSS RESULTING FROM SYSTEM OR SYSTEM SERVICE FAILURE, MALFUNCTION, OR SHUTDOWN; LOSS RESULTING FROM ANY DATA TRANSMISSION TO ANY AUTHORIZED OR UNAUTHORIZED THIRD PARTY, FAILURE TO ACCURATELY TRANSFER, READ, STORE, OR TRANSMIT INFORMATION OR LICENSEE DATA; FAILURE TO UPDATE OR PROVIDE CORRECT INFORMATION; SYSTEM INCOMPATIBILITY OR PROVISION OF INCORRECT COMPATIBILITY INFORMATION; OR BREACHES IN SYSTEM SECURITY; OR FOR ANY CONSEQUENTIAL, INCIDENTAL, INDIRECT, EXEMPLARY, SPECIAL, OR PUNITIVE DAMAGES, WHETHER ARISING OUT OF OR IN CONNECTION WITH THIS AGREEMENT, BREACH OF CONTRACT, TORT (INCLUDING NEGLIGENCE), OR OTHERWISE, REGARDLESS OF WHETHER SUCH DAMAGES WERE FORESEEABLE AND WHETHER OR NOT THE LICENSOR WAS ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 40 | (b) IN NO EVENT WILL LICENSOR'S AND ITS AFFILIATES', INCLUDING ANY OF ITS OR THEIR RESPECTIVE LICENSORS', EMPLOYEES', MEMBERS', MANAGERS', SUCCESSORS', ASSIGNS', AND SERVICE PROVIDERS', COLLECTIVE AGGREGATE LIABILITY UNDER OR IN CONNECTION WITH THIS AGREEMENT OR ITS SUBJECT MATTER, UNDER ANY LEGAL OR EQUITABLE THEORY, INCLUDING BREACH OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY, AND OTHERWISE, EXCEED THE TOTAL AMOUNT PAID TO THE LICENSOR BY THE LICENSEE PURSUANT TO THE MSA. 41 | (c) THE LIMITATIONS SET FORTH IN THIS SECTION SHALL APPLY EVEN IF THE LICENSEE'S (OR ANY END USER'S) REMEDIES UNDER THIS AGREEMENT FAIL OF THEIR ESSENTIAL PURPOSE. 42 | 43 | **7. Disclaimer of Warranties.** THE SOFTWARE, ANY REPORT GENERATED THEREFROM AND DOCUMENTATION ARE PROVIDED TO LICENSEE AND ANY END USER THEREOF "AS IS" AND WITH ALL FAULTS AND DEFECTS WITHOUT WARRANTY OF ANY KIND. TO THE MAXIMUM EXTENT PERMITTED UNDER APPLICABLE LAW, LICENSOR, ON ITS OWN BEHALF AND ON BEHALF OF ITS AFFILIATES AND ITS AND THEIR RESPECTIVE LICENSORS, MEMBERS, MANAGERS, AFFILIATES, SUCCESSORS, ASSIGNS AND SERVICE PROVIDERS, EXPRESSLY DISCLAIMS ALL WARRANTIES, WHETHER EXPRESS, IMPLIED, STATUTORY, OR OTHERWISE, WITH RESPECT TO THE SOFTWARE, ANY REPORT GENERATED THEREFROM AND DOCUMENTATION, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, AND NON-INFRINGEMENT, AND WARRANTIES THAT MAY ARISE OUT OF COURSE OF DEALING, COURSE OF PERFORMANCE, USAGE, OR TRADE PRACTICE. WITHOUT LIMITATION TO THE FOREGOING, THE LICENSOR PROVIDES NO WARRANTY OR UNDERTAKING, AND MAKES NO REPRESENTATION OF ANY KIND THAT THE LICENSED SOFTWARE WILL MEET THE LICENSEE'S REQUIREMENTS, ACHIEVE ANY INTENDED RESULTS, BE COMPATIBLE, OR WORK WITH ANY OTHER SOFTWARE, APPLICATIONS, SYSTEMS, OR SERVICES, OPERATE WITHOUT INTERRUPTION, MEET ANY PERFORMANCE OR RELIABILITY STANDARDS OR BE ERROR FREE, BE STORED OR PROTECTED IN ANY WAY, OR THAT ANY ERRORS OR DEFECTS CAN OR WILL BE CORRECTED. 44 | 45 | **8. Export Regulation.** The Software may be subject to US export control laws, including the US Export Administration Act and its associated regulations. You shall not, directly or indirectly, export, re-export, or release the Software to, or make the Software, Reports or Documentation accessible from, any jurisdiction or country to which export, re-export, or release is prohibited by law, rule, or regulation. You shall comply with all applicable laws, regulations, orders and rules, and complete all required undertakings (including obtaining any necessary export license or other governmental approval), prior to exporting, re-exporting, releasing, or otherwise making the Software available outside the US. 46 | 47 | **9. Miscellaneous.** 48 | 49 | (a) All matters arising out of or relating to this Agreement shall be governed by and construed in accordance with the internal laws of the State of Washington without giving effect to any choice or conflict of law provision or rule of the State of Washington. Any legal suit, action, or proceeding arising out of or relating to this Agreement or the transactions contemplated hereby shall be instituted in the federal courts of the United States of America or the courts of the State of Washington in each case located in the City of Seattle and County of King, and each party irrevocably submits to the non-exclusive jurisdiction of such courts in any such legal suit, action, or proceeding. Service of process, summons, notice, or other document by mail to such party's address set forth herein shall be effective service of process for any suit, action, or other proceeding brought in any such court. If any suit or action is instituted by Licensor in connection with any controversy arising out of this Agreement or to enforce any rights hereunder, Licensor, if the substantially prevailing party, shall be entitled to recover, in addition to costs, such sums as the court may find reasonable as attorneys' fees, including litigation expenses and costs, and such similar sums incurred on any appeal. 50 | 51 | (b) Licensor will not be responsible or liable to Licensee or any End User, or deemed in default or breach hereunder by reason of any failure or delay in the performance of its obligations hereunder where such failure or delay is due to strikes, labor disputes, civil disturbances, riot, rebellion, invasion, epidemic, pandemic, hostilities, war, terrorist attack, embargo, natural disaster, acts of God, flood, fire, sabotage, fluctuations or non-availability of electrical power, heat, light, air conditioning, or Licensee equipment, loss and destruction of property, or any other circumstances or causes beyond Licensor's reasonable control. 52 | (c) All notices, requests, consents, claims, demands, waivers, and other communications hereunder shall be in writing and shall be deemed to have been given: (i) when delivered by hand (with written confirmation of receipt); (ii) when received by the addressee if sent by a nationally recognized overnight courier (receipt requested); (iii) on the date sent by or email (with confirmation of transmission) if sent during normal business hours of the recipient, and on the next business day if sent after normal business hours of the recipient; or (iv) on the third day after the date mailed, by certified or registered mail, return receipt requested, postage prepaid. Such communications must be sent to the respective parties at the addresses set forth in the MSA. 53 | 54 | (d) This Agreement may only be amended, modified, or supplemented by an agreement in writing signed by each of Licensee and Licensor. No waiver by any party of any of the provisions hereof shall be effective unless explicitly set forth in writing and signed by the party so waiving. Except as otherwise set forth in this Agreement, no failure to exercise, or delay in exercising, any right, remedy, power, or privilege arising from this Agreement shall operate or be construed as a waiver thereof; nor shall any single or partial exercise of any right, remedy, power, or privilege hereunder preclude any other or further exercise thereof or the exercise of any other right, remedy, power, or privilege. 55 | 56 | (e) If any term or provision of this Agreement is invalid, illegal, or unenforceable in any jurisdiction, such invalidity, illegality, or unenforceability shall not affect any other term or provision of this Agreement or invalidate or render unenforceable such term or provision in any other jurisdiction. 57 | -------------------------------------------------------------------------------- /src/web/static/wbr.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
20 | 29 |
30 | 31 | 32 |
33 |
34 | 35 |

Welcome to the WBR App

36 | What you'll need to build your Weekly Business 37 | Review: 38 |
39 | 40 |
41 |
42 |
43 |
44 |
45 |
46 | 48 | 50 | 51 |
52 |
53 |

Weekly Data

54 |

File containing weekly data converted from a spreadsheet or other 56 | aggregator

57 |
58 |
59 |
60 |

Upload a new one every week.

61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | 70 | 72 | 73 |
74 |
75 |

Configuration

76 |

A markup file that configures how your WBR deck is built

78 |
79 |
80 |
81 |

Update the 82 | week_ending and week_number in your configuration file, then upload

83 |

You can always upload a new when you want to 84 | update a structure of your report

85 |
86 |
87 |
88 |
89 |
90 | 91 |
92 |
93 | 94 |

95 | 98 |
99 | 100 | 101 | 102 | 103 |
104 |
105 |

106 | 108 |
109 |
110 |
111 |
112 |
113 |

Upload

114 |
115 |
116 | 117 | 122 |
123 |
124 |
125 |
126 | 128 | 133 |
134 |
135 |
136 |
137 | 140 |
141 |
142 | Already have a configured report? 143 |
144 | Upload JSON and generate report 146 | > 147 |
148 |
149 |
150 |
151 |
152 | 157 |
158 |
159 |
160 |
161 | 173 |
174 |

Generate a simple YAML config file stub 175 | from a local csv data file and download to your computer

176 | 177 |
178 |
179 |
180 |
181 |
182 | 252 | 253 | 254 | 282 | 283 | 284 | 320 | 321 | 322 | 323 | 324 | 352 | 353 |
354 |
355 |
356 |
@Copyright Working Backwards LLC
357 | 358 | 361 | 364 | 367 | 438 | 439 | 440 | --------------------------------------------------------------------------------