Skip to content

Commit

Permalink
Replace tf.keras with tf_keras.
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 601706370
  • Loading branch information
achoum authored and copybara-github committed Jan 26, 2024
1 parent 207b647 commit b9a4cf1
Show file tree
Hide file tree
Showing 33 changed files with 617 additions and 335 deletions.
1 change: 1 addition & 0 deletions configure/setup.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"absl_py", "absl_py",
"wheel", "wheel",
"wurlitzer", "wurlitzer",
"tf_keras",
] ]




Expand Down
2 changes: 1 addition & 1 deletion documentation/known_issues.md
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ to the estimator format.


While abstracted by the Keras API, a model instantiated in Python (e.g., with While abstracted by the Keras API, a model instantiated in Python (e.g., with
`tfdf.keras.RandomForestModel()`) and a model loaded from disk (e.g., with `tfdf.keras.RandomForestModel()`) and a model loaded from disk (e.g., with
`tf.keras.models.load_model()`) can behave differently. Notably, a Python `tf_keras.models.load_model()`) can behave differently. Notably, a Python
instantiated model automatically applies necessary type conversions. For instantiated model automatically applies necessary type conversions. For
example, if a `float64` feature is fed to a model expecting a `float32` feature, example, if a `float64` feature is fed to a model expecting a `float32` feature,
this conversion is performed implicitly. However, such a conversion is not this conversion is performed implicitly. However, such a conversion is not
Expand Down
6 changes: 3 additions & 3 deletions documentation/migration.md
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -352,18 +352,18 @@ dataset reads are deterministic as well.
#### Specify a task (e.g. classification, ranking) instead of a loss (e.g. binary cross-entropy) #### Specify a task (e.g. classification, ranking) instead of a loss (e.g. binary cross-entropy)


```diff {.bad} ```diff {.bad}
- model = tf.keras.Sequential() - model = tf_keras.Sequential()
- model.add(Dense(64, activation=relu)) - model.add(Dense(64, activation=relu))
- model.add(Dense(1)) # One output for binary classification - model.add(Dense(1)) # One output for binary classification


- model.compile(loss=tf.keras.losses.BinaryCrossentropy(from_logits=True), - model.compile(loss=tf_keras.losses.BinaryCrossentropy(from_logits=True),
- optimizer='adam', - optimizer='adam',
- metrics=['accuracy']) - metrics=['accuracy'])
``` ```


```diff {.good} ```diff {.good}
# The loss is automatically determined from the task. # The loss is automatically determined from the task.
+ model = tfdf.keras.GradientBoostedTreesModel(task=tf.keras.Task.CLASSIFICATION) + model = tfdf.keras.GradientBoostedTreesModel(task=tf_keras.Task.CLASSIFICATION)


# Optional if you want to report the accuracy. # Optional if you want to report the accuracy.
+ model.compile(metrics=['accuracy']) + model.compile(metrics=['accuracy'])
Expand Down
3 changes: 2 additions & 1 deletion documentation/tf_df_in_tf_js.md
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -45,9 +45,10 @@ import tensorflow as tf
import tensorflow_decision_forests as tfdf import tensorflow_decision_forests as tfdf
import tensorflowjs as tfjs import tensorflowjs as tfjs
from google.colab import files from google.colab import files
import tf_keras


# Load the model with Keras # Load the model with Keras
model = tf.keras.models.load_model("/tmp/my_saved_model/") model = tf_keras.models.load_model("/tmp/my_saved_model/")


