Compare commits

..
Author SHA1 Message Date
Eric Schmidt 9ea9d6005e added images 2022-12-16 18:30:34 +00:00
Eric Schmidt bd7ddfbf26 iter 2022-12-15 21:37:51 +00:00
Eric Schmidt a6d9b9a8f0 feat: Adds first part of Fantasy Maps notebooks 2022-12-15 21:34:42 +00:00
Andrew FerlitschandGitHub 427bd3d5ea upgrade: replace CURL with GAPIC (#1357) 2022-12-15 11:47:33 -08:00
14 changed files with 4073 additions and 248 deletions
+5 -1
View File
@@ -5,6 +5,8 @@ from resource_cleanup_manager import (
ModelResourceCleanupManager,
EndpointResourceCleanupManager,
ResourceCleanupManager,
MatchingEngineIndexEndpointResourceCleanupManager,
MatchingEngineIndexResourceCleanupManager,
)
rate_limit = RateLimit(max_count=25, per=60, greedy=False)
@@ -40,10 +42,12 @@ if is_dry_run:
print("Starting cleanup in dry run mode...")
# List of all cleanup managers
managers = [
managers: List[ResourceCleanupManager] = [
DatasetResourceCleanupManager(),
EndpointResourceCleanupManager(),
ModelResourceCleanupManager(), # ModelResourceCleanupManager must follow EndpointResourceCleanupManager due to deployed models blocking model deletion.
MatchingEngineIndexEndpointResourceCleanupManager(),
MatchingEngineIndexResourceCleanupManager(),
]
run_cleanup_managers(managers=managers, is_dry_run=is_dry_run)
@@ -110,3 +110,10 @@ class EndpointResourceCleanupManager(VertexAIResourceCleanupManager):
class ModelResourceCleanupManager(VertexAIResourceCleanupManager):
vertex_ai_resource = aiplatform.Model
class MatchingEngineIndexResourceCleanupManager(VertexAIResourceCleanupManager):
vertex_ai_resource = aiplatform.MatchingEngineIndex
class MatchingEngineIndexEndpointResourceCleanupManager(VertexAIResourceCleanupManager):
vertex_ai_resource = aiplatform.MatchingEngineIndexEndpoint
@@ -245,15 +245,13 @@ def process_and_execute_notebook(
result.logs_bucket = operation_metadata.build.logs_bucket
# Block and wait for the result
operation_result = operation.result(timeout=84600)
operation_result = operation.result()
result.duration = datetime.datetime.now() - time_start
result.is_pass = True
print(f"{notebook} PASSED in {format_timedelta(result.duration)}.")
except Exception as error:
result.error_message = str(error)
import traceback
traceback.print_exc()
if operation and should_get_tail_logs:
# Extract the logs
-1
View File
@@ -66,7 +66,6 @@ def execute_notebook(
# Execute notebook
try:
print("DEBUG HERE\n")
# Execute notebook
pm.execute_notebook(
input_path=notebook_source,
+1 -11
View File
@@ -45,9 +45,6 @@ def execute_notebook_remote(
"""Create and execute a single notebook on Google Cloud Build"""
# Load build steps from YAML
print(f"DEBUG TIMEOUT {timeout_in_seconds}\n")
cloudbuild_config = yaml.load(open(CLOUD_BUILD_FILEPATH), Loader=FullLoader)
substitutions = {
@@ -98,14 +95,7 @@ def execute_notebook_remote(
if tag:
build.tags = [tag]
try:
print("DEBUG: START\n")
operation = client.create_build(project_id=project_id, build=build)
except Exception as e:
import traceback
traceback.print_exc()
print("DEBUG: FINISH\n")
print(operation)
operation = client.create_build(project_id=project_id, build=build)
# Print the in-progress operation
# print("IN PROGRESS:")
# print(operation.metadata)
@@ -36,7 +36,7 @@ steps:
- -c
- |
. workspace/env/bin/activate &&
python3 .cloud-build/execute_changed_notebooks_cli.py --test_paths_file "${_TEST_PATHS_FILE}" --base_branch "${_FORCED_BASE_BRANCH}" --container_uri ${_PYTHON_IMAGE} --staging_bucket ${_GCS_STAGING_BUCKET} --artifacts_bucket ${_GCS_STAGING_BUCKET}/executed_notebooks/PR_${_PR_NUMBER}/BUILD_${BUILD_ID} --variable_project_id ${PROJECT_ID} --variable_region ${_GCP_REGION} --variable_service_account ${_GCP_SERVICE_ACCOUNT} --variable_vpc_network "${_GPC_VPC_NETWORK_NAME}" --timeout 86400 `if [ ! -z "${_PRIVATE_POOL_NAME}" ]; then echo "--private_pool_id ${_PRIVATE_POOL_NAME}"; fi`
python3 .cloud-build/execute_changed_notebooks_cli.py --test_paths_file "${_TEST_PATHS_FILE}" --base_branch "${_FORCED_BASE_BRANCH}" --container_uri ${_PYTHON_IMAGE} --staging_bucket ${_GCS_STAGING_BUCKET} --artifacts_bucket ${_GCS_STAGING_BUCKET}/executed_notebooks/PR_${_PR_NUMBER}/BUILD_${BUILD_ID} --variable_project_id ${PROJECT_ID} --variable_region ${_GCP_REGION} --variable_service_account ${_GCP_SERVICE_ACCOUNT} --variable_vpc_network "${_GPC_VPC_NETWORK_NAME}" `if [ ! -z "${_PRIVATE_POOL_NAME}" ]; then echo "--private_pool_id ${_PRIVATE_POOL_NAME}"; fi`
env:
- 'IS_TESTING=1'
timeout: 86400s
+2 -3
View File
@@ -8,7 +8,6 @@ matplotlib
tabulate
google-cloud-aiplatform
google-cloud-storage
google-cloud-build==3.9.3
protobuf==4.21.9
google-cloud-build
ratemate
GitPython
GitPython
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,45 @@
# Fantasy Maps
Virtual tabletop gaming continues to grow in popularity. Often times, a core requirement for
virtual tabletop gaming apps (VTTs) is to provide a map (an image) and some metadata that
describes the valid game moves (as a grid) on that map.
With this explosion in popularity of VTTs, many folks in the gaming community publish their
own maps for others to use. However, the maps don't always include the metadata needed for
a VTT app.
That's where the Fantasy Maps project comes in.
This project uses
[Vertex AI](https://cloud.google.com/vertex-ai),
AutoML object detection models to train a model capable of identifying gridlines on a map
like the example below:
![Predictions shown on map of a desert](resources/predicted-gridded-desert.png)
## Products demonstrated
This project demonstrates how to combine multiple Google Cloud products together
to build this ML model. Specifically, the project demonstrates:
+ How to use [Firestore](https://cloud.google.com/firestore/docs) to store Vertex AI
training metadata for unstructured data (e.g. images)
+ How to use [Secret Manager](https://cloud.google.com/secret-manager/docs) to keep
API keys hidden until they're needed
+ How to store data in [Cloud Storage](https://cloud.google.com/storage/docs)
+ How to build a
[Vertex AI Pipeline](https://cloud.google.com/vertex-ai/docs/pipelines/introduction?hl=en)
to train, deploy, and retrain a Vertex AI model
+ How to use Vertex AI to to train, deploy, and get inferences from an image object
detection model
## Tutorial version
The project consists of five notebooks, where each notebook builds upon the lessons
of the previous notebook. The notebooks are as follows:
1. [Storing training data in Firestore](1_firestore.ipynb)
2. Creating a Vertex AI Pipeline to store training data in Firestore
3. Building an AutoML image object detection model
4. Creating a Vertex AI Pipeline to train and deploy an AutoML model
5. Getting predictions from the deployed model
Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

@@ -159,9 +159,9 @@
"\n",
"# Install the packages\n",
"\n",
"! pip3 install --upgrade google-cloud-aiplatform $USER_FLAG -q\n",
"! pip3 install --upgrade tensorflow $USER_FLAG -q\n",
"! pip3 install --upgrade tensorflow-hub $USER_FLAG -q"
"! pip3 install --upgrade google-cloud-aiplatform \\\n",
" tensorflow \\\n",
" tensorflow-hub $USER_FLAG -q"
]
},
{
@@ -307,22 +307,29 @@
"id": "timestamp"
},
"source": [
"#### Timestamp\n",
"#### UUID\n",
"\n",
"If you are in a live tutorial session, you might be using a shared test account or project. To avoid name collisions between users on resources created, you create a timestamp for each instance session, and append the timestamp onto the name of resources you create in this tutorial."
"If you are in a live tutorial session, you might be using a shared test account or project. To avoid name collisions between users on resources created, you create a uuid for each instance session, and append it onto the name of resources you create in this tutorial."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "timestamp"
"id": "84Vdv7R-QEH6"
},
"outputs": [],
"source": [
"from datetime import datetime\n",
"import random\n",
"import string\n",
"\n",
"TIMESTAMP = datetime.now().strftime(\"%Y%m%d%H%M%S\")"
"\n",
"# Generate a uuid of a specifed length(default=8)\n",
"def generate_uuid(length: int = 8) -> str:\n",
" return \"\".join(random.choices(string.ascii_lowercase + string.digits, k=length))\n",
"\n",
"\n",
"UUID = generate_uuid()"
]
},
{
@@ -421,7 +428,7 @@
"outputs": [],
"source": [
"if BUCKET_URI == \"\" or BUCKET_URI is None or BUCKET_URI == \"gs://[your-bucket-name]\":\n",
" BUCKET_NAME = PROJECT_ID + \"aip-\" + TIMESTAMP\n",
" BUCKET_NAME = PROJECT_ID + \"aip-\" + UUID\n",
" BUCKET_URI = \"gs://\" + BUCKET_NAME"
]
},
@@ -523,7 +530,7 @@
"\n",
"Setup up the following constants for Vertex AI:\n",
"\n",
"- `API_ENDPOINT`: The Vertex AI API service endpoint for `Endpoint` services."
"- `API_ENDPOINT`: The Vertex AI API service endpoint."
]
},
{
@@ -538,46 +545,10 @@
"API_ENDPOINT = \"{}-aiplatform.googleapis.com\".format(REGION)\n",
"\n",
"# Vertex location root path for your dataset, model and endpoint resources\n",
"PARENT = \"projects/\" + PROJECT_ID + \"/locations/\" + REGION"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "clients:metadata"
},
"source": [
"## Set up clients\n",
"PARENT = \"projects/\" + PROJECT_ID + \"/locations/\" + REGION\n",
"\n",
"The Vertex works as a client/server model. On your side (the Python script) you will create a client that sends requests and receives responses from the Vertex AI server.\n",
"\n",
"You will use different clients in this tutorial for different steps in the workflow. So set them all up upfront.\n",
"\n",
"- Endpoint Service for creating endpoints, and deploying models to endpoints."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "clients:metadata"
},
"outputs": [],
"source": [
"# client options same for all services\n",
"client_options = {\"api_endpoint\": API_ENDPOINT}\n",
"\n",
"\n",
"def create_endpoint_client():\n",
" client = aip_beta.EndpointServiceClient(client_options=client_options)\n",
" return client\n",
"\n",
"\n",
"clients = {}\n",
"clients[\"endpoint\"] = create_endpoint_client()\n",
"\n",
"for client in clients.items():\n",
" print(client)"
"client_options = {\"api_endpoint\": API_ENDPOINT}"
]
},
{
@@ -592,7 +563,7 @@
"\n",
"Set the variables `DEPLOY_GPU/DEPLOY_NGPU` to use a container image supporting a GPU and the number of GPUs allocated to the virtual machine (VM) instance. For example, to use a GPU container image with 4 Nvidia Telsa K80 GPUs allocated to each VM, you would specify:\n",
"\n",
" (aip.AcceleratorType.NVIDIA_TESLA_K80, 4)\n",
" (aip.gapic.AcceleratorType.NVIDIA_TESLA_K80, 4)\n",
"\n",
"\n",
"Otherwise specify `(None, None)` to use a container image to run on a CPU.\n",
@@ -902,7 +873,7 @@
"outputs": [],
"source": [
"model_icn = aiplatform.Model.upload(\n",
" display_name=\"icn_\" + TIMESTAMP,\n",
" display_name=\"icn_\" + UUID,\n",
" artifact_uri=MODEL_ICN_DIR,\n",
" serving_container_image_uri=DEPLOY_IMAGE,\n",
")\n",
@@ -1013,7 +984,7 @@
"outputs": [],
"source": [
"model_use = aiplatform.Model.upload(\n",
" display_name=\"icn_\" + TIMESTAMP,\n",
" display_name=\"icn_\" + UUID,\n",
" artifact_uri=MODEL_USE_DIR,\n",
" serving_container_image_uri=DEPLOY_IMAGE,\n",
")\n",
@@ -1029,64 +1000,52 @@
"source": [
"## Creating a deployment resource pool\n",
"\n",
"Currently, creating deploynent resource pools is only supported via the REST-based API (e.g., CURL).\n",
"Currently, creating deploynent resource pools is only supported via the REST-based API (e.g., CURL) and GAPIC APIs (Python).\n",
"\n",
"Use `CreateDeploymentResourcePool` API to create a resource pool, with the following configuration:\n",
"Use `create_deployment_resource_pool` API to create a resource pool, with the following configuration:\n",
"\n",
"- `dedicated_resources`: Compute (HW) resources to allocate for the shared vm.\n",
"- `min_replica_count`: Auto-scaling, the minimum number of compute nodes.\n",
"- `max_replica_count`: Auto-scaling, the maximum number of compute nodes.\n",
"\n",
"Learn more about [Deployment Resource Pools]()."
"Learn more about [Deployment Resource Pools](https://googleapis.dev/python/aiplatform/latest/aiplatform_v1beta1/deployment_resource_pool_service.html)."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "YiBmoiWYcMQt"
"id": "90c51b6cf34a"
},
"outputs": [],
"source": [
"DEPLOYMENT_RESOURCE_POOL_ID = \"shared-vm\" # @param {type: \"string\"}"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "0CHPJ4h-Slgs"
},
"outputs": [],
"source": [
"import json\n",
"import pprint\n",
"pp = pprint.PrettyPrinter(indent=4)\n",
"\n",
"DEPLOYMENT_RESOURCE_POOL_ID = f\"shared-vm-{UUID}\" # @param {type: \"string\"}\n",
"MIN_NODES = 1\n",
"MAX_NODES = 2\n",
"\n",
"CREATE_RP_PAYLOAD = {\n",
" \"deployment_resource_pool\":{\n",
" \"dedicated_resources\":{\n",
" \"machine_spec\":{\n",
" \"machine_type\": DEPLOY_COMPUTE\n",
" },\n",
" \"min_replica_count\": MIN_NODES, \n",
" \"max_replica_count\": MAX_NODES\n",
" }\n",
" },\n",
" \"deployment_resource_pool_id\":DEPLOYMENT_RESOURCE_POOL_ID\n",
"}\n",
"CREATE_RP_REQUEST=json.dumps(CREATE_RP_PAYLOAD)\n",
"pp.pprint(\"CREATE_RP_REQUEST: \" + CREATE_RP_REQUEST)\n",
"# Initialize request argument(s)\n",
"deployment_resource_pool = aip_beta.DeploymentResourcePool()\n",
"deployment_resource_pool.dedicated_resources.min_replica_count = MIN_NODES\n",
"deployment_resource_pool.dedicated_resources.max_replica_count = MAX_NODES\n",
"deployment_resource_pool.dedicated_resources.machine_spec.machine_type = DEPLOY_COMPUTE\n",
"\n",
"! curl \\\n",
"-X POST \\\n",
"-H \"Authorization: Bearer $(gcloud auth print-access-token)\" \\\n",
"-H \"Content-Type: application/json\" \\\n",
"https://{REGION}-aiplatform.googleapis.com/v1beta1/projects/{PROJECT_ID}/locations/{REGION}/deploymentResourcePools \\\n",
"-d '{CREATE_RP_REQUEST}'"
"request = aip_beta.CreateDeploymentResourcePoolRequest(\n",
" parent=f\"projects/{PROJECT_ID}/locations/{REGION}\",\n",
" deployment_resource_pool=deployment_resource_pool,\n",
" deployment_resource_pool_id=DEPLOYMENT_RESOURCE_POOL_ID,\n",
")\n",
"\n",
"pool_client = aip_beta.services.deployment_resource_pool_service.DeploymentResourcePoolServiceClient(\n",
" client_options=client_options\n",
")\n",
"\n",
"op = pool_client.create_deployment_resource_pool(request=request)\n",
"print(op)\n",
"\n",
"result = op.result()\n",
"print(result)\n",
"\n",
"deployment_pool_id = result.name"
]
},
{
@@ -1099,21 +1058,19 @@
"\n",
"Use `GetDeploymentResourcePool` API to check out the deploynent resource pool that you created. \n",
"\n",
"Learn more about [Get Deployment Resource Pool](https://source.corp.google.com/piper///depot/google3/google/cloud/aiplatform/master/deployment_resource_pool_service.proto;l=75?q=deployment_resource_pool&sq=package:piper%20file:%2F%2Fdepot%2Fgoogle3%20-file:google3%2Fexperimental)."
"Learn more about [Get Deployment Resource Pool](https://googleapis.dev/python/aiplatform/latest/aiplatform_v1beta1/deployment_resource_pool_service.html)."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "6wTLyhPraFah"
"id": "b740253903c0"
},
"outputs": [],
"source": [
"! curl -X GET \\\n",
"-H \"Authorization: Bearer $(gcloud auth print-access-token)\" \\\n",
"-H \"Content-Type: application/json\" \\\n",
"https://{REGION}-aiplatform.googleapis.com/v1beta1/projects/{PROJECT_ID}/locations/{REGION}/deploymentResourcePools/{DEPLOYMENT_RESOURCE_POOL_ID}"
"response = pool_client.get_deployment_resource_pool(name=deployment_pool_id)\n",
"print(response)"
]
},
{
@@ -1126,21 +1083,22 @@
"\n",
"Use `ListDeploymentResourcePools` API to list all the deployment resource pools. \n",
"\n",
"Learn more about [Listing Deployment Resource Pools](https://source.corp.google.com/piper///depot/google3/google/cloud/aiplatform/master/deployment_resource_pool_service.proto;l=101?q=deployment_resource_pool&sq=package:piper%20file:%2F%2Fdepot%2Fgoogle3%20-file:google3%2Fexperimental)."
"Learn more about [Listing Deployment Resource Pools](https://googleapis.dev/python/aiplatform/latest/aiplatform_v1beta1/deployment_resource_pool_service.html)."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "Pxls4sNnaltU"
"id": "3ebfd007bff2"
},
"outputs": [],
"source": [
"! curl -X GET \\\n",
"-H \"Authorization: Bearer $(gcloud auth print-access-token)\" \\\n",
"-H \"Content-Type: application/json\" \\\n",
"https://{REGION}-aiplatform.googleapis.com/v1beta1/projects/{PROJECT_ID}/locations/{REGION}/deploymentResourcePools"
"pools = pool_client.list_deployment_resource_pools(\n",
" parent=f\"projects/{PROJECT_ID}/locations/{REGION}\"\n",
")\n",
"for pool in pools:\n",
" print(pool)"
]
},
{
@@ -1170,11 +1128,11 @@
},
"outputs": [],
"source": [
"endpoint_icn = aiplatform.Endpoint.create(display_name=\"icn_\" + TIMESTAMP)\n",
"endpoint_icn = aiplatform.Endpoint.create(display_name=\"icn_\" + UUID)\n",
"\n",
"print(endpoint_icn)\n",
"\n",
"endpoint_use = aiplatform.Endpoint.create(display_name=\"use_\" + TIMESTAMP)\n",
"endpoint_use = aiplatform.Endpoint.create(display_name=\"use_\" + UUID)\n",
"\n",
"print(endpoint_use)"
]
@@ -1204,6 +1162,12 @@
},
"outputs": [],
"source": [
"import json\n",
"import pprint\n",
"\n",
"pp = pprint.PrettyPrinter(indent=4)\n",
"\n",
"\n",
"SHARED_RESOURCE = \"projects/{project_id}/locations/{region}/deploymentResourcePools/{deployment_resource_pool_id}\".format(\n",
" project_id=PROJECT_ID,\n",
" region=REGION,\n",
@@ -1363,18 +1327,27 @@
" time.sleep(30)"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "52248c450776"
},
"source": [
"### Get deployment details for the endpoint\n",
"\n",
"List the deployed models on the endpoint."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "86a659bf60f0"
"id": "3b768614e7c6"
},
"outputs": [],
"source": [
"! curl -X GET \\\n",
" -H \"Authorization: Bearer $(gcloud auth print-access-token)\" \\\n",
" -H \"Content-Type: application/json\" \\\n",
"https://{REGION}-aiplatform.googleapis.com/v1/projects/759209241365/locations/us-central1/endpoints/2259566763823857664"
"print(endpoint_icn.list_models())\n",
"print(endpoint_use.list_models())"
]
},
{
@@ -1557,21 +1530,19 @@
"source": [
"#### Delete the `DeploymentResourcePool`\n",
"\n",
"The method 'delete()' will delete your deployment resource pool."
"The method 'delete_deployment_resource_pool()' will delete your deployment resource pool."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "ac40cc1d594a"
"id": "b76a4de1e57e"
},
"outputs": [],
"source": [
"! curl -X DELETE \\\n",
"-H \"Authorization: Bearer $(gcloud auth print-access-token)\" \\\n",
"-H \"Content-Type: application/json\" \\\n",
"https://{REGION}-aiplatform.googleapis.com/v1beta1/projects/{PROJECT_ID}/locations/{REGION}/deploymentResourcePools/{DEPLOYMENT_RESOURCE_POOL_ID}"
"response = pool_client.delete_deployment_resource_pool(name=deployment_pool_id)\n",
"print(response)"
]
},
{
-113
View File
@@ -1,113 +0,0 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "82b522b9e130"
},
"outputs": [],
"source": [
"import time"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "7dc682542238"
},
"outputs": [],
"source": [
"# 5 mins\n",
"time.sleep(5 * 60)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "3ab331234b42"
},
"outputs": [],
"source": [
"print(\"5 minutes\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "2a5ea0ae33f0"
},
"outputs": [],
"source": [
"time.sleep(10 * 60)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "ab9a3894d60e"
},
"outputs": [],
"source": [
"print(\"15 minutes\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "d9efa8e9b70a"
},
"outputs": [],
"source": [
"time.sleep(5 * 60)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "bc63b3db2e71"
},
"outputs": [],
"source": [
"print(\"20 minutes\")"
]
}
],
"metadata": {
"colab": {
"name": "test.ipynb",
"toc_visible": true
},
"environment": {
"kernel": "python3",
"name": "common-cpu.m95",
"type": "gcloud",
"uri": "gcr.io/deeplearning-platform-release/base-cpu:m95"
},
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.12"
}
},
"nbformat": 4,
"nbformat_minor": 4
}