{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Quickstart\n",
    "\n",
    "## Using the Geoscience Object API Client\n",
    "\n",
    "Using the notebook utilities provided by `evo-objects` you can easily interact with the Geoscience Object API in a Jupyter notebook environment."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from evo.notebooks import ServiceManagerWidget\n",
    "\n",
    "manager = await ServiceManagerWidget.with_auth_code(\n",
    "    client_id=\"your-client-id\", cache_location=\"./notebook-data\"\n",
    ").login()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## ObjectAPIClient\n",
    "\n",
    "The `ObjectAPIClient` wraps endpoint functionality to provide a cohesive interface to the underlying API client implementation."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from evo.objects import ObjectAPIClient\n",
    "\n",
    "object_client = ObjectAPIClient.from_context(manager)\n",
    "service_health = await object_client.get_service_health()\n",
    "\n",
    "print(f\"Object API is {service_health.status.name.lower()}\")\n",
    "\n",
    "# The data client is an optional utility that provides helpers for uploading and downloading\n",
    "# parquet data via pyarrow.Table objects\n",
    "data_client = object_client.get_data_client(manager.cache)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Listing objects\n",
    "\n",
    "Listing a subset of objects is simple, just call the `ObjectAPIClient.list_objects()` method."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "offset = 0\n",
    "while True:\n",
    "    page = await object_client.list_objects(offset=offset, limit=10)\n",
    "    if offset == 0:\n",
    "        print(f\"Found {page.total} object{'' if page.total == 1 else 's'}\")\n",
    "    for object in page:\n",
    "        print(f\"{object.path}: <{object.schema_id}> ({object.id})\")\n",
    "\n",
    "    if page.is_last:\n",
    "        break\n",
    "    else:\n",
    "        offset = page.next_offset"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You can also list all objects. Internally, this recursively calls the `list_objects()` method until all objects are fetched."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from evo.objects import ObjectMetadata\n",
    "\n",
    "all_objects = await object_client.list_all_objects(limit_per_request=50)\n",
    "\n",
    "\n",
    "# Pretty print all objects\n",
    "def display_objects(objects: list[ObjectMetadata]):\n",
    "    n_objects = len(objects)\n",
    "    print(f\"Found {n_objects} object{'' if n_objects == 1 else 's'}\")\n",
    "    for object in objects:\n",
    "        print(f\"{object.path}: <{object.schema_id}> ({object.id})\")\n",
    "\n",
    "\n",
    "display_objects(all_objects)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Create a pointset object"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pyarrow as pa\n",
    "import pyarrow.compute as pc\n",
    "import pyarrow.csv as csv\n",
    "\n",
    "# Import points from CSV\n",
    "imported_points = csv.read_csv(\n",
    "    \"data/topo.csv\",\n",
    "    parse_options=csv.ParseOptions(delimiter=\",\"),\n",
    "    convert_options=csv.ConvertOptions(\n",
    "        column_types={\n",
    "            \"x\": pa.float64(),\n",
    "            \"y\": pa.float64(),\n",
    "            \"z\": pa.float64(),\n",
    "        }\n",
    "    ),\n",
    ")\n",
    "\n",
    "# Extract bounding box coordinates\n",
    "min_x, max_x = pc.min_max(imported_points[0]).values()\n",
    "min_y, max_y = pc.min_max(imported_points[1]).values()\n",
    "min_z, max_z = pc.min_max(imported_points[2]).values()\n",
    "\n",
    "sample_pointset = {\n",
    "    \"name\": \"Sample pointset\",\n",
    "    \"uuid\": None,\n",
    "    \"bounding_box\": {\n",
    "        \"min_x\": min_x.as_py(),\n",
    "        \"min_y\": min_y.as_py(),\n",
    "        \"min_z\": min_z.as_py(),\n",
    "        \"max_x\": max_x.as_py(),\n",
    "        \"max_y\": max_y.as_py(),\n",
    "        \"max_z\": max_z.as_py(),\n",
    "    },\n",
    "    \"coordinate_reference_system\": {\"epsg_code\": 2048},\n",
    "    \"locations\": {\n",
    "        # Use the data client to save the pyarrow Table as parquet data\n",
    "        \"coordinates\": data_client.save_table(imported_points),\n",
    "    },\n",
    "    \"schema\": \"/objects/pointset/1.0.1/pointset.schema.json\",\n",
    "}\n",
    "\n",
    "print(sample_pointset)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Upload a new pointset object"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from evo.notebooks import FeedbackWidget\n",
    "\n",
    "# Use the data client to upload all data referenced by the pointset\n",
    "await data_client.upload_referenced_data(sample_pointset, fb=FeedbackWidget(\"Uploading data\"))\n",
    "\n",
    "# Use the service client to publish the pointset\n",
    "new_pointset_metadata = await object_client.create_geoscience_object(\"sdk/v2/sample-pointset.json\", sample_pointset)\n",
    "\n",
    "# The service responds with creation metadata\n",
    "print(f\"{new_pointset_metadata.path}: <{new_pointset_metadata.schema_id}> ({new_pointset_metadata.id})\")\n",
    "print(f\"\\tCreated at: {new_pointset_metadata.created_at}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Update an existing pointset object"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(f\"Sample pointset has UUID: {sample_pointset['uuid']}\")\n",
    "\n",
    "updated_pointset_metadata = await object_client.update_geoscience_object(sample_pointset)\n",
    "\n",
    "# The service responds with creation metadata\n",
    "print(f\"{updated_pointset_metadata.path}: <{updated_pointset_metadata.schema_id}>\")\n",
    "print(f\"\\tCreated at: {updated_pointset_metadata.created_at}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### List versions"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from evo.objects import ObjectVersion\n",
    "\n",
    "all_versions = await object_client.list_versions_by_path(\"sdk/v2/sample-pointset.json\")\n",
    "\n",
    "\n",
    "def display_versions(versions: list[ObjectVersion]) -> None:\n",
    "    n_versions = len(versions)\n",
    "    print(f\"Found {n_versions} version{'' if n_versions == 1 else 's'}\")\n",
    "    for version in sorted(versions, key=lambda v: v.created_at):\n",
    "        print(\n",
    "            f\"version:\\t({version.version_id})\"\n",
    "            f\" created on {version.created_at.date().isoformat()}\"\n",
    "            f\" at {version.created_at.time().isoformat('seconds')}\"\n",
    "            f\" by {version.created_by.name}\"\n",
    "        )\n",
    "\n",
    "\n",
    "display_versions(all_versions)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Download a pointset object"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from evo.notebooks import FeedbackWidget\n",
    "\n",
    "downloaded_object = await object_client.download_object_by_path(\"sdk/v2/sample-pointset.json\")\n",
    "metadata = downloaded_object.metadata\n",
    "\n",
    "if metadata.created_by is not None and metadata.created_by.name is not None:\n",
    "    accreditation = f\"{metadata.created_by.name}\"\n",
    "else:\n",
    "    accreditation = \"an unknown user\"\n",
    "created_at_str = metadata.created_at.astimezone().strftime(\"on %Y-%m-%d at %H:%M:%S\")\n",
    "print(f\"{metadata.path} :: uploaded by {accreditation} {created_at_str}\")\n",
    "\n",
    "# Downloaded object supports JMESPath queries for more expressive access to JSON data.\n",
    "print(\n",
    "    downloaded_object.search(  # Project only a few fields for display\n",
    "        \"\"\"\n",
    "        {\n",
    "            name: name,\n",
    "            uuid: uuid,\n",
    "            schema: schema,\n",
    "            coordinate_reference_system: coordinate_reference_system,\n",
    "            bounding_box: {\n",
    "                min: [bounding_box.min_x, bounding_box.min_y, bounding_box.min_z],\n",
    "                max: [bounding_box.max_x, bounding_box.max_y, bounding_box.max_z]\n",
    "            }\n",
    "        }\n",
    "        \"\"\"\n",
    "    )\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from evo.objects import DownloadedObject, ObjectReference\n",
    "\n",
    "# If you already know the object you want, you don't even need the object client\n",
    "ref = ObjectReference.new(\n",
    "    environment=manager.get_environment(),\n",
    "    object_path=\"sdk/v2/sample-pointset.json\",\n",
    "    version_id=metadata.version_id,  # The version ID is optional\n",
    ")\n",
    "print(\"ObjectReference URL:\", ref)  # An object reference can also be a string in the format printed here.\n",
    "downloaded_object = await DownloadedObject.from_reference(\n",
    "    connector=manager.get_connector(),\n",
    "    reference=ref,\n",
    "    cache=manager.cache,\n",
    ")\n",
    "downloaded_object.search(\"@\")  # Pretty-print the entire object via a JMESPath proxy object"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Download parquet data from a pointset object"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "table_info = downloaded_object.search(\"locations.coordinates\")  # Use a JMESPath expression to find the table info\n",
    "\n",
    "# Download parquet data by table info reference\n",
    "downloaded_data = await downloaded_object.download_table(table_info, fb=FeedbackWidget(\"Downloading pyarrow.Table\"))\n",
    "\n",
    "# OR you can just use the JMESPath expression in the download_table call directly\n",
    "downloaded_data = await downloaded_object.download_table(\"locations.coordinates\")\n",
    "print(downloaded_data)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# A similar interface can be used to download as a pandas DataFrame\n",
    "await downloaded_object.download_dataframe(\"locations.coordinates\", fb=FeedbackWidget(\"Downloading pandas.DataFrame\"))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# And as a NumPy array\n",
    "await downloaded_object.download_array(\"locations.coordinates\", fb=FeedbackWidget(\"Downloading numpy.ndarray\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Modify an object"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Use the as_dict() to get the dictionary representation of the object, which can be modified\n",
    "object_dict = downloaded_object.as_dict()\n",
    "object_dict[\"description\"] = \"A pointset\"\n",
    "\n",
    "# Use the update() method to update the object on the Geoscience Object Service.\n",
    "# By default, this prevents accidentally overwriting any updates that happened since the object was downloaded. by\n",
    "# checking that the version of the downloaded object is in fact the lastest version.\n",
    "new_downloaded_object = await downloaded_object.update(object_dict, check_for_conflict=True)\n",
    "print(new_downloaded_object.metadata)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Get latest object versions"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Requires running the list objects from above to get the list of items\n",
    "object_ids = [o.id for o in all_objects]\n",
    "versions = await object_client.get_latest_object_versions(object_ids)\n",
    "for uuid, version_id in versions.items():\n",
    "    print(f\"{uuid} -> {version_id}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### List stages"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "stages = await object_client.list_stages()\n",
    "\n",
    "for stage in stages:\n",
    "    print(f\"{stage.name} ({stage.id})\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Set object stage"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "downloaded_object = await object_client.download_object_by_path(\"sdk/v2/sample-pointset.json\")\n",
    "metadata = downloaded_object.metadata\n",
    "print(f\"Current stage: {metadata}\")\n",
    "\n",
    "await object_client.set_stage(metadata.id, version_id=metadata.version_id, stage_id=stages[1].id)\n",
    "\n",
    "updated_metadata = await object_client.download_object_by_id(metadata.id)\n",
    "print(f\"Updated stage: {updated_metadata.metadata.stage}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## API documentation\n",
    "\n",
    "For more information about the Geoscience Object API, visit the [Seequent developer portal](https://developer.seequent.com/docs/guides/objects)."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "evo-python-sdk",
   "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.18"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