# Convert the keras model to TensorFlow.js # Convert the keras model to TensorFlow.js
tfjs.converters.tf_saved_model_conversion_v2.convert_keras_model_to_graph_model(model, "./tfjs_model") tfjs.converters.tf_saved_model_conversion_v2.convert_keras_model_to_graph_model(model, "./tfjs_model")
Expand Down
2 changes: 1 addition & 1 deletion documentation/tutorials/advanced_colab.ipynb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -639,7 +639,7 @@
}, },
"outputs": [], "outputs": [],
"source": [ "source": [
"manual_model = tf.keras.models.load_model(\"/tmp/manual_model\")" "manual_model = tf_keras.models.load_model(\"/tmp/manual_model\")"
] ]
}, },
{ {
Expand Down
8 changes: 4 additions & 4 deletions documentation/tutorials/beginner_colab.ipynb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -1078,16 +1078,16 @@
"source": [ "source": [
"%set_cell_height 300\n", "%set_cell_height 300\n",
"\n", "\n",
"body_mass_g = tf.keras.layers.Input(shape=(1,), name=\"body_mass_g\")\n", "body_mass_g = tf_keras.layers.Input(shape=(1,), name=\"body_mass_g\")\n",
"body_mass_kg = body_mass_g / 1000.0\n", "body_mass_kg = body_mass_g / 1000.0\n",
"\n", "\n",
"bill_length_mm = tf.keras.layers.Input(shape=(1,), name=\"bill_length_mm\")\n", "bill_length_mm = tf_keras.layers.Input(shape=(1,), name=\"bill_length_mm\")\n",
"\n", "\n",
"raw_inputs = {\"body_mass_g\": body_mass_g, \"bill_length_mm\": bill_length_mm}\n", "raw_inputs = {\"body_mass_g\": body_mass_g, \"bill_length_mm\": bill_length_mm}\n",
"processed_inputs = {\"body_mass_kg\": body_mass_kg, \"bill_length_mm\": bill_length_mm}\n", "processed_inputs = {\"body_mass_kg\": body_mass_kg, \"bill_length_mm\": bill_length_mm}\n",
"\n", "\n",
"# \"preprocessor\" contains the preprocessing logic.\n", "# \"preprocessor\" contains the preprocessing logic.\n",
"preprocessor = tf.keras.Model(inputs=raw_inputs, outputs=processed_inputs)\n", "preprocessor = tf_keras.Model(inputs=raw_inputs, outputs=processed_inputs)\n",
"\n", "\n",
"# \"model_4\" contains both the pre-processing logic and the decision forest.\n", "# \"model_4\" contains both the pre-processing logic and the decision forest.\n",
"model_4 = tfdf.keras.RandomForestModel(preprocessing=preprocessor)\n", "model_4 = tfdf.keras.RandomForestModel(preprocessing=preprocessor)\n",
Expand Down Expand Up @@ -1122,7 +1122,7 @@
" tf.feature_column.numeric_column(\"bill_length_mm\"),\n", " tf.feature_column.numeric_column(\"bill_length_mm\"),\n",
"]\n", "]\n",
"\n", "\n",
"preprocessing = tf.keras.layers.DenseFeatures(feature_columns)\n", "preprocessing = tf_keras.layers.DenseFeatures(feature_columns)\n",
"\n", "\n",
"model_5 = tfdf.keras.RandomForestModel(preprocessing=preprocessing)\n", "model_5 = tfdf.keras.RandomForestModel(preprocessing=preprocessing)\n",
"model_5.fit(train_ds)" "model_5.fit(train_ds)"
Expand Down
30 changes: 15 additions & 15 deletions documentation/tutorials/intermediate_colab.ipynb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -421,12 +421,12 @@
"hub_url = \"https://tfhub.dev/google/universal-sentence-encoder/4\"\n", "hub_url = \"https://tfhub.dev/google/universal-sentence-encoder/4\"\n",
"embedding = hub.KerasLayer(hub_url)\n", "embedding = hub.KerasLayer(hub_url)\n",
"\n", "\n",
"sentence = tf.keras.layers.Input(shape=(), name=\"sentence\", dtype=tf.string)\n", "sentence = tf_keras.layers.Input(shape=(), name=\"sentence\", dtype=tf.string)\n",
"embedded_sentence = embedding(sentence)\n", "embedded_sentence = embedding(sentence)\n",
"\n", "\n",
"raw_inputs = {\"sentence\": sentence}\n", "raw_inputs = {\"sentence\": sentence}\n",
"processed_inputs = {\"embedded_sentence\": embedded_sentence}\n", "processed_inputs = {\"embedded_sentence\": embedded_sentence}\n",
"preprocessor = tf.keras.Model(inputs=raw_inputs, outputs=processed_inputs)\n", "preprocessor = tf_keras.Model(inputs=raw_inputs, outputs=processed_inputs)\n",
"\n", "\n",
"model_2 = tfdf.keras.RandomForestModel(\n", "model_2 = tfdf.keras.RandomForestModel(\n",
" preprocessing=preprocessor,\n", " preprocessing=preprocessor,\n",
Expand Down Expand Up @@ -621,8 +621,8 @@
}, },
"outputs": [], "outputs": [],
"source": [ "source": [
"input_1 = tf.keras.Input(shape=(1,), name=\"bill_length_mm\", dtype=\"float\")\n", "input_1 = tf_keras.Input(shape=(1,), name=\"bill_length_mm\", dtype=\"float\")\n",
"input_2 = tf.keras.Input(shape=(1,), name=\"island\", dtype=\"string\")\n", "input_2 = tf_keras.Input(shape=(1,), name=\"island\", dtype=\"string\")\n",
"\n", "\n",
"nn_raw_inputs = [input_1, input_2]" "nn_raw_inputs = [input_1, input_2]"
] ]
Expand All @@ -645,9 +645,9 @@
"outputs": [], "outputs": [],
"source": [ "source": [
"# Normalization.\n", "# Normalization.\n",
"Normalization = tf.keras.layers.Normalization\n", "Normalization = tf_keras.layers.Normalization\n",
"CategoryEncoding = tf.keras.layers.CategoryEncoding\n", "CategoryEncoding = tf_keras.layers.CategoryEncoding\n",
"StringLookup = tf.keras.layers.StringLookup\n", "StringLookup = tf_keras.layers.StringLookup\n",
"\n", "\n",
"values = train_ds_pd[\"bill_length_mm\"].values[:, tf.newaxis]\n", "values = train_ds_pd[\"bill_length_mm\"].values[:, tf.newaxis]\n",
"input_1_normalizer = Normalization()\n", "input_1_normalizer = Normalization()\n",
Expand Down Expand Up @@ -682,15 +682,15 @@
}, },
"outputs": [], "outputs": [],
"source": [ "source": [
"y = tf.keras.layers.Concatenate()(nn_processed_inputs)\n", "y = tf_keras.layers.Concatenate()(nn_processed_inputs)\n",
"y = tf.keras.layers.Dense(16, activation=tf.nn.relu6)(y)\n", "y = tf_keras.layers.Dense(16, activation=tf.nn.relu6)(y)\n",
"last_layer = tf.keras.layers.Dense(8, activation=tf.nn.relu, name=\"last\")(y)\n", "last_layer = tf_keras.layers.Dense(8, activation=tf.nn.relu, name=\"last\")(y)\n",
"\n", "\n",
"# \"3\" for the three label classes. If it were a binary classification, the\n", "# \"3\" for the three label classes. If it were a binary classification, the\n",
"# output dim would be 1.\n", "# output dim would be 1.\n",
"classification_output = tf.keras.layers.Dense(3)(y)\n", "classification_output = tf_keras.layers.Dense(3)(y)\n",
"\n", "\n",
"nn_model = tf.keras.models.Model(nn_raw_inputs, classification_output)" "nn_model = tf_keras.models.Model(nn_raw_inputs, classification_output)"
] ]
}, },
{ {
Expand All @@ -714,7 +714,7 @@
"source": [ "source": [
"# To reduce the risk of mistakes, group both the decision forest and the\n", "# To reduce the risk of mistakes, group both the decision forest and the\n",
"# neural network in a single keras model.\n", "# neural network in a single keras model.\n",
"nn_without_head = tf.keras.models.Model(inputs=nn_model.inputs, outputs=last_layer)\n", "nn_without_head = tf_keras.models.Model(inputs=nn_model.inputs, outputs=last_layer)\n",
"df_and_nn_model = tfdf.keras.RandomForestModel(preprocessing=nn_without_head)" "df_and_nn_model = tfdf.keras.RandomForestModel(preprocessing=nn_without_head)"
] ]
}, },
Expand All @@ -740,8 +740,8 @@
"%set_cell_height 300\n", "%set_cell_height 300\n",
"\n", "\n",
"nn_model.compile(\n", "nn_model.compile(\n",
" optimizer=tf.keras.optimizers.Adam(),\n", " optimizer=tf_keras.optimizers.Adam(),\n",
" loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),\n", " loss=tf_keras.losses.SparseCategoricalCrossentropy(from_logits=True),\n",
" metrics=[\"accuracy\"])\n", " metrics=[\"accuracy\"])\n",
"\n", "\n",
"nn_model.fit(x=train_ds, validation_data=test_ds, epochs=10)\n", "nn_model.fit(x=train_ds, validation_data=test_ds, epochs=10)\n",
Expand Down
22 changes: 11 additions & 11 deletions documentation/tutorials/model_composition_colab.ipynb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -414,25 +414,25 @@
"outputs": [], "outputs": [],
"source": [ "source": [
"# Input features.\n", "# Input features.\n",
"raw_features = tf.keras.layers.Input(shape=(num_features,))\n", "raw_features = tf_keras.layers.Input(shape=(num_features,))\n",
"\n", "\n",
"# Stage 1\n", "# Stage 1\n",
"# =======\n", "# =======\n",
"\n", "\n",
"# Common learnable pre-processing\n", "# Common learnable pre-processing\n",
"preprocessor = tf.keras.layers.Dense(10, activation=tf.nn.relu6)\n", "preprocessor = tf_keras.layers.Dense(10, activation=tf.nn.relu6)\n",
"preprocess_features = preprocessor(raw_features)\n", "preprocess_features = preprocessor(raw_features)\n",
"\n", "\n",
"# Stage 2\n", "# Stage 2\n",
"# =======\n", "# =======\n",
"\n", "\n",
"# Model #1: NN\n", "# Model #1: NN\n",
"m1_z1 = tf.keras.layers.Dense(5, activation=tf.nn.relu6)(preprocess_features)\n", "m1_z1 = tf_keras.layers.Dense(5, activation=tf.nn.relu6)(preprocess_features)\n",
"m1_pred = tf.keras.layers.Dense(1, activation=tf.nn.sigmoid)(m1_z1)\n", "m1_pred = tf_keras.layers.Dense(1, activation=tf.nn.sigmoid)(m1_z1)\n",
"\n", "\n",
"# Model #2: NN\n", "# Model #2: NN\n",
"m2_z1 = tf.keras.layers.Dense(5, activation=tf.nn.relu6)(preprocess_features)\n", "m2_z1 = tf_keras.layers.Dense(5, activation=tf.nn.relu6)(preprocess_features)\n",
"m2_pred = tf.keras.layers.Dense(1, activation=tf.nn.sigmoid)(m2_z1)\n", "m2_pred = tf_keras.layers.Dense(1, activation=tf.nn.sigmoid)(m2_z1)\n",
"\n", "\n",
"\n", "\n",
"# Model #3: DF\n", "# Model #3: DF\n",
Expand Down Expand Up @@ -460,8 +460,8 @@
"# Keras Models\n", "# Keras Models\n",
"# ============\n", "# ============\n",
"\n", "\n",
"ensemble_nn_only = tf.keras.models.Model(raw_features, mean_nn_only)\n", "ensemble_nn_only = tf_keras.models.Model(raw_features, mean_nn_only)\n",
"ensemble_nn_and_df = tf.keras.models.Model(raw_features, mean_nn_and_df)" "ensemble_nn_and_df = tf_keras.models.Model(raw_features, mean_nn_and_df)"
] ]
}, },
{ {
Expand Down Expand Up @@ -509,8 +509,8 @@
"source": [ "source": [
"%%time\n", "%%time\n",
"ensemble_nn_only.compile(\n", "ensemble_nn_only.compile(\n",
" optimizer=tf.keras.optimizers.Adam(),\n", " optimizer=tf_keras.optimizers.Adam(),\n",
" loss=tf.keras.losses.BinaryCrossentropy(),\n", " loss=tf_keras.losses.BinaryCrossentropy(),\n",
" metrics=[\"accuracy\"])\n", " metrics=[\"accuracy\"])\n",
"\n", "\n",
"ensemble_nn_only.fit(train_dataset, epochs=20, validation_data=test_dataset)" "ensemble_nn_only.fit(train_dataset, epochs=20, validation_data=test_dataset)"
Expand Down Expand Up @@ -610,7 +610,7 @@
"outputs": [], "outputs": [],
"source": [ "source": [
"ensemble_nn_and_df.compile(\n", "ensemble_nn_and_df.compile(\n",
" loss=tf.keras.losses.BinaryCrossentropy(), metrics=[\"accuracy\"])\n", " loss=tf_keras.losses.BinaryCrossentropy(), metrics=[\"accuracy\"])\n",
"\n", "\n",
"evaluation_nn_and_df = ensemble_nn_and_df.evaluate(\n", "evaluation_nn_and_df = ensemble_nn_and_df.evaluate(\n",
" test_dataset, return_dict=True)\n", " test_dataset, return_dict=True)\n",
Expand Down
2 changes: 1 addition & 1 deletion documentation/tutorials/predict_colab.ipynb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@
"\n", "\n",
"While abstracted by the Keras API, a model instantiated in Python (e.g., with\n", "While abstracted by the Keras API, a model instantiated in Python (e.g., with\n",
"`tfdf.keras.RandomForestModel()`) and a model loaded from disk (e.g., with\n", "`tfdf.keras.RandomForestModel()`) and a model loaded from disk (e.g., with\n",
"`tf.keras.models.load_model()`) can behave differently. Notably, a Python\n", "`tf_keras.models.load_model()`) can behave differently. Notably, a Python\n",
"instantiated model automatically applies necessary type conversions. For\n", "instantiated model automatically applies necessary type conversions. For\n",
"example, if a `float64` feature is fed to a model expecting a `float32` feature,\n", "example, if a `float64` feature is fed to a model expecting a `float32` feature,\n",
"this conversion is performed implicitly. However, such a conversion is not\n", "this conversion is performed implicitly. However, such a conversion is not\n",
Expand Down
2 changes: 1 addition & 1 deletion documentation/tutorials/ranking_colab.ipynb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@
}, },
"outputs": [], "outputs": [],
"source": [ "source": [
"archive_path = tf.keras.utils.get_file(\"letor.zip\",\n", "archive_path = tf_keras.utils.get_file(\"letor.zip\",\n",
" \"https://download.microsoft.com/download/E/7/E/E7EABEF1-4C7B-4E31-ACE5-73927950ED5E/Letor.zip\",\n", " \"https://download.microsoft.com/download/E/7/E/E7EABEF1-4C7B-4E31-ACE5-73927950ED5E/Letor.zip\",\n",
" extract=True)\n", " extract=True)\n",
"\n", "\n",
Expand Down
4 changes: 4 additions & 0 deletions examples/BUILD
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ py_binary(
# pandas dep, # pandas dep,
# TensorFlow Python, # TensorFlow Python,
"//tensorflow_decision_forests", "//tensorflow_decision_forests",
# tf_keras dep,
], ],
) )


Expand All @@ -28,6 +29,7 @@ py_binary(
# pandas dep, # pandas dep,
# TensorFlow Python, # TensorFlow Python,
"//tensorflow_decision_forests", "//tensorflow_decision_forests",
# tf_keras dep,
], ],
) )


