{
 "cells": [
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Python Sample For Fabricator Steps\n",
    "\n",
    "### Pre-requisites"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# You may need to run this on Jupyter\n",
    "# !pip install requests\n",
    "# !pip install python-dotenv"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1. API Server URLs and Auth Variables"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from dotenv import dotenv_values\n",
    "config = dotenv_values(\".env\")\n",
    "\n",
    "# The auth server is used for authentication and account management\n",
    "authApiUrl = config['BIGSCREEN_API_URL']\n",
    "\n",
    "# The admin server has the fabricator endpoints\n",
    "adminApiUrl = config['BIGSCREEN_ADMIN_API_URL']\n",
    "\n",
    "# Each http request needs a bearer token. This is a revokable token which is assigned to each external application that\n",
    "# uses the Bigscreen APIs (e.g. this script is an external application).\n",
    "apiBearerToken = config['BIGSCREEN_API_KEY']\n",
    "\n",
    "# Finally, we need an account.  The account is necessary for us to identify who is using the various APIs. The account \n",
    "# must have access to the \"Fabricator\" endpoints to work.\n",
    "email = config['FABRICATOR_ACCOUNT_EMAIL']\n",
    "password = config['FABRICATOR_ACCOUNT_PASSWORD']\n"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. Get auth tokens from the auth server\n",
    "\n",
    "To be able to talk to our APIs, we need two additional tokens, in addition to the \"apiBearerToken\" above.  These are known as **refresh** and **access** tokens, respectively.\n",
    "* The **access token** is required to access all the server APIs. However, it only last for 15 minutes.\n",
    "* The **refresh token** is required to refresh the access token. The refresh token is long lasting (e.g. several weeks). It can be written to disk and re-used across different sessions.\n",
    "\n",
    "Note the neither token can be shared across IPs or devices."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# You may need to run this on Jupyter\n",
    "# !pip install requests\n",
    "\n",
    "import requests\n",
    "\n",
    "headers = {\n",
    "    \"Content-Type\": \"application/json\",\n",
    "    \"Authorization\": f\"Bearer {apiBearerToken}\"\n",
    "}\n",
    "\n",
    "payload = {\n",
    "    \"email\": config['FABRICATOR_ACCOUNT_EMAIL'],\n",
    "    \"password\": config['FABRICATOR_ACCOUNT_PASSWORD']\n",
    "}\n",
    "\n",
    "r = requests.post(f\"{authApiUrl}/auth/login\", headers=headers, json=payload)\n",
    "print(r.status_code)\n",
    "refreshToken = r.headers[\"x-refresh-token\"]\n",
    "accessToken = r.headers[\"x-access-token\"]"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3. Verify the access token\n",
    "\n",
    "Verify the access token. This should return an HTTP status code of 200, if the api keys, tokens, and urls, are correct."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def getAdminHeaders():\n",
    "    return {\n",
    "        \"Content-Type\": \"application/json\",\n",
    "        \"Authorization\": f\"Bearer {apiBearerToken}\",\n",
    "        \"x-access-token\": accessToken\n",
    "    }\n",
    "    \n",
    "r = requests.get(f\"{authApiUrl}/auth/verify\", headers=getAdminHeaders())\n",
    "r.status_code"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3a. Renew the access token\n",
    "\n",
    "Call this function to check the access token state and renew it if it has expired.\n",
    "\n",
    "(All HTTP endpoints will return a 401 and a nonce that can be used to renew the access token.)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import json\n",
    "import base64\n",
    "\n",
    "def checkAccessToken():\n",
    "    r = requests.get(f\"{authApiUrl}/verify\", headers=getAdminHeaders())\n",
    "    if (r.status_code == 401):\n",
    "        nonce = r.headers[\"x-bigscreen-nonce\"]\n",
    "        code = r.json()[\"code\"]\n",
    "\n",
    "        systemInfo = {\n",
    "            \"deviceUniqueIdentifier\": \"python notebook\",\n",
    "            \"version\": \"001\",\n",
    "            \"deviceName\": \"python notebook\",\n",
    "            \"deviceModel\": \"python notebook\",\n",
    "            \"operatingSystem\": \"whatever\"\n",
    "        }\n",
    "\n",
    "        headers = {\n",
    "            \"Content-Type\": \"application/json\",\n",
    "            \"Authorization\": f\"Bearer {apiBearerToken}\",\n",
    "            \"x-refresh-token\": refreshToken,\n",
    "            \"x-bigscreen-nonce\": nonce,\n",
    "            \"x-bigscreen-system-info\": base64.b64encode(bytes(json.dumps(systemInfo), \"utf-8\"))\n",
    "        }\n",
    "\n",
    "        renew = requests.get(f\"{authApiUrl}/renew\", headers=headers)\n",
    "        if (renew.status_code == 200):\n",
    "            accessToken = renew.headers[\"x-access-token\"]\n",
    "            print(\"Renewed the access token!\")\n",
    "    elif (r.status_code == 200):\n",
    "        print(\"Access token ok.\")\n",
    "\n",
    "checkAccessToken()"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4. Using the Fabricator API\n",
    "\n",
    "We can now start using the admin/fabricator endpoints.\n",
    "\n",
    "First, we get the schemas so we can use them in future requests.  The schemas define the various actions and states of a **job**, which is the main data structure the fabricator API uses to manage the cushion production process."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from enum import Enum\n",
    "\n",
    "r = requests.get(f\"{adminApiUrl}/admin/fabricator/schemas\", headers=getAdminHeaders())\n",
    "assert r.status_code == 200, f\"status code should be 200 (actual: {r.status_code})\"\n",
    "\n",
    "schemas = r.json()\n",
    "JobStates = Enum(\"JobStates\", dict([(v, int(k)) for k, v in schemas[\"JobStates\"].items()]))\n",
    "JobActions = Enum(\"JobActions\", dict([(v, int(k)) for k, v in schemas[\"JobActions\"].items()]))\n",
    "\n",
    "JobActions, JobStates"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5. Import test data to create a job\n",
    "\n",
    "(This step is necessary for the purposes of this test.)\n",
    "\n",
    "As mentioned previously, the fabricator system operates primarily on a data structure known as a **job**. A job is the main data structure the fabricator API uses to manage the cushion production process.  A job represents a single cushion from the initial Shopify order right through to fulfillment and shipping.\n",
    "\n",
    "Internally, the fabricator system will automatically create jobs for each order in Shopify.  However, this test does not have access to Shopify, so we need to insert some test data here."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# TODO: this step is obsolete.\n",
    "#\n",
    "# Job creation now requires a valid scan request first."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5a. Get the job\n",
    "\n",
    "Using the job id from the previous, get the attached job just to make sure everything is working."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "r = requests.get(f\"{adminApiUrl}/admin/fabricator/job/{jobId}\", headers=getAdminHeaders())\n",
    "\n",
    "assert r.status_code == 200, f\"status code should be 200 (actual: {r.status_code})\"\n",
    "\n",
    "data = r.json()\n",
    "print(data)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5d. (Optional) Download the scan file\n",
    "\n",
    "For test purposes, you can download the scan file that the customer uploaded to topology."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "r = requests.get(f\"{adminApiUrl}/admin/fabricator/job/{jobId}/scan\", headers=getAdminHeaders())\n",
    "assert r.status_code == 200, f\"status code should be 200 (actual: {r.status_code})\"\n",
    "\n",
    "# stream the requests output to a file\n",
    "with open(\"scan.zip\", \"wb\") as f:\n",
    "    r.raw.decode_content = True\n",
    "    f.write(r.content)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5e. Verify the scan and get it ready for Fusion 360"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "payload = { \"action\": JobActions.TryScanVerification.value }\n",
    "r = requests.put(f\"{adminApiUrl}/admin/fabricator/job/{jobId}\", headers=getAdminHeaders(), json=payload)\n",
    "assert r.status_code == 200, f\"status code should be 200 (actual: {r.status_code} {r.text})\""
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If there is a scan available (and there should be if you've followed all the previous steps), the job should be ready for the next step:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "r = requests.get(f\"{adminApiUrl}/admin/fabricator/job/{jobId}\", headers=getAdminHeaders())\n",
    "assert r.json()[\"jobState\"] == JobStates.AwaitingToolPathCreation.value, f\"job state should be {JobStates.AwaitingToolPathCreation.value} (actual: {r.json()['jobState']})\""
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5f. (Optional) Download the Preprocessed Mesh File"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "r = requests.get(f\"{adminApiUrl}/admin/fabricator/job/{jobId}/mesh\", headers=getAdminHeaders())\n",
    "assert r.status_code == 200, f\"status code should be 200 (actual: {r.status_code})\"\n",
    "\n",
    "# stream the requests output to a file\n",
    "with open(\"mesh.stl\", \"wb\") as f:\n",
    "    r.raw.decode_content = True\n",
    "    f.write(r.content)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 6. Fusion 360"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6a. (Option 1) Fusion 360 APIs\n",
    "\n",
    "The fusion 360 machine uses a separate API, and thus requires a separate API key.\n",
    "\n",
    "This API key does not have access to the job itself, or any customer data associated with the job.  Instead it pulls down a \"manifest\", which includes the scan data that was uploading by the customer.  It can use the id of this manifest to upload the tool path file it generates from the scan.\n",
    "\n",
    "(Note: currently, the fusion 360 process is currently irreversible.)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from urllib3 import encode_multipart_formdata\n",
    "\n",
    "fusion360ApiKey = \"Bz68CUVkZsDmjJksRBdSzzUFYQouCp43x0Zx6hwOWw183oJ8RUXSrSjcjsHHcoHt\"\n",
    "\n",
    "headers = {\n",
    "    \"Content-Type\": \"application/json\",\n",
    "    \"Authorization\": f\"Bearer {fusion360ApiKey}\"\n",
    "}\n",
    "\n",
    "r = requests.get(f\"{adminApiUrl}/fusion/next\", headers=headers)\n",
    "assert r.status_code == 200, f\"status code should be 200 (actual: {r.status_code})\"\n",
    "\n",
    "manifest = r.json()\n",
    "manifestId = manifest[\"id\"]\n",
    "meshData = manifest[\"meshData\"]\n",
    "\n",
    "# sleep for one second\n",
    "import time\n",
    "\n",
    "time.sleep(1)\n",
    "\n",
    "#-------------------------------------------------------\n",
    "# RUN THE FUSION 360 PROCESS FOR GENERATING THE TOOLPATH\n",
    "#-------------------------------------------------------\n",
    "\n",
    "(content, header) = encode_multipart_formdata({\n",
    "    \"tool-path\": (\"tool_path_example\", open(\"../tests/fabricator/example_tool_path.nc\", \"rb\").read())\n",
    "})\n",
    "\n",
    "headers[\"Content-Type\"]=header\n",
    "\n",
    "r = requests.put(f\"{adminApiUrl}/fusion/{manifestId}\", headers=headers, data=content)\n",
    "\n",
    "assert r.status_code == 200, f\"status code should be 200 (actual: {r.status_code})\"\n",
    "r.status_code"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6a. (Option 2) Prepare the toolpath with the admin API with debug functions\n",
    "\n",
    "As an alternative to the Fusion360 specific endpoints above, the admin API also has endpoints to prepare a toolpath for a specific job."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from urllib3 import encode_multipart_formdata\n",
    "r = requests.get(f\"{adminApiUrl}/admin/fabricator/job/{jobId}/debug_start_toolpath\", headers=getAdminHeaders())\n",
    "\n",
    "(content, header) = encode_multipart_formdata({\n",
    "    \"tool-path\": (\"tool_path_example\", open(\"../tests/fabricator/example_tool_path.nc\", \"rb\").read())\n",
    "})\n",
    "\n",
    "headers = getAdminHeaders()\n",
    "headers[\"Content-Type\"]=header\n",
    "print(headers)\n",
    "r = requests.put(f\"{adminApiUrl}/admin/fabricator/job/{jobId}/debug_upload_toolpath\", headers=headers, data=content)\n",
    "assert r.status_code == 200, f\"status code should be 200 (actual: {r.status_code})\"\n",
    "r.status_code"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 6b. Reset the Tool Path\n",
    "\n",
    "If you want to rerun the fusion sequence, you can reset the job's tool path, and return to the \"AwaitingToolPathCreation\" state."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "payload = { \"action\": JobActions.ResetToolPath.value }\n",
    "r = requests.put(f\"{adminApiUrl}/admin/fabricator/job/{jobId}\", headers=getAdminHeaders(), json=payload)\n",
    "assert r.status_code == 200, f\"status code should be 200 (actual: {r.status_code} {r.text})\"\n",
    "\n",
    "r = requests.get(f\"{adminApiUrl}/admin/fabricator/job/{jobId}\", headers=getAdminHeaders())\n",
    "assert r.json()[\"jobState\"] == JobStates.AwaitingToolPathCreation.value, f\"job state should be {JobStates.AwaitingToolPathCreation.value} (actual: {r.json()['jobState']})\""
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 7. WaxId assignment\n",
    "\n",
    "The WaxId is assigned when the fusion 360 process uploads the scan back to the manifest.\n",
    "\n",
    "## 7a. Visualize the WaxId"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "r = requests.get(f\"{adminApiUrl}/admin/fabricator/job/{jobId}\", headers=getAdminHeaders())\n",
    "assert r.json()[\"jobState\"] == JobStates.AwaitingManufacture.value, f\"job state should be {JobStates.AwaitingManufacture.value} (actual: {r.json()['jobState']})\"\n",
    "\n",
    "data = r.json()\n",
    "waxId = data[\"waxId\"]\n",
    "bits = data[\"toolPathBitFieldSnapshot\"][\"bits\"]\n",
    "\n",
    "echoBit = lambda bit : \"⚫\" if bit == \"1\" else \"🔵\"\n",
    "print(\" \".join(map(echoBit, bits[0 : 5])),  \"   \", \" \".join(map(echoBit, bits[10 : 15])))\n",
    "print(\" \".join(map(echoBit, bits[5 : 10])), \"   \", \" \".join(map(echoBit, bits[15 : 20])))"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 8. Retrieving jobs with a general query\n",
    "\n",
    "POST a query to the jobs endpoint.  The full schema of things you can query:\n",
    "\n",
    "```\n",
    "type FindJobParams = {\n",
    "    orderSnapshotId?: string,\n",
    "    shopifyOrderId?: number,\n",
    "    bigscreenAccountId?: string,\n",
    "    jobState?: FabricatorSchemas.JobState,\n",
    "    ipd?: number,\n",
    "    minIpd?: number,\n",
    "    maxIpd?: number,\n",
    "    waxId?: string\n",
    "}\n",
    "```\n",
    "\n",
    "Returns:\n",
    "```\n",
    "{\n",
    "    jobs: [Array of jobs...]\n",
    "}\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Query via a job state\n",
    "query = {\n",
    "    \"jobState\": JobStates.ManufacturingInProgress.value\n",
    "}\n",
    "\n",
    "r = requests.post(f\"{adminApiUrl}/admin/fabricator/jobs\", headers=getAdminHeaders(), json=query)\n",
    "assert r.status_code == 200, f\"status code should be 200 (actual: {r.status_code})\"\n",
    "\n",
    "r.json()"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 9. Manufacturing the ToolPath with the CNC machine\n",
    "\n",
    "The raspberry Pi attached to each CNC machine can download a tool path from a job anonymously.  This characteristic is similar to the fusion 360 machine, in that the CNC machine can operate on the job data without any other information that identifies the job.\n",
    "\n",
    "The raspberry Pi endpoints require a user with a special `CncMachine` access policy attached, so we have to login with that user before we can use those endpoints."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import requests\n",
    "\n",
    "# CNC machine account\n",
    "payload = {\n",
    "    \"email\": \"qa+cnc@bigscreenvr.com\",\n",
    "    \"password\": \"TQDnMGe2tod26B2JgeWBi3W86oPkph9H\",\n",
    "}\n",
    "\n",
    "headers = {\n",
    "    \"Content-Type\": \"application/json\",\n",
    "    \"Authorization\": f\"Bearer {apiBearerToken}\"\n",
    "}\n",
    "\n",
    "cncUser = {\n",
    "    \"refreshToken\": \"\",\n",
    "    \"accessToken\": \"\"\n",
    "}\n",
    "\n",
    "r = requests.post(f\"{authApiUrl}/auth/login\", headers=headers, json=payload)\n",
    "assert r.status_code == 200, f\"status code should be 200 (actual: {r.status_code})\"\n",
    "cncUser[\"refreshToken\"] = r.headers[\"x-refresh-token\"]\n",
    "cncUser[\"accessToken\"] = r.headers[\"x-access-token\"]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Send a get request to `/admin/cnc` to check if the account can use the `cnc` endpoints.\n",
    "def getCncAdminHeaders():\n",
    "    return {\n",
    "        \"Content-Type\": \"application/json\",\n",
    "        \"Authorization\": f\"Bearer {apiBearerToken}\",\n",
    "        \"x-access-token\": cncUser[\"accessToken\"]\n",
    "    }\n",
    "\n",
    "r = requests.get(f\"{adminApiUrl}/admin/cnc\", headers=getCncAdminHeaders())\n",
    "assert r.status_code == 200, f\"status code should be 200 (actual: {r.status_code})\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# CNC endpoints will succeed with the fabricator user.\n",
    "r = requests.get(f\"{adminApiUrl}/cnc\", headers=getAdminHeaders())\n",
    "assert r.status_code == 200, f\"status code should be 200 (actual: {r.status_code})\""
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 9a. Calling `/cnc/next` from the raspberry pi to get the next job\n",
    "\n",
    "Now call `/cnc/next` to get the next available toolpath. Successfully doing this will put the job into the \"Manufacturing\" state.\n",
    "\n",
    "If this returns a 404 error, it means there are no jobs available to process."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "r = requests.get(f\"{adminApiUrl}/cnc\", headers=getCncAdminHeaders(), json=payload)\n",
    "assert r.status_code == 200, f\"status code should be 200 (actual: {r.status_code})\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "payload = {\n",
    "    \"metaData\": {\n",
    "        \"machineId\": \"python_notebook\",\n",
    "    }\n",
    "}\n",
    "\n",
    "r = requests.put(f\"{adminApiUrl}/cnc/next\", headers=getCncAdminHeaders(), json=payload)\n",
    "assert r.status_code == 200, f\"status code should be 200 (actual: {r.status_code})\"\n",
    "\n",
    "data = r.json()\n",
    "cncId = data[\"id\"]\n",
    "toolPathData = data[\"toolPathData\"]\n",
    "print(cncId)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 9b. Handling errors\n",
    "\n",
    "See `002.cncErrors.ipynb` for a notebook with an example of how to handle errors from the CNC machine."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 9c. Sending the \"Manufacturing Succeeded\" message\n",
    "\n",
    "Send this once the raspberry pi's attached CNC machine has completed the toolpath job."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "payload = {\n",
    "    \"action\" : JobActions.ManufacturingSucceeded.value,\n",
    "    \"metaData\": {\n",
    "        \"machineId\": \"python_notebook\"\n",
    "    }\n",
    "}\n",
    "\n",
    "r = requests.put(f\"{adminApiUrl}/cnc/{cncId}\", headers=getCncAdminHeaders(), json=payload)\n",
    "assert r.status_code == 200, f\"status code should be 200 (actual: {r.status_code})\""
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 10. Retrieving the Job using the WaxId (e.g. with CV)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# First, just query the original job to get a waxId we know exists.\n",
    "r = requests.get(f\"{adminApiUrl}/admin/fabricator/job/{jobId}\", headers=getAdminHeaders())\n",
    "assert r.json()[\"jobState\"] == JobStates.AwaitingNFC.value, f\"job state should be {JobStates.AwaitingNFC.value} (actual: {r.json()['jobState']})\"\n",
    "\n",
    "data = r.json()\n",
    "waxId = data[\"waxId\"]\n",
    "\n",
    "query = {\n",
    "    \"waxId\": waxId\n",
    "}\n",
    "\n",
    "r = requests.post(f\"{adminApiUrl}/admin/fabricator/jobs\", headers=getAdminHeaders(), json=query)\n",
    "assert r.status_code == 200, f\"status code should be 200 (actual: {r.status_code})\"\n",
    "\n",
    "assert len(r.json()[\"jobs\"]) == 1\n",
    "assert r.json()[\"jobs\"][0][\"id\"] == jobId"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 11. Assigning an NFC tag to the Job"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "r = requests.get(f\"{adminApiUrl}/admin/fabricator/job/{jobId}\", headers=getAdminHeaders())\n",
    "assert r.json()[\"jobState\"] == JobStates.AwaitingNFC.value, f\"job state should be {JobStates.AwaitingNFC.value} (actual: {r.json()['jobState']})\"\n",
    "\n",
    "# Generate a UUID\n",
    "import uuid\n",
    "nfcId = str(uuid.uuid4())\n",
    "\n",
    "payload = {\n",
    "    \"action\": JobActions.TryNFC.value,\n",
    "    \"nfcTagId\": f\"nfcId-{nfcId}\",\n",
    "    \"metaData\": {\n",
    "        \"machineId\": \"python_notebook\"\n",
    "    }\n",
    "}\n",
    "\n",
    "r = requests.put(f\"{adminApiUrl}/admin/fabricator/job/{jobId}\", headers=getAdminHeaders(), json=payload)\n",
    "assert r.status_code == 200, f\"status code should be 200 (actual: {r.status_code})\"\n",
    "\n",
    "r = requests.get(f\"{adminApiUrl}/admin/fabricator/job/{jobId}\", headers=getAdminHeaders())\n",
    "assert r.json()[\"jobState\"] == JobStates.AwaitingQA.value, f\"job state should be {JobStates.AwaitingQA.value} (actual: {r.json()['jobState']})\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 12. QA and uploading meta data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "payload = {\n",
    "    \"action\": JobActions.TryQA.value,\n",
    "    \"metaData\": {\n",
    "        \"machineId\": \"python_notebook\",\n",
    "        \"qaResult\": \"pass\"\n",
    "    }\n",
    "}\n",
    "\n",
    "r = requests.put(f\"{adminApiUrl}/admin/fabricator/job/{jobId}\", headers=getAdminHeaders(), json=payload)\n",
    "assert r.status_code == 200, f\"status code should be 200 (actual: {r.status_code})\"\n",
    "\n",
    "payload = {\n",
    "    \"action\": JobActions.QASuccess.value,\n",
    "    \"metaData\": {\n",
    "        \"machineId\": \"python_notebook\"\n",
    "    }\n",
    "}\n",
    "\n",
    "r = requests.put(f\"{adminApiUrl}/admin/fabricator/job/{jobId}\", headers=getAdminHeaders(), json=payload)\n",
    "assert r.status_code == 200, f\"status code should be 200 (actual: {r.status_code})\"\n",
    "\n",
    "r = requests.get(f\"{adminApiUrl}/admin/fabricator/job/{jobId}\", headers=getAdminHeaders())\n",
    "assert r.json()[\"jobState\"] == JobStates.AwaitingFulfilment.value, f\"job state should be {JobStates.AwaitingFulfilment.value} (actual: {r.json()['jobState']})\""
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 13. Fulfilment and completion"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "r = requests.get(f\"{adminApiUrl}/admin/fabricator/job/{jobId}\", headers=getAdminHeaders())\n",
    "assert r.json()[\"jobState\"] == JobStates.AwaitingFulfilment.value, f\"job state should be {JobStates.AwaitingFulfilment.value} (actual: {r.json()['jobState']})\""
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3.10.0 64-bit",
   "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.10.11"
  },
  "orig_nbformat": 4,
  "vscode": {
   "interpreter": {
    "hash": "369f2c481f4da34e4445cda3fffd2e751bd1c4d706f27375911949ba6bb62e1c"
   }
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
