Package: schemachange
schemachangeLooking for snowchange? You've found the right spot. snowchange has been renamed to schemachange.Overviewschemachange is a simple python based tool to manage all of yourSnowflakeobjects. It follows an Imperative-style approach to Database Change Management (DCM) and was inspired by theFlyway database migration tool. When combined with a version control system and a CI/CD tool, database changes can be approved and deployed through a pipeline using modern software delivery practices. As such schemachange plays a critical role in enabling Database (or Data) DevOps.DCM tools (also known as Database Migration, Schema Change Management, or Schema Migration tools) follow one of two approaches: Declarative or Imperative. For a background on Database DevOps, including a discussion on the differences between the Declarative and Imperative approaches, please read theEmbracing Agile Software Delivery and DevOps with Snowflakeblog post.For the complete list of changes made to schemachange check out theCHANGELOG.Please notethat schemachange is a community-developed tool, not an official Snowflake offering. It comes with no support or warranty.Table of ContentsOverviewProject StructureFolder StructureChange ScriptsVersioned Script NamingRepeatable Script NamingAlways Script NamingScript RequirementsUsing Variables in ScriptsSecrets filteringJinja templating engineGotchasChange History TableAuthenticationPassword AuthenticationPrivate Key AuthenticationOauth AuthenticationExternal Browser AuthenticationOkta AuthenticationConfigurationYAML Config FileYaml Jinja supportCommand Line ArgumentsRunning schemachangePrerequisitesRunning The ScriptGetting Started with schemachangeIntegrating With DevOpsSample DevOps Process FlowUsing in a CI/CD PipelineMaintainersThird Party PackagesLegalProject StructureFolder Structureschemachange expects a directory structure like the following to exist:(project_root)
|
|-- folder_1
    |-- V1.1.1__first_change.sql
    |-- V1.1.2__second_change.sql
    |-- R__sp_add_sales.sql
    |-- R__fn_get_timezone.sql
|-- folder_2
    |-- folder_3
        |-- V1.1.3__third_change.sql
        |-- R__fn_sort_ascii.sqlThe schemachange folder structure is very flexible. Theproject_rootfolder is specified with the-for--root-folderargument. schemachange only pays attention to the filenames, not the paths. Therefore, under theproject_rootfolder you are free to arrange the change scripts any way you see fit. You can have as many subfolders (and nested subfolders) as you would like.Change ScriptsVersioned Script NamingVersioned change scripts follow a similar naming convention to that used byFlyway Versioned Migrations. The script name must follow this pattern (image taken fromFlyway docs):With the following rules for each part of the filename:Prefix: The letter 'V' for versioned changeVersion: A unique version number with dots or underscores separating as many number parts as you likeSeparator: __ (two underscores)Description: An arbitrary description with words separated by underscores or spaces (can not include two underscores)Suffix: .sql or .sql.jinjaFor example, a script name that follows this convention is:V1.1.1__first_change.sql. As with Flyway, the unique version string is very flexible. You just need to be consistent and always use the same convention, like 3 sets of numbers separated by periods. Here are a few valid version strings:1.11_11.2.31_2_3Every script within a database folder must have a unique version number. schemachange will check for duplicate version numbers and throw an error if it finds any. This helps to ensure that developers who are working in parallel don't accidently (re-)use the same version number.Repeatable Script NamingRepeatable change scripts follow a similar naming convention to that used byFlyway Versioned Migrations. The script name must follow this pattern (image taken fromFlyway docs:e.g:R__sp_add_sales.sqlR__fn_get_timezone.sqlR__fn_sort_ascii.sqlAll repeatable change scripts are applied each time the utility is run, if there is a change in the file.