Expand All @@ -43,6 +45,7 @@ py_binary(
# pandas dep, # pandas dep,
# TensorFlow Python, # TensorFlow Python,
"//tensorflow_decision_forests", "//tensorflow_decision_forests",
# tf_keras dep,
], ],
) )


Expand All @@ -56,5 +59,6 @@ py_binary(
# absl/logging dep, # absl/logging dep,
# TensorFlow Python, # TensorFlow Python,
"//tensorflow_decision_forests", "//tensorflow_decision_forests",
# tf_keras dep,
], ],
) )
4 changes: 2 additions & 2 deletions examples/hyperparameter_optimization.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@
import pandas as pd import pandas as pd
import tensorflow as tf import tensorflow as tf
import tensorflow_decision_forests as tfdf import tensorflow_decision_forests as tfdf

import tf_keras


def main(argv): def main(argv):
if len(argv) > 1: if len(argv) > 1:
raise app.UsageError("Too many command-line arguments.") raise app.UsageError("Too many command-line arguments.")


# Download the Adult dataset. # Download the Adult dataset.
dataset_path = tf.keras.utils.get_file( dataset_path = tf_keras.utils.get_file(
"adult.csv", "adult.csv",
"https://raw.githubusercontent.com/google/yggdrasil-decision-forests/" "https://raw.githubusercontent.com/google/yggdrasil-decision-forests/"
"main/yggdrasil_decision_forests/test_data/dataset/adult.csv") "main/yggdrasil_decision_forests/test_data/dataset/adult.csv")
Expand Down
13 changes: 8 additions & 5 deletions examples/minimal.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -30,22 +30,23 @@
""" """


