├── .gitignore ├── example-run.png ├── package.json ├── action.yml ├── README.md ├── index.js └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /example-run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fal-ai/dbt-cloud-action/HEAD/example-run.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dbt-cloud-job-action", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "Apache-2.0", 12 | "dependencies": { 13 | "@actions/core": "^1.10.0", 14 | "@actions/github": "^5.1.1", 15 | "axios": "^0.21.2", 16 | "axios-retry": "^3.3.1", 17 | "node-fetch": ">=2.6.7", 18 | "yaml": "^2.1.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: "dbt Cloud action" 2 | description: "Runs a dbt Cloud Job specified by Job ID" 3 | branding: 4 | icon: "cloud" 5 | color: "orange" 6 | inputs: 7 | dbt_cloud_url: 8 | description: dbt Cloud base API URL 9 | required: false 10 | default: https://cloud.getdbt.com 11 | dbt_cloud_token: 12 | description: dbt Cloud API token 13 | required: true 14 | dbt_cloud_account_id: 15 | description: dbt Cloud account ID 16 | required: true 17 | dbt_cloud_job_id: 18 | description: dbt Cloud Job ID 19 | required: true 20 | interval: 21 | description: Interval between polls in seconds 22 | required: false 23 | default: "30" 24 | failure_on_error: 25 | description: Boolean to make the action report a failure when dbt-cloud runs. Mark this as `false` to run fal after the dbt-cloud job. 26 | required: true 27 | get_artifacts: 28 | description: Whether run results, needed by fal, are fetched from dbt cloud. If using this action in other contexts this can be set to `false`, useful for jobs which do not generate artifacts. 29 | required: false 30 | default: true 31 | 32 | cause: 33 | description: Job trigger cause 34 | required: true 35 | default: Triggered by a GitHub Action 36 | 37 | git_sha: 38 | description: The git sha to check out before running this job 39 | required: false 40 | 41 | git_branch: 42 | description: The git branch to check out before running this job 43 | required: false 44 | 45 | schema_override: 46 | description: Override the destination schema in the configured target for this job. 47 | required: false 48 | 49 | dbt_version_override: 50 | description: Override the version of dbt used to run this job 51 | required: false 52 | 53 | threads_override: 54 | description: Override the number of threads used to run this job 55 | required: false 56 | 57 | target_name_override: 58 | description: Override the target.name context variable used when running this job 59 | required: false 60 | 61 | generate_docs_override: 62 | description: Override whether or not this job generates docs (true=yes, false=no) 63 | required: false 64 | # type: boolean 65 | 66 | timeout_seconds_override: 67 | description: Override the timeout in seconds for this job 68 | required: false 69 | # type: integer 70 | 71 | steps_override: 72 | description: Override the list of steps for this job. Pass a yaml list enclosed in a string and it will be parsed and sent as a list. 73 | required: false 74 | # type: array of strings 75 | 76 | wait_for_job: 77 | description: Boolean for whether action should wait until dbt job finishes 78 | required: false 79 | default: true 80 | 81 | 82 | outputs: 83 | git_sha: 84 | description: "Repository SHA in which dbt Cloud Job ran" 85 | run_id: 86 | description: "Run ID of the dbt Cloud Job that was triggered" 87 | runs: 88 | using: node20 89 | main: "index.js" 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dbt Cloud action 2 | 3 | This action lets you trigger a job run on [dbt Cloud](https://cloud.getdbt.com), fetches the `run_results.json` artifact, and `git checkout`s the branch that was ran by dbt Cloud. 4 | 5 | Example usage at [fal-ai/fal_bike_example](https://github.com/fal-ai/fal_bike_example) 6 | 7 | ## Inputs 8 | 9 | ### Credentials 10 | 11 | - `dbt_cloud_url` - dbt Cloud [API URL](https://docs.getdbt.com/dbt-cloud/api-v2#/) (Default: `https://cloud.getdbt.com`) 12 | - `dbt_cloud_token` - dbt Cloud [API token](https://docs.getdbt.com/docs/dbt-cloud/dbt-cloud-api/service-tokens) 13 | - `dbt_cloud_account_id` - dbt Cloud Account ID 14 | - `dbt_cloud_job_id` - dbt Cloud Job ID 15 | 16 | We recommend passing sensitive variables as GitHub secrets. [Example usage](https://github.com/fal-ai/fal_bike_example/blob/main/.github/workflows/fal_dbt.yml). 17 | 18 | ### Action configuration 19 | 20 | - `failure_on_error` - Boolean to make the action report a failure when dbt-cloud runs. Mark this as `false` to run fal after the dbt-cloud job. 21 | - `interval` - The interval between polls in seconds (Default: `30`) 22 | - `get_artifacts` - Whether run results, needed by fal, are fetched from dbt cloud. If using this action in other contexts this can be set to `false`, useful for jobs which do not generate artifacts. 23 | 24 | ### dbt Cloud Job configuration 25 | 26 | Use any of the [documented options for the dbt API](https://docs.getdbt.com/dbt-cloud/api-v2#tag/Jobs/operation/triggerRun). 27 | 28 | - `cause` (Default: `Triggered by a Github Action`) 29 | - `git_sha` 30 | - `git_branch` 31 | - `schema_override` 32 | - `dbt_version_override` 33 | - `threads_override` 34 | - `target_name_override` 35 | - `generate_docs_override` 36 | - `timeout_seconds_override` 37 | - `steps_override`: pass a YAML-parseable string. (e.g. `steps_override: '["dbt seed", "dbt run"]'`) 38 | 39 | ## Create your workflow 40 | ```yaml 41 | name: Run dbt cloud 42 | on: 43 | workflow_dispatch: 44 | 45 | jobs: 46 | deploy: 47 | runs-on: ubuntu-latest 48 | 49 | steps: 50 | - uses: fal-ai/dbt-cloud-action@main 51 | id: dbt_cloud_run 52 | with: 53 | dbt_cloud_token: ${{ secrets.DBT_CLOUD_API_TOKEN }} 54 | dbt_cloud_account_id: ${{ secrets.DBT_CLOUD_ACCOUNT_ID }} 55 | dbt_cloud_job_id: ${{ secrets.DBT_CLOUD_JOB_ID }} 56 | failure_on_error: true 57 | steps_override: | 58 | - dbt seed 59 | - dbt run 60 | ``` 61 | 62 | ### Use with [fal](https://github.com/fal-ai/fal) 63 | 64 | You can trigger a dbt Cloud run and it will download the artifacts to be able to run your `fal run` command easily in GitHub Actions. 65 | 66 | You have to do certain extra steps described here: 67 | 68 | ```yaml 69 | name: Run dbt cloud and fal scripts 70 | on: 71 | workflow_dispatch: 72 | 73 | jobs: 74 | deploy: 75 | runs-on: ubuntu-latest 76 | 77 | steps: 78 | # Checkout before downloading artifacts or setting profiles.yml 79 | - uses: actions/checkout@v3 80 | with: 81 | fetch-depth: 0 82 | 83 | - uses: fal-ai/dbt-cloud-action@main 84 | id: dbt_cloud_run 85 | with: 86 | dbt_cloud_token: ${{ secrets.DBT_CLOUD_API_TOKEN }} 87 | dbt_cloud_account_id: ${{ secrets.DBT_ACCOUNT_ID }} 88 | dbt_cloud_job_id: ${{ secrets.DBT_CLOUD_JOB_ID }} 89 | failure_on_error: false 90 | 91 | - name: Setup profiles.yml 92 | shell: python 93 | env: 94 | contents: ${{ secrets.PROFILES_YML }} 95 | run: | 96 | import yaml 97 | import os 98 | import io 99 | 100 | profiles_string = os.getenv('contents') 101 | profiles_data = yaml.safe_load(profiles_string) 102 | 103 | with io.open('profiles.yml', 'w', encoding='utf8') as outfile: 104 | yaml.dump(profiles_data, outfile, default_flow_style=False, allow_unicode=True) 105 | 106 | - uses: actions/setup-python@v2 107 | with: 108 | python-version: "3.9.x" 109 | 110 | - name: Install dependencies 111 | # Normally would use a `requirements.txt`. 112 | run: | 113 | pip install dbt-bigquery 114 | pip install fal[bigquery] 115 | 116 | - name: Run fal scripts 117 | env: 118 | SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} 119 | SLACK_BOT_CHANNEL: ${{ secrets.SLACK_BOT_CHANNEL }} 120 | run: | 121 | # Move to the same code state of the dbt Cloud Job 122 | git checkout ${{ steps.dbt_cloud_run.outputs.git_sha }} 123 | # TODO: review target in passed profiles.yaml contents 124 | fal run --profiles-dir . 125 | 126 | ``` 127 | 128 | #### Getting the correct artifacts from dbt-cloud 129 | 130 | fal relies on the generated artifacts from a dbt run step to get model statuses. dbt-cloud only makes these artifacts available after the **last** step finished running. 131 | 132 | In order to get the status information that you need for fal, make sure to run the step you are interested in **last**. 133 | 134 | For example, this dbt job will provide the `run_results.json` of `dbt docs generate`, which is probably not what you want fal to report about: 135 | 136 | ![Example run](./example-run.png) 137 | 138 | So, you would make `dbt docs generate` run before `dbt run` and leave `dbt run` as the last step. 139 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const core = require('@actions/core'); 3 | const fs = require('fs'); 4 | const axiosRetry = require('axios-retry'); 5 | const YAML = require('yaml') 6 | 7 | axiosRetry(axios, { 8 | retryDelay: (retryCount) => retryCount * 1000, 9 | retries: 3, 10 | shouldResetTimeout: true, 11 | onRetry: (retryCount, error, requestConfig) => { 12 | console.error("Error in request. Retrying...") 13 | } 14 | }); 15 | 16 | const run_status = { 17 | 1: 'Queued', 18 | 2: 'Starting', 19 | 3: 'Running', 20 | 10: 'Success', 21 | 20: 'Error', 22 | 30: 'Cancelled' 23 | } 24 | 25 | const dbt_cloud_api = axios.create({ 26 | baseURL: `${core.getInput('dbt_cloud_url')}/api/v2/`, 27 | timeout: 5000, // 5 seconds 28 | headers: { 29 | 'Authorization': `Token ${core.getInput('dbt_cloud_token')}`, 30 | 'Content-Type': 'application/json' 31 | } 32 | }); 33 | 34 | function sleep(ms) { 35 | return new Promise((resolve) => { 36 | setTimeout(resolve, ms); 37 | }); 38 | } 39 | 40 | const OPTIONAL_KEYS = [ 41 | 'git_sha', 42 | 'git_branch', 43 | 'schema_override', 44 | 'dbt_version_override', 45 | 'threads_override', 46 | 'target_name_override', 47 | 'generate_docs_override', 48 | 'timeout_seconds_override', 49 | 'steps_override', 50 | ]; 51 | 52 | const BOOL_OPTIONAL_KEYS = [ 'generate_docs_override' ]; 53 | const INTEGER_OPTIONAL_KEYS = [ 'threads_override', 'timeout_seconds_override' ]; 54 | const YAML_PARSE_OPTIONAL_KEYS = [ 'steps_override' ]; 55 | 56 | async function runJob(account_id, job_id) { 57 | const cause = core.getInput('cause'); 58 | 59 | const body = { cause }; 60 | 61 | for (const key of OPTIONAL_KEYS) { 62 | let input = core.getInput(key); 63 | 64 | if (input != '' && BOOL_OPTIONAL_KEYS.includes(key)) { 65 | input = core.getBooleanInput(key); 66 | } else if (input != '' && INTEGER_OPTIONAL_KEYS.includes(key)) { 67 | input = parseInt(input); 68 | } else if (input != '' && YAML_PARSE_OPTIONAL_KEYS.includes(key)) { 69 | core.debug(input); 70 | try { 71 | input = YAML.parse(input); 72 | if (typeof input == 'string') { 73 | input = [ input ]; 74 | } 75 | } catch (e) { 76 | core.setFailed(`Could not interpret ${key} correctly. Pass valid YAML in a string.\n Example:\n property: '["a string", "another string"]'`); 77 | throw e; 78 | } 79 | } 80 | 81 | // Type-checking equality becuase of boolean inputs 82 | if (input !== '') { 83 | body[key] = input; 84 | } 85 | } 86 | 87 | core.debug(`Run job body:\n${JSON.stringify(body, null, 2)}`) 88 | 89 | let res = await dbt_cloud_api.post(`/accounts/${account_id}/jobs/${job_id}/run/`, body) 90 | return res.data; 91 | } 92 | 93 | async function getJobRun(account_id, run_id) { 94 | try { 95 | let res = await dbt_cloud_api.get(`/accounts/${account_id}/runs/${run_id}/?include_related=["run_steps"]`); 96 | return res.data; 97 | } catch (e) { 98 | let errorMsg = e.toString() 99 | if (errorMsg.search("timeout of ") != -1 && errorMsg.search(" exceeded") != -1) { 100 | // Special case for axios timeout 101 | errorMsg += ". The dbt Cloud API is taking too long to respond." 102 | } 103 | 104 | console.error("Error getting job information from dbt Cloud. " + errorMsg); 105 | } 106 | } 107 | 108 | async function getArtifacts(account_id, run_id) { 109 | let res = await dbt_cloud_api.get(`/accounts/${account_id}/runs/${run_id}/artifacts/run_results.json`); 110 | let run_results = res.data; 111 | 112 | core.info('Saving artifacts in target directory') 113 | const dir = './target'; 114 | 115 | if (!fs.existsSync(dir)) { 116 | fs.mkdirSync(dir); 117 | } 118 | 119 | fs.writeFileSync(`${dir}/run_results.json`, JSON.stringify(run_results)); 120 | } 121 | 122 | 123 | async function executeAction() { 124 | const account_id = core.getInput('dbt_cloud_account_id'); 125 | const job_id = core.getInput('dbt_cloud_job_id'); 126 | const failure_on_error = core.getBooleanInput('failure_on_error'); 127 | 128 | const jobRun = await runJob(account_id, job_id); 129 | const runId = jobRun.data.id; 130 | 131 | core.info(`Triggered job. ${jobRun.data.href}`); 132 | 133 | let res; 134 | while (true) { 135 | await sleep(core.getInput('interval') * 1000); 136 | res = await getJobRun(account_id, runId); 137 | 138 | if (!res) { 139 | // Retry if there is no response 140 | continue; 141 | } 142 | 143 | let status = run_status[res.data.status]; 144 | core.info(`Run: ${res.data.id} - ${status}`); 145 | 146 | if (core.getBooleanInput('wait_for_job')) { 147 | if (res.data.is_complete) { 148 | core.info(`job finished with '${status}'`); 149 | break; 150 | } 151 | } else { 152 | core.info("Not waiting for job to finish. Relevant run logs will be omitted.") 153 | break; 154 | } 155 | } 156 | 157 | if (res.data.is_error && failure_on_error) { 158 | core.setFailed(); 159 | } 160 | 161 | if (res.data.is_error) { 162 | // Wait for the step information to load in run 163 | core.info("Loading logs...") 164 | await sleep(5000); 165 | res = await getJobRun(account_id, runId); 166 | // Print logs 167 | for (let step of res.data.run_steps) { 168 | core.info("# " + step.name) 169 | core.info(step.logs) 170 | core.info("\n************\n") 171 | } 172 | } 173 | 174 | if (core.getBooleanInput('get_artifacts')) { 175 | await getArtifacts(account_id, runId); 176 | } 177 | 178 | const outputs = { 179 | "git_sha": res.data['git_sha'], 180 | "run_id": runId 181 | }; 182 | 183 | return outputs; 184 | } 185 | 186 | async function main() { 187 | try { 188 | const outputs = await executeAction(); 189 | const git_sha = outputs["git_sha"]; 190 | const run_id = outputs["run_id"]; 191 | 192 | // GitHub Action output 193 | core.info(`dbt Cloud Job commit SHA is ${git_sha}`) 194 | core.setOutput('git_sha', git_sha); 195 | core.setOutput('run_id', run_id); 196 | } catch (e) { 197 | // Always fail in this case because it is not a dbt error 198 | core.setFailed('There has been a problem with running your dbt cloud job:\n' + e.toString()); 199 | core.debug(e.stack) 200 | } 201 | } 202 | 203 | main(); 204 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2022 fal - Features & Labels, Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------