Repeatable scripts could be used for maintaining code that always needs to be applied in its entirety. e.g. stores procedures, functions and view definitions etc.Just like Flyway, within a single migration run, repeatable scripts are always applied after all pending versioned scripts have been executed. Repeatable scripts are applied in alphabetical order of their description.Always Script NamingAlways change scripts are executed with every run of schemachange. This is an addition to the implementation ofFlyway Versioned Migrations.
The script name must following pattern:A__Some_description.sqle.g.A__add_user.sqlA__assign_roles.sqlThis type of change script is useful for an environment set up after cloning. Always scripts are applied always last.Script Requirementsschemachange is designed to be very lightweight and not impose to many limitations. Each change script can have any number of SQL statements within it and must supply the necessary context, like database and schema names. The context can be supplied by using an explicitUSE <DATABASE>command or by naming all objects with a three-part name (<database name>.<schema name>.<object name>). schemachange will simply run the contents of each script against the target Snowflake account, in the correct order.Using Variables in Scriptsschemachange supports the jinja engine for a variable replacement strategy. One important use of variables is to support multiple environments (dev, test, prod) in a single Snowflake account by dynamically changing the database name during deployment. To use a variable in a change script, use this syntax anywhere in the script:{{ variable1 }}.To pass variables to schemachange, check out theConfigurationsection below. You can either use the--varscommand line parameter or the YAML config fileschemachange-config.yml. For the command line version you can pass variables like this:--vars '{"variable1": "value", "variable2": "value2"}'. This parameter accepts a flat JSON object formatted as a string. Nested objects and arrays don't make sense at this point and aren't supported.schemachange will replace any variable placeholders before running your change script code and will throw an error if it finds any variable placeholders that haven't been replaced.Secrets filteringWhile many CI/CD tools already have the capability to filter secrets, it is best that any tool also does not output secrets to the console or logs. Schemachange implements secrets filtering in a number of areas to ensure secrets are not writen to the console or logs. The only exception is therendercommand which will display secrets.A secret is just a standard variable that has been tagged as a secret. This is determined using a naming convention and either of the following will tag a variable as a secret:The variable name has the word secret in it.config-version:1vars:bucket_name:S3://......# not a secretsecret_key:567576D8E# a secretThe variable is a child of a key named secrets.config-version:1vars:secrets:my_key:567576D8E# a secretaws:bucket_name:S3://......# not a secretsecrets:encryption_key:FGDSUUEHDHJK# a secretus_east_1:encryption_key:sdsdsd# a secretJinja templating engineschemachange uses the Jinja templating engine internally and supports:expressions,macros,includesandtemplate inheritance.These files can be stored in the root-folder but schemachange also provides a separate modules folder--modules-folder. This allows common logic to be stored outside of the main changes scripts. Thedemo/citibike_jinjahas a simple example that demonstrates this.The Jinja autoescaping feature is disabled in schemachange, this feature in Jinja is currently designed for where the output language is HTML/XML. So if you are using schemachange with untrusted inputs you will need to handle this within your change scripts.GotchasWithin change scripts:Snowflake Scripting blocks need delimitersThe last line can't be a commentChange History Tableschemachange records all applied changes scripts to the change history table. By default schemachange will attempt to log all activities to theMETADATA.SCHEMACHANGE.CHANGE_HISTORYtable. The name and location of the change history table can be overriden by using the-c(or--change-history-table) parameter. The value passed to the parameter can have a one, two, or three part name (e.g. "TABLE_NAME", or "SCHEMA_NAME.TABLE_NAME", or "DATABASE_NAME.SCHEMA_NAME.TABLE_NAME"). This can be used to support multiple environments (dev, test, prod) or multiple subject areas within the same Snowflake account. By default schemachange will not try to create the change history table, and will fail if the table does not exist.Additionally, if the--create-change-history-tableparameter is given, then schemachange will attempt to create the schema and table associated with the change history table. schemachange will not attempt to create the database for the change history table, so that must be created ahead of time, even when using the--create-change-history-tableparameter.The structure of theCHANGE_HISTORYtable is as follows:Column NameTypeExampleVERSIONVARCHAR1.1.1DESCRIPTIONVARCHARFirst changeSCRIPTVARCHARV1.1.1__first_change.sqlSCRIPT_TYPEVARCHARVCHECKSUMVARCHAR38e5ba03b1a6d2...EXECUTION_TIMENUMBER4STATUSVARCHARSuccessINSTALLED_BYVARCHARSNOWFLAKE_USERINSTALLED_ONTIMESTAMP_LTZ2020-03-17 12:54:33.056 -0700A new row will be added to this table every time a change script has been applied to the database. schemachange will use this table to identify which changes have been applied to the database and will not apply the same version more than once.Here is the current schema DDL for the change history table (found in theschemachange/cli.pyscript), in case you choose to create it manually and not use the--create-change-history-tableparameter:CREATETABLEIFNOTEXISTSSCHEMACHANGE.CHANGE_HISTORY(VERSIONVARCHAR,DESCRIPTIONVARCHAR,SCRIPTVARCHAR,SCRIPT_TYPEVARCHAR,CHECKSUMVARCHAR,EXECUTION_TIMENUMBER,STATUSVARCHAR,INSTALLED_BYVARCHAR,INSTALLED_ONTIMESTAMP_LTZ)AuthenticationSchemachange supports snowflake's default authenticator, External Oauth, Browswer based SSO and Programmatic SSO options supported by theSnowflake Python Connector. Set the environment variableSNOWFLAKE_AUTHENTICATORto one of the followingAuthentication OptionExpected ValueDefaultPasswordAuthenticatorsnowflakeKey PairAuthenticatorsnowflakeExternal OauthoauthBrowser based SSOexternalbrowserProgrammatic SSO(Okta Only)Okta URL endpoing for your Okta account typically in the formhttps://<okta_account_name>.okta.comORhttps://<okta_account_name>.oktapreview.comIf an authenticator is unsupported, then schemachange will default tosnowflake. If the authenticator issnowflake, and both password and key pair values are provided then schemachange will use the password over the key pair values.Password AuthenticationThe Snowflake user password forSNOWFLAKE_USERis required to be set in the environment variableSNOWFLAKE_PASSWORDprior to calling the script. schemachange will fail if theSNOWFLAKE_PASSWORDenvironment variable is not set. The environment variableSNOWFLAKE_AUTHENTICATORwill be set tosnowflakeif it not explicitly set.DEPRECATION NOTICE: TheSNOWSQL_PWDenvironment variable is deprecated but currently still supported. Support for it will be removed in a later version of schemachange. Please useSNOWFLAKE_PASSWORDinstead.Private Key AuthenticationThe Snowflake user encrypted private key forSNOWFLAKE_USERis required to be in a file with the file path set in the environment variableSNOWFLAKE_PRIVATE_KEY_PATH. Additionally, the password for the encrypted private key file is required to be set in the environment variableSNOWFLAKE_PRIVATE_KEY_PASSPHRASE. If the variable is not set, schemachange will assume the private key is not encrypted. These two environment variables must be set prior to calling the script. Schemachange will fail if theSNOWFLAKE_PRIVATE_KEY_PATHis not set.Oauth AuthenticationA Oauth Configuration can be made in theYAML Config Fileor passing an equivalent json dictionary to the switch--oauth-config. Invoke this method by setting the environment variableSNOWFLAKE_AUTHENTICATORto the valueoauthprior to calling schemachange.  Since different Oauth providers may require different information the Oauth configuration uses four named variables that are fed into a POST request to obtain a token. Azure is shown in the example YAML but other providers should use a similar pattern and request payload contents.token-provider-url
The URL of the authenticator resource that will be receive the POST request.token-response-name
The Expected name of the JSON element containing the Token in the return response from the authenticator resource.token-request-payload
The Set of variables passed as a dictionary to thedataelement of the request.token-request-headers
The Set of variables passed as a dictionary to theheaderselement of the request.It is recomended to use the YAML file and pass oauth secrets into the configuration using the templating engine instead of the command line option.External Browser AuthenticationExternal browser authentication can be used for local development by setting the environment variableSNOWFLAKE_AUTHENTICATORto the valueexternalbrowserprior to calling schemachange.
The client will be prompted to authenticate in a browser that pops up. Refer to thedocumentationto cache the token to minimize the number of times the browser pops up to authenticate the user.Okta AuthenticationFor clients that do not have a browser, can use the popular SaaS Idp option to connect via Okta. This will require the Okta URL that you utilize for SSO.
Okta authentication can be used setting the environment variableSNOWFLAKE_AUTHENTICATORto the value of your okta endpoint as a fully formed URL ( E.g.https://<org_name>.okta.com) prior to calling schemachange.** NOTE**: Please disable Okta MFA for the user who uses Native SSO authentication with client drivers. Please consult your Okta administrator for more information.ConfigurationParameters to schemachange can be supplied in two different ways:Through a YAML config fileVia command line argumentsIf supplied by both the command line and the YAML file, The command line overides the YAML values.Additionally, regardless of the approach taken, the following paramaters are required to run schemachange:snowflake-accountsnowflake-usersnowflake-rolesnowflake-warehousePlese seeUsage Notes for the account Parameter (for the connect Method)for more details on how to structure the account name.YAML Config Fileschemachange expects the YAML config file to be namedschemachange-config.ymland looks for it by default in the current folder. The folder can be overridden by using the--config-foldercommand line argument (seeCommand Line Argumentsbelow for more details).Here is the list of available configurations in theschemachange-config.ymlfile:config-version:1# The root folder for the database change scriptsroot-folder:'/path/to/folder'# The modules folder for jinja macros and templates to be used across multiple scripts.modules-folder:null# The name of the snowflake account (e.g. xy12345.east-us-2.azure)snowflake-account:'xy12345.east-us-2.azure'# The name of the snowflake usersnowflake-user:'user'# The name of the default role to use. Can be overrideen in the change scripts.snowflake-role:'role'# The name of the default warehouse to use. Can be overridden in the change scripts.snowflake-warehouse:'warehouse'# The name of the default database to use. Can be overridden in the change scripts.snowflake-database:null# The name of the default schema to use. Can be overridden in the change scripts.snowflake-schema:null# Used to override the default name of the change history table (the default is METADATA.SCHEMACHANGE.CHANGE_HISTORY)change-history-table:null# Define values for the variables to replaced in change scriptsvars:var1:'value1'var2:'value2'secrets:var3:'value3'# This is considered a secret and will not be displayed in any output# Create the change history schema and table, if they do not exist (the default is False)create-change-history-table:false# Enable autocommit feature for DML commands (the default is False)autocommit:false# Display verbose debugging details during execution (the default is False)verbose:false# Run schemachange in dry run mode (the default is False)dry-run:false# A string to include in the QUERY_TAG that is attached to every SQL statement executedquery-tag:'QUERY_TAG'# Information for Oauth token requestsoauthconfig:# url Where token request are posted totoken-provider-url:'https://login.microsoftonline.com/{{env_var('AZURE_ORG_GUID', 'default') }}/oauth2/v2.0/token'# name of Json entity returned by requesttoken-response-name:'access_token'# Headers needed for successful post or other security markings ( multiple labeled items permittedtoken-request-headers:Content-Type:"application/x-www-form-urlencoded"User-Agent:"python/schemachange"# Request Payload for Token (it is recommended passtoken-request-payload:client_id:'{{env_var('CLIENT_ID', 'default') }}'username:'{{env_var('USER_ID', 'default') }}'password:'{{env_var('USER_PASSWORD', 'default') }}'grant_type:'password'scope:'{{env_var('SESSION_SCOPE', 'default') }}'Yaml Jinja supportThe YAML config file supports the jinja templating language and has a custom function "env_var" to access environmental variables.  Jinja variables are unavailible and not yet loaded since they are supplied by the YAML file. Customisation of the YAML file can only happen through values passed via environment variables.env_varProvides access to environmental variables. The function can be used two different ways.Return the value of the environmental variable if it exists, otherwise return the default value.{{env_var('<environmental_variable>','default')}}Return the value of the environmental variable if it exists, otherwise raise an error.{{env_var('<environmental_variable>')}}Command Line ArgumentsSchemachange supports a number of subcommands, it the subcommand is not provided it is defaulted to deploy. This behaviour keeps compatibility with versions prior to 3.2.deployThis is the main command that runs the deployment process.usage: schemachange deploy [-h] [--config-folder CONFIG_FOLDER] [-f ROOT_FOLDER] [-m MODULES_FOLDER] [-a SNOWFLAKE_ACCOUNT] [-u SNOWFLAKE_USER] [-r SNOWFLAKE_ROLE] [-w SNOWFLAKE_WAREHOUSE] [-d SNOWFLAKE_DATABASE] [-s SNOWFLAKE_SCHEMA] [-c CHANGE_HISTORY_TABLE] [--vars VARS] [--create-change-history-table] [-ac] [-v] [--dry-run] [--query-tag QUERY_TAG]ParameterDescription-h, --helpShow the help message and exit--config-folder CONFIG_FOLDERThe folder to look in for the schemachange-config.yml file (the default is the current working directory)-f ROOT_FOLDER, --root-folder ROOT_FOLDERThe root folder for the database change scripts. The default is the current directory.-m MODULES_FOLDER, --modules-folder MODULES_FOLDERThe modules folder for jinja macros and templates to be used across mutliple scripts-a SNOWFLAKE_ACCOUNT, --snowflake-account SNOWFLAKE_ACCOUNTThe name of the snowflake account (e.g. xy12345.east-us-2.azure).-u SNOWFLAKE_USER, --snowflake-user SNOWFLAKE_USERThe name of the snowflake user-r SNOWFLAKE_ROLE, --snowflake-role SNOWFLAKE_ROLEThe name of the role to use-w SNOWFLAKE_WAREHOUSE, --snowflake-warehouse SNOWFLAKE_WAREHOUSEThe name of the default warehouse to use. Can be overridden in the change scripts.-d SNOWFLAKE_DATABASE, --snowflake-database SNOWFLAKE_DATABASEThe name of the default database to use. Can be overridden in the change scripts.-s SNOWFLAKE_SCHEMA, --snowflake-schema SNOWFLAKE_SCHEMAThe name of the default schema to use. Can be overridden in the change scripts.-c CHANGE_HISTORY_TABLE, --change-history-table CHANGE_HISTORY_TABLEUsed to override the default name of the change history table (which is METADATA.SCHEMACHANGE.CHANGE_HISTORY)--vars VARSDefine values for the variables to replaced in change scripts, given in JSON format (e.g. '{"variable1": "value1", "variable2": "value2"}')--create-change-history-tableCreate the change history table if it does not exist. The default is 'False'.-ac, --autocommitEnable autocommit feature for DML commands. The default is 'False'.-v, --verboseDisplay verbose debugging details during execution. The default is 'False'.--dry-runRun schemachange in dry run mode. The default is 'False'.--query-tagA string to include in the QUERY_TAG that is attached to every SQL statement executed.--oauth-configDefine values for the variables to Make Oauth Token requests  (e.g. {"token-provider-url": "https//...", "token-request-payload": {"client_id": "GUID_xyz",...},... })'renderThis subcommand is used to render a single script to the console. It is intended to support the development and troubleshooting of script that use features from the jinja template engine.usage: schemachange render [-h] [--config-folder CONFIG_FOLDER] [-f ROOT_FOLDER] [-m MODULES_FOLDER] [--vars VARS] [-v] scriptParameterDescription--config-folder CONFIG_FOLDERThe folder to look in for the schemachange-config.yml file (the default is the current working directory)-f ROOT_FOLDER, --root-folder ROOT_FOLDERThe root folder for the database change scripts-m MODULES_FOLDER, --modules-folder MODULES_FOLDERThe modules folder for jinja macros and templates to be used across multiple scripts--vars VARSDefine values for the variables to replaced in change scripts, given in JSON format (e.g. {"variable1": "value1", "variable2": "value2"})-v, --verboseDisplay verbose debugging details during execution (the default is False)Running schemachangePrerequisitesIn order to run schemachange you must have the following:You will need to have a recent version of python 3 installedYou will need to have the latestSnowflake Python driver installedYou will need to create the change history table used by schemachange in Snowflake (seeChange History Tableabove for more details)First, you will need to create a database to store your change history table (schemachange will not help you with this)Second, you will need to create the change history schema and table. You can do this manually (seeChange History Tableabove for the DDL) or have schemachange create them by running it with the--create-change-history-tableparameter (just make sure the Snowflake user you're running schemachange with has privileges to create a schema and table in that database)You will need to create (or choose) a user account that has privileges to apply the changes in your change scriptDon't forget that this user also needs the SELECT and INSERT privileges on the change history tableRunning the Scriptschemachange is a single python script located atschemachange/cli.py. It can be executed as follows:python schemachange/cli.py [-h] [--config-folder CONFIG_FOLDER] [-f ROOT_FOLDER] [-a SNOWFLAKE_ACCOUNT] [-u SNOWFLAKE_USER] [-r SNOWFLAKE_ROLE] [-w SNOWFLAKE_WAREHOUSE] [-d SNOWFLAKE_DATABASE] [-s SNOWFLAKE_SCHEMA] [-c CHANGE_HISTORY_TABLE] [--vars VARS] [--create-change-history-table] [-ac] [-v] [--dry-run] [--query-tag QUERY_TAG] [--oauth-config OUATH_CONFIG]Or if installed viapip, it can be executed as follows:schemachange [-h] [--config-folder CONFIG_FOLDER] [-f ROOT_FOLDER] [-a SNOWFLAKE_ACCOUNT] [-u SNOWFLAKE_USER] [-r SNOWFLAKE_ROLE] [-w SNOWFLAKE_WAREHOUSE] [-d SNOWFLAKE_DATABASE] [-s SNOWFLAKE_SCHEMA] [-c CHANGE_HISTORY_TABLE] [--vars VARS] [--create-change-history-table] [-ac] [-v] [--dry-run] [--query-tag QUERY_TAG] [--oauth-config OUATH_CONFIG]Getting Started with schemachangeThedemofolder in this project repository contains a schemachange demo project for you to try out. This demo is based on the standard Snowflake Citibike demo which can be found inthe Snowflake Hands-on Lab. It contains the following database change scripts:Change ScriptDescriptionv1.1__initial_database_objects.sqlCreate the initial Citibike demo objects including file formats, stages, and tables.v1.2__load_tables_from_s3.sqlLoad the Citibike and weather data from the Snowlake lab S3 bucket.TheCitibike datafor this demo comes from the NYC Citi Bike bike share program.To get started with schemachange and these demo Citibike scripts follow these steps:Make sure you've completed thePrerequisitessteps aboveGet a copy of this schemachange repository (either via a clone or download)Open a shell and change directory to your copy of the schemachange repositoryRun schemachange (seeRunning the Scriptabove) with your Snowflake account details and thedemo/citibikefolder as the root folder (make sure you use the full path)Integrating With DevOpsSample DevOps Process FlowHere is a sample DevOps development lifecycle with schemachange:Using in a CI/CD PipelineIf your build agent has a recent version of python 3 installed, the script can be ran like so:pip install schemachange --upgrade
schemachange [-h] [-f ROOT_FOLDER] -a SNOWFLAKE_ACCOUNT -u SNOWFLAKE_USER -r SNOWFLAKE_ROLE -w SNOWFLAKE_WAREHOUSE [-d SNOWFLAKE_DATABASE] [-s SNOWFLAKE_SCHEMA] [-c CHANGE_HISTORY_TABLE] [--vars VARS] [--create-change-history-table] [-ac] [-v] [--dry-run] [--query-tag QUERY_TAG] [--oauth-config OUATH_CONFIG]Or if you prefer docker, set the environment variables and run like so:docker run -it --rm \
  --name schemachange-script \
  -v "$PWD":/usr/src/schemachange \
  -w /usr/src/schemachange \
  -e ROOT_FOLDER \
  -e SNOWFLAKE_ACCOUNT \
  -e SNOWFLAKE_USER \
  -e SNOWFLAKE_ROLE \
  -e SNOWFLAKE_WAREHOUSE \
  -e SNOWFLAKE_PASSWORD \
  python:3 /bin/bash -c "pip install schemachange --upgrade && schemachange -f $ROOT_FOLDER -a $SNOWFLAKE_ACCOUNT -u $SNOWFLAKE_USER -r $SNOWFLAKE_ROLE -w $SNOWFLAKE_WAREHOUSE"Either way, don't forget to set theSNOWFLAKE_PASSWORDenvironment variable if using password authentication!MaintainersJames Weakley (@jamesweakley)Jeremiah Hansen (@jeremiahhansen)This is a community-developed tool, not an official Snowflake offering. It comes with no support or warranty. However, feel free to raise a github issue if you find a bug or would like a new feature.Third Party PackagesThe current functionality in schemachange would not be possible without the following third party packages and all those that maintain and have contributed.NameLicenseAuthorURLJinja2BSD LicenseArmin Ronacherhttps://palletsprojects.com/p/jinja/PyYAMLMIT LicenseKirill Simonovhttps://pyyaml.org/pandasBSD LicenseThe Pandas Development Teamhttps://pandas.pydata.orgpytestMIT LicenseHolger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, Brianna Laugher, Florian Bruhin and othershttps://docs.pytest.org/en/latest/snowflake-connector-pythonApache Software LicenseSnowflake, Inchttps://www.snowflake.com/LegalLicensed under the Apache License, Version 2.0 (the "License"); you may not use this tool except in compliance with the License. You may obtain a copy of the License at:http://www.apache.org/licenses/LICENSE-2.0Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.