diff --git a/.gitignore b/.gitignore index 4570d7f..0d8d9ff 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,5 @@ out/ output/ tmp/ results/ -report/ !modules/_example_module/** diff --git a/docs/development_guide/getting_started.md b/docs/development_guide/getting_started.md index dabe766..4382e6f 100644 --- a/docs/development_guide/getting_started.md +++ b/docs/development_guide/getting_started.md @@ -17,5 +17,61 @@ If you don't, We recommend following `mamba`'s [installation advice](https://git conda activate ec_modules ``` -3. You are ready to go! +3. Create your module or wrapper using one of our standard [`copier`](https://github.com/copier-org/copier) templates. + + ??? example "Example: using a module template" + + With the `ec_modules` environment activated, type: + + ```shell + copier copy modules/_template/ modules/ + ``` + + You'll be prompted with some questions. After answering them, `copier` will auto-generate the module for you! + + ```html + 🎀 What is your module's name? + wind_offshore + 🎀 Please give a brief sentence describing your module. + A module to estimate offshore-wind potentials for arbitrary subregions in Europe. + 🎀 We auto-generate an MIT license for you. Please provide your full name. + E. Dantès + 🎀 We auto-generate an MIT license for you. Please provide the name of your institution. + Morrel Technical Institute + 🎀 We auto-generate an MIT license for you. Please provide an email address. + e.dantes@morrel-ti.edu + 🎀 We auto-generate an MIT license for you. What year is this? + 1815 + ``` + + ??? example "Example: using a wrapper template" + + Similar to the module example, call `copier` with the following: + + ```shell + copier copy wrappers/_template/ wrappers/ + ``` + + In the case of wrappers, there are some additional answers you must provide. + + ```html + 🎀 What is the name of the tool you are designing a wrapper for? + gregor + 🎀 Please provide a valid link to the tool's official website. + https://github.com/jnnr/gregor + 🎀 What is the name of the wrapper? + snip + 🎀 Please give a brief sentence describing your wrapper. + Snip a raster file into a smaller raster file. + 🎀 We auto-generate an MIT license for you. Please provide your full name. + G. Samsa + 🎀 We auto-generate an MIT license for you. Please provide the name of your institution. + Bekannt University + 🎀 We auto-generate an MIT license for you. Please provide an email address. + g.s@un.bekannt.edu + 🎀 We auto-generate an MIT license for you. What year is this? + 1915 + ``` + +You are ready to go! Please look into our [code conventions](conventions.md#code-conventions) and our requirements for developing [modules](modules.md) and [wrappers](wrappers.md) for more details. diff --git a/modules/_template/README.md b/modules/_template/README.md deleted file mode 100644 index 9a7e441..0000000 --- a/modules/_template/README.md +++ /dev/null @@ -1,55 +0,0 @@ -# Module template - - - - - -This module is a simple example of how you can build exportable and reproductible workflows in `snakemake`. - -## Input-Output - - - -Here is a brief summary of the IO structure of the module. -It just downloads a file with region shapes from a remote repository and also creates a text file with a greeting! - -```mermaid -flowchart LR - I1(shapefile.geojson) -.-> |Download| C - C(config.yaml) -->M((_template)) - M --> O1(downloaded_shapefile.csv) - M --> O2(hello-world.txt) -``` - -## DAG - -Here is a brief overview of the module's steps. -Please consult the code for more details. - -![dag](rulegraph.png) diff --git a/modules/_template/config/README.md b/modules/_template/config/README.md deleted file mode 100644 index 90ae39d..0000000 --- a/modules/_template/config/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# Module configuration - -User-modifiable module settings should go in `default.yaml`. - -You should also provide a brief explanation of each setting below. - -## Example - -- shapefile: `.geojson` file containing at least a `geometry` column, a column with the country code in ISO 3166-1 alpha 3 standard, and a column with unique subregion IDs. - - path: URL to the shape, local path, or relative path (must be relative to the main workflow). - - download: if `True`, the module will use the `path` to download the shapefile. - - country-id-column: name of the column containing ISO country codes in alpha-3 format. - - subregion-id-column: name of the column containing unique subregion identifiers. -- year-slice: years to process, in the form [start_year, end_year]. Inclusive (e.g., [2015, 2017] will include 2015, 2016, 2017). diff --git a/modules/_template/config/default.yaml b/modules/_template/config/default.yaml deleted file mode 100644 index 6b95c71..0000000 --- a/modules/_template/config/default.yaml +++ /dev/null @@ -1,7 +0,0 @@ -# User modifiable configuration -shapefile: - path: https://www.dropbox.com/scl/fi/sjjisuqnzck2mhxs9jca7/units-national.geojson?rlkey=rv0q1exsytxws3gfvankbt9p7&e=1&dl=0 - download: true - country-id-column: id - subregion-id-column: id # Shapes at national level -year-slice: [2016, 2016] diff --git a/modules/_template/copier.yaml b/modules/_template/copier.yaml new file mode 100644 index 0000000..6a3f51b --- /dev/null +++ b/modules/_template/copier.yaml @@ -0,0 +1,32 @@ +module_name: + type: str + help: What is your module's name? + placeholder: transport_road + validator: >- + {% if not (module_name | regex_search('^[a-z][a-z0-9_]+$')) %} + "Must be a single word in lowercase. Letters, digits and underscores are valid characters." + {% endif %} +module_description: + type: str + help: Please give a brief sentence describing your module. + placeholder: A module to estimate energy demand of road vehicles at a subnational level. +author_name: + type: str + help: We auto-generate an MIT license for you. Please provide your full name. + placeholder: Laura Patricia Orellana +author_institution: + type: str + help: We auto-generate an MIT license for you. Please provide the name of your institution. + placeholder: TU Delft +author_email: + type: str + help: We auto-generate an MIT license for you. Please provide an email address. + placeholder: yourname@university.edu + validator: >- + {% if not (author_email | regex_search('^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$')) %} + "This email does not seem valid... did you forget to add a dot?" + {% endif %} +year: + type: str + help: We auto-generate an MIT license for you. What year is this? + placeholder: "2024" diff --git a/modules/_template/rulegraph.png b/modules/_template/rulegraph.png deleted file mode 100644 index 0c0e675..0000000 Binary files a/modules/_template/rulegraph.png and /dev/null differ diff --git a/modules/_template/workflow/Snakefile b/modules/_template/workflow/Snakefile deleted file mode 100644 index 2aa91cd..0000000 --- a/modules/_template/workflow/Snakefile +++ /dev/null @@ -1,23 +0,0 @@ -import yaml - -from pathlib import Path -from snakemake.utils import min_version, validate - -# Limit the snakemake version to a modern one. -min_version("8.10") - -configfile: "config/default.yaml" -validate(config, workflow.source_path("schemas/config.schema.yaml")) - -# We load internal settings separately so users cannot modify them. -with open(workflow.source_path("resources/internal.yaml"), "r") as f: - internal = yaml.safe_load(f) - -include: "rules/example.smk" - -rule all: - message: "Welcome to easy and collaborative energy modelling!" - default_target: True - input: - rules.download_shapefile.output, - rules.hello_world.output diff --git a/modules/_template/workflow/envs/README.md b/modules/_template/workflow/envs/README.md deleted file mode 100644 index cdb695f..0000000 --- a/modules/_template/workflow/envs/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Module environments - -`rule:` commands should always use a `conda` environment. -Please try to keep your dependencies short, and always pin the version you used! diff --git a/modules/_template/workflow/profiles/README.md b/modules/_template/workflow/profiles/README.md deleted file mode 100644 index e0e0159..0000000 --- a/modules/_template/workflow/profiles/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Profiles - ->[!WARNING] ->Do not modify the files in this folder - -`default/config.yaml` is a `snakemake` [profile](https://snakemake.readthedocs.io/en/v8.18.0/executing/cli.html) that enables us to link `ec_modules` together. -Any workflow that wishes to use our modules and wrappers should have a similar file. -It ensures two things: - -- That the workflow is always ran using `conda`. -- That wrappers point to the [`ec_modules`](https://github.com/calliope-project/ec_modules) repository. diff --git a/modules/_template/workflow/resources/README.md b/modules/_template/workflow/resources/README.md deleted file mode 100644 index 14e102e..0000000 --- a/modules/_template/workflow/resources/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Resources - -You can place small files that your workflow needs here. - -The most important of these is `internal.yaml`, where you can define settings that users cannot modify (i.e., urls for datasets you download, hard-coded parameters, etc). diff --git a/modules/_template/workflow/resources/internal.yaml b/modules/_template/workflow/resources/internal.yaml deleted file mode 100644 index 64711af..0000000 --- a/modules/_template/workflow/resources/internal.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# This file contains settings that users cannot modify. -# Use it for: -# - stuff that should not change (e.g., the URL of a raster the module needs to download) -# - data used for checks (e.g., a list of valid countries, if your module is not global) -valid-country-id: -- TZA -- MEX -- DEU -- BGD diff --git a/modules/_template/workflow/rules/README.md b/modules/_template/workflow/rules/README.md deleted file mode 100644 index 3e8e331..0000000 --- a/modules/_template/workflow/rules/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Rules - -All your rule files should be placed here in the form `file_name.smk`. diff --git a/modules/_template/workflow/rules/example.smk b/modules/_template/workflow/rules/example.smk deleted file mode 100644 index d414144..0000000 --- a/modules/_template/workflow/rules/example.smk +++ /dev/null @@ -1,16 +0,0 @@ - -if config["shapefile"]["download"]: - rule download_shapefile: - message: "Downloading the configured shapefile." - params: - url = config["shapefile"]["path"], - output: "results/downloads/shapes.geojson" - conda: "../envs/shell.yaml" - shell: "curl -sSLo {output} '{params.url}'" - - -rule hello_world: - message: "I am a module and that's OK!" - output: "results/hello.txt" - conda: "../envs/shell.yaml" - script: "../scripts/example.py" diff --git a/modules/_template/workflow/schemas/README.md b/modules/_template/workflow/schemas/README.md deleted file mode 100644 index a3a1190..0000000 --- a/modules/_template/workflow/schemas/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Schemas - -Optionally, you may want to validate user configurations through a [JSON schema](https://json-schema.org/understanding-json-schema/reference/array). - -This will enable you to automatically reject wrong module configurations before they reach rules or code. diff --git a/modules/_template/workflow/schemas/config.schema.yaml b/modules/_template/workflow/schemas/config.schema.yaml deleted file mode 100644 index 2316d03..0000000 --- a/modules/_template/workflow/schemas/config.schema.yaml +++ /dev/null @@ -1,26 +0,0 @@ -$schema: https://json-schema.org/draft/2020-12/schema -type: object -additionalProperties: false -properties: - shapefile: - type: object - additionalProperties: false - description: Settings related to shapes. - properties: - path: - type: string - description: url, full path or relative path to the shapefile. - download: - type: boolean - description: If True, the module will attempt to download the shapefile from the url in 'path'. - country-id-column: - type: string - description: name of the column containing ISO country codes in alpha-3 format. - subregion-id-column: - type: string - description: name of the column containing unique subregion IDs. - year-slice: - type: array - description: range of years to process. - items: - type: number diff --git a/modules/_template/workflow/scripts/README.md b/modules/_template/workflow/scripts/README.md deleted file mode 100644 index d5c34a8..0000000 --- a/modules/_template/workflow/scripts/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Scripts - -Python, R and similar scripts should be placed here. -Keep in mind that `snakemake` is not meant to be a development environment, making scripts hard to debug. -If your scripts are getting long (>100 lines), consider building a python module with wrappers instead. diff --git a/modules/_template/workflow/scripts/example.py b/modules/_template/workflow/scripts/example.py deleted file mode 100644 index a7e6902..0000000 --- a/modules/_template/workflow/scripts/example.py +++ /dev/null @@ -1,7 +0,0 @@ -"""A python script for your workflow!""" - -with open(snakemake.output[0], "w") as file: - file.write( - "I am just an example for your worklow... " - "Please delete me if you copy this example as a template!" - ) diff --git a/modules/_template/{{module_name}}/AUTHORS.md.jinja b/modules/_template/{{module_name}}/AUTHORS.md.jinja new file mode 100644 index 0000000..db49f13 --- /dev/null +++ b/modules/_template/{{module_name}}/AUTHORS.md.jinja @@ -0,0 +1,10 @@ +# Authors + +This is the list of contributors to the '{{module_name}}' module for copyright purposes. + +- {{author_name}}, {{author_institution}} <{{author_email}}> + +This does not necessarily list everyone who has contributed to the '{{module_name}}' module code or documentation. +For a full contributor list, see: + + diff --git a/modules/_template/LICENSE b/modules/_template/{{module_name}}/LICENSE.jinja similarity index 97% rename from modules/_template/LICENSE rename to modules/_template/{{module_name}}/LICENSE.jinja index e9b75a4..ca13cfd 100644 --- a/modules/_template/LICENSE +++ b/modules/_template/{{module_name}}/LICENSE.jinja @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021, AUTHORS +Copyright (c) {{year}}, AUTHORS Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/modules/_template/{{module_name}}/README.md.jinja b/modules/_template/{{module_name}}/README.md.jinja new file mode 100644 index 0000000..212fe51 --- /dev/null +++ b/modules/_template/{{module_name}}/README.md.jinja @@ -0,0 +1,34 @@ +# Easy Energy Modules - {{module_name}} + +{{module_description}} + +## Input-Ouput + +Here is a brief IO diagram of the module's operation. + +```mermaid +--- +title: {{module_name}} +--- +flowchart LR + D1[("`**Databases** + database1 + ... + `")] --> |Download| M + C1[/"`**User input** + shapefile.geojson + ... + `"/] --> |Resources| M(({{module_name}})) + M --> O1(" + output1.csv + ") + M --> O2(" + output2.nc + ") +``` + +## DAG + +Here is a brief example of the module's steps. + +![DAG](rulegraph.png) diff --git a/modules/_template/{{module_name}}/config/default.yaml b/modules/_template/{{module_name}}/config/default.yaml new file mode 100644 index 0000000..cd90385 --- /dev/null +++ b/modules/_template/{{module_name}}/config/default.yaml @@ -0,0 +1,5 @@ +# Module input files by users go here. +resources: + download: True + shapefile: "resources/shapefile.geojson" +# Any other user configuration goes below diff --git a/modules/_template/{{module_name}}/rulegraph.png b/modules/_template/{{module_name}}/rulegraph.png new file mode 100644 index 0000000..88b3eb1 Binary files /dev/null and b/modules/_template/{{module_name}}/rulegraph.png differ diff --git a/modules/_template/{{module_name}}/workflow/Snakefile.jinja b/modules/_template/{{module_name}}/workflow/Snakefile.jinja new file mode 100644 index 0000000..e168ae3 --- /dev/null +++ b/modules/_template/{{module_name}}/workflow/Snakefile.jinja @@ -0,0 +1,22 @@ +import yaml + +from snakemake.utils import min_version, validate + +# Limit the snakemake version to a modern one. +min_version("8.10") + +# Load the default configuration. This will be overridden by users. +configfile: "config/default.yaml" +# Validate the configuration using the schema file. +validate(config, workflow.source_path("schemas/config.schema.yaml")) + +# Load internal settings separately so users cannot modify them. +with open(workflow.source_path("resources/internal_config.yaml"), "r") as f: + internal = yaml.safe_load(f) + +# Add all your includes here. +include: "rules/downloads.smk" + +rule all: + message: "Generate all outputs for '{{module_name}}'." + input: diff --git a/modules/_template/workflow/envs/shell.yaml b/modules/_template/{{module_name}}/workflow/envs/shell.yaml similarity index 72% rename from modules/_template/workflow/envs/shell.yaml rename to modules/_template/{{module_name}}/workflow/envs/shell.yaml index 9272d11..8906f10 100644 --- a/modules/_template/workflow/envs/shell.yaml +++ b/modules/_template/{{module_name}}/workflow/envs/shell.yaml @@ -3,6 +3,5 @@ channels: - conda-forge - nodefaults dependencies: - - curl=8.6.0 + - curl=8.9.1 - unzip=6.0 - - rsync=3.2.3 diff --git a/modules/_template/{{module_name}}/workflow/internal/internal_config.yaml b/modules/_template/{{module_name}}/workflow/internal/internal_config.yaml new file mode 100644 index 0000000..f00554e --- /dev/null +++ b/modules/_template/{{module_name}}/workflow/internal/internal_config.yaml @@ -0,0 +1,3 @@ +# This file contains configuration that users should not modify. +resources: + shapefile_url: diff --git a/modules/_template/workflow/profiles/default/config.yaml b/modules/_template/{{module_name}}/workflow/profiles/default/config.yaml similarity index 64% rename from modules/_template/workflow/profiles/default/config.yaml rename to modules/_template/{{module_name}}/workflow/profiles/default/config.yaml index 7ac5232..e8e2be8 100644 --- a/modules/_template/workflow/profiles/default/config.yaml +++ b/modules/_template/{{module_name}}/workflow/profiles/default/config.yaml @@ -1,3 +1,5 @@ +# DO NOT MODIFY THESE VALUES! +# They enable our wrappers to work. software-deployment-method: conda use-conda: True wrapper-prefix: https://github.com/calliope-project/ec_modules/raw/ diff --git a/modules/_template/{{module_name}}/workflow/report/report.rst.jinja b/modules/_template/{{module_name}}/workflow/report/report.rst.jinja new file mode 100644 index 0000000..494373f --- /dev/null +++ b/modules/_template/{{module_name}}/workflow/report/report.rst.jinja @@ -0,0 +1 @@ +Report of '{{module_name}}'. diff --git a/modules/_template/{{module_name}}/workflow/rules/downloads.smk b/modules/_template/{{module_name}}/workflow/rules/downloads.smk new file mode 100644 index 0000000..917f72c --- /dev/null +++ b/modules/_template/{{module_name}}/workflow/rules/downloads.smk @@ -0,0 +1 @@ +# We recommend adding rules that download necessary files here. diff --git a/modules/_template/{{module_name}}/workflow/schemas/config.schema.yaml.jinja b/modules/_template/{{module_name}}/workflow/schemas/config.schema.yaml.jinja new file mode 100644 index 0000000..657983b --- /dev/null +++ b/modules/_template/{{module_name}}/workflow/schemas/config.schema.yaml.jinja @@ -0,0 +1,18 @@ +$schema: https://json-schema.org/draft/2020-12/schema +description: "Configuration schema for '{{module_name}}'." +type: object +additionalProperties: false +properties: + resources: + type: object + additionalProperties: false + description: Resources are paths to files that the module expects users to provide. + properties: + download: + type: boolean + description: If True, the module will attempt to download resource files instead of relying on user inputs. + shapefile: + type: string + description: | + A file with the shapes you wish to process. + Must have the following columns: 'country_id', 'subregion_id' and 'subregion_spec'. diff --git a/modules/_template/{{module_name}}/workflow/scripts/.gitkeep b/modules/_template/{{module_name}}/workflow/scripts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/wrappers/_template/copier.yaml b/wrappers/_template/copier.yaml new file mode 100644 index 0000000..a54a060 --- /dev/null +++ b/wrappers/_template/copier.yaml @@ -0,0 +1,48 @@ +tool_name: + type: str + help: What is the name of the tool you are designing a wrapper for? + placeholder: tsam + validator: >- + {% if not (tool_name | regex_search('^[a-z][a-z0-9_]+$')) %} + "Must be a single word in lowercase. Letters, digits and underscores are valid characters." + {% endif %} +tool_url: + type: str + help: Please provide a valid link to the tool's official website. + placeholder: https://github.com/FZJ-IEK3-VSA/tsam + validator: >- + {% if not (tool_url | regex_search('^(https?|http?):\/\/.+$')) %} + "Must be a valid URL." + {% endif %} +wrapper_name: + type: str + help: What is the name of the wrapper? + placeholder: timeseries + validator: >- + {% if not (wrapper_name | regex_search('^[a-z][a-z0-9_]+$')) %} + "Must be a single word in lowercase. Letters, digits and underscores are valid characters." + {% endif %} +wrapper_description: + type: str + help: Please give a brief sentence describing your wrapper. + placeholder: Runs timeseries aggregation functions for any number of files. +author_name: + type: str + help: We auto-generate an MIT license for you. Please provide your full name. + placeholder: A. Donda +author_institution: + type: str + help: We auto-generate an MIT license for you. Please provide the name of your institution. + placeholder: Solaris University +author_email: + type: str + help: We auto-generate an MIT license for you. Please provide an email address. + placeholder: a.donda@solaris.uni.edu + validator: >- + {% if not (author_email | regex_search('^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$')) %} + "This email does not seem valid... did you forget to add a dot?" + {% endif %} +year: + type: str + help: We auto-generate an MIT license for you. What year is this? + placeholder: "2024" diff --git a/wrappers/_template/{{tool_name}}/{{wrapper_name}}/README.md.jinja b/wrappers/_template/{{tool_name}}/{{wrapper_name}}/README.md.jinja new file mode 100644 index 0000000..1470691 --- /dev/null +++ b/wrappers/_template/{{tool_name}}/{{wrapper_name}}/README.md.jinja @@ -0,0 +1,11 @@ +# `{{tool_name}}` - {{wrapper_name}} + +{{wrapper_description}} + +```mermaid +flowchart LR + I1(some_file.csv) --> W(({{wrapper_name}})) + I2(some_optional_file.nc) --> |Optional| W + W --> O1(some_output.csv) + W --> |Optional| O2(optional_output.png) +``` diff --git a/wrappers/_template/{{tool_name}}/{{wrapper_name}}/environment.yaml.jinja b/wrappers/_template/{{tool_name}}/{{wrapper_name}}/environment.yaml.jinja new file mode 100644 index 0000000..b7a23a7 --- /dev/null +++ b/wrappers/_template/{{tool_name}}/{{wrapper_name}}/environment.yaml.jinja @@ -0,0 +1,6 @@ +name: ec-{{tool_name}}-{{wrapper_name}} +channels: + - conda-forge + - nodefaults +dependencies: + - diff --git a/wrappers/_template/{{tool_name}}/{{wrapper_name}}/meta.yaml.jinja b/wrappers/_template/{{tool_name}}/{{wrapper_name}}/meta.yaml.jinja new file mode 100644 index 0000000..8d9af82 --- /dev/null +++ b/wrappers/_template/{{tool_name}}/{{wrapper_name}}/meta.yaml.jinja @@ -0,0 +1,9 @@ +name: {{tool_name}} - {{wrapper_name}} +description: | + {{wrapper_description}} +url: {{tool_url}} +authors: + - {{author_name}} +input: +output: +params: diff --git a/wrappers/_template/{{tool_name}}/{{wrapper_name}}/test/Snakefile.jinja b/wrappers/_template/{{tool_name}}/{{wrapper_name}}/test/Snakefile.jinja new file mode 100644 index 0000000..80cbb66 --- /dev/null +++ b/wrappers/_template/{{tool_name}}/{{wrapper_name}}/test/Snakefile.jinja @@ -0,0 +1,9 @@ +rule {{tool_name}}_{{wrapper_name}}: + input: + some_file = "somefile.csv", + some_optional_file = "some_optional_file.nc" + output: + some_output = "results/some_output.csv", + optional_output = "results/optional_output.png" + params: + wrapper: "file:../" diff --git a/wrappers/_template/{{tool_name}}/{{wrapper_name}}/wrapper.py.jinja b/wrappers/_template/{{tool_name}}/{{wrapper_name}}/wrapper.py.jinja new file mode 100644 index 0000000..df00b1c --- /dev/null +++ b/wrappers/_template/{{tool_name}}/{{wrapper_name}}/wrapper.py.jinja @@ -0,0 +1,6 @@ +"""Wrapper for {{tool_name}} - {{wrapper_name}}.""" + +__author__ = "{{author_name}}" +__copyright__ = "Copyright {{year}}, {{author_name}}" +__email__ = "{{author_email}}" +__license__ = "MIT"