from absl import app from absl import app

import numpy as np import numpy as np
import pandas as pd import pandas as pd
import tensorflow as tf import tensorflow as tf
import tensorflow_decision_forests as tfdf import tensorflow_decision_forests as tfdf
import tf_keras




def main(argv): def main(argv):
if len(argv) > 1: if len(argv) > 1:
raise app.UsageError("Too many command-line arguments.") raise app.UsageError("Too many command-line arguments.")


# Download the Adult dataset. # Download the Adult dataset.
dataset_path = tf.keras.utils.get_file( dataset_path = tf_keras.utils.get_file(
"adult.csv", "adult.csv",
"https://raw.githubusercontent.com/google/yggdrasil-decision-forests/" "https://raw.githubusercontent.com/google/yggdrasil-decision-forests/"
"main/yggdrasil_decision_forests/test_data/dataset/adult.csv") "main/yggdrasil_decision_forests/test_data/dataset/adult.csv",
)


# Load a dataset into a Pandas Dataframe. # Load a dataset into a Pandas Dataframe.
dataset_df = pd.read_csv(dataset_path) # "df" for Pandas's DataFrame. dataset_df = pd.read_csv(dataset_path) # "df" for Pandas's DataFrame.
Expand All @@ -61,8 +62,10 @@ def main(argv):
test_indices = np.random.rand(len(dataset_df)) < 0.30 test_indices = np.random.rand(len(dataset_df)) < 0.30
test_ds_pd = dataset_df[test_indices] test_ds_pd = dataset_df[test_indices]
train_ds_pd = dataset_df[~test_indices] train_ds_pd = dataset_df[~test_indices]
print(f"{len(train_ds_pd)} examples in training" print(
f", {len(test_ds_pd)} examples for testing.") f"{len(train_ds_pd)} examples in training"
f", {len(test_ds_pd)} examples for testing."
)


# Converts datasets from Pandas dataframe to TensorFlow dataset format. # Converts datasets from Pandas dataframe to TensorFlow dataset format.
train_ds = tfdf.keras.pd_dataframe_to_tf_dataset(train_ds_pd, label="income") train_ds = tfdf.keras.pd_dataframe_to_tf_dataset(train_ds_pd, label="income")
Expand Down
2 changes: 1 addition & 1 deletion tensorflow_decision_forests/__init__.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
# ... # ...
# Load a model: it loads as a generic keras model. # Load a model: it loads as a generic keras model.
loaded_model = tf.keras.models.load_model("/tmp/my_saved_model") loaded_model = tf_keras.models.load_model("/tmp/my_saved_model")
``` ```
""" """
Expand Down
2 changes: 1 addition & 1 deletion tensorflow_decision_forests/component/builder/builder.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@
builder.close() builder.close()
# Load and use the model # Load and use the model
model = tf.keras.models.load_model("/path/to/model") model = tf_keras.models.load_model("/path/to/model")
predictions = model.predict(...) predictions = model.predict(...)
``` ```
""" """
Expand Down
Loading

0 comments on commit b9a4cf1

Please sign in to comment.