Skip to content

Instantly share code, notes, and snippets.

@migurski
Last active February 21, 2024 00:43
Show Gist options
  • Save migurski/bc1c5f518f42666801b1973a71703318 to your computer and use it in GitHub Desktop.
Save migurski/bc1c5f518f42666801b1973a71703318 to your computer and use it in GitHub Desktop.
Felt API + Jupyter Notebook
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "markdown",
"id": "68c116e4-a98a-4899-8f98-1b0c8eee01e8",
"metadata": {},
"source": [
"# Felt API + Jupyter Notebook\n",
"\n",
"👉 [Resulting map at Felt.com](https://felt.com/map/jupyter-felt-FeltMap-r1FSyoI6TwigEYqKPQ7DrC?loc=43.11,-58.64,3.52z)\n",
"- See `felt_jupyter.py` for API code driving this example.\n",
"- [Getting Started With The Felt API](https://feltmaps.notion.site/Getting-Started-With-The-Felt-API-69c8b02b7d8e436daa657a04a2dbaffa)\n",
"- [Felt Public API reference](https://feltmaps.notion.site/Felt-Public-API-reference-c01e0e6b0d954a678c608131b894e8e1)\n",
"- [Personal Access Token authorization](https://feltmaps.notion.site/Felt-Public-API-reference-c01e0e6b0d954a678c608131b894e8e1#065791134e0c4d82b156d97db3f663a5)\n"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "a73860f2-8e1b-44fd-9bf9-f248ec78e562",
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>Name</th>\n",
" <th>Lat</th>\n",
" <th>Lon</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>Oakland</td>\n",
" <td>37.81</td>\n",
" <td>-122.28</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>New York</td>\n",
" <td>40.72</td>\n",
" <td>-74.03</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>Chicago</td>\n",
" <td>41.89</td>\n",
" <td>-87.63</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>Los Angeles</td>\n",
" <td>34.06</td>\n",
" <td>-118.26</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td>London</td>\n",
" <td>51.00</td>\n",
" <td>0.00</td>\n",
" </tr>\n",
" <tr>\n",
" <th>5</th>\n",
" <td>Paris</td>\n",
" <td>48.00</td>\n",
" <td>5.00</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" Name Lat Lon\n",
"0 Oakland 37.81 -122.28\n",
"1 New York 40.72 -74.03\n",
"2 Chicago 41.89 -87.63\n",
"3 Los Angeles 34.06 -118.26\n",
"4 London 51.00 0.00\n",
"5 Paris 48.00 5.00"
]
},
"execution_count": 2,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"import pandas\n",
"\n",
"df = pandas.DataFrame(\n",
" [\n",
" [\"Oakland\", 37.81, -122.28],\n",
" [\"New York\", 40.72, -74.03],\n",
" [\"Chicago\", 41.89, -87.63],\n",
" [\"Los Angeles\", 34.06, -118.26],\n",
" [\"London\", 51, 0],\n",
" [\"Paris\", 48, 5],\n",
" ],\n",
" columns=[\"Name\", \"Lat\", \"Lon\"],\n",
")\n",
"\n",
"df"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "eeec56c7-0c3e-4f09-b95d-ea2363d56a1f",
"metadata": {},
"outputs": [],
"source": [
"import importlib\n",
"jupyter_felt = importlib.import_module('jupyter_felt', 'felt_jupyter.py')\n",
"\n",
"TOKEN = \"felt_pat_******\"\n",
"\n",
"fmap = jupyter_felt.FeltMap(TOKEN)"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "a019845d-6689-4bef-be17-2533af6fc9d6",
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"<progress style='width:60ex' max='4' value='4'></progress>"
],
"text/plain": [
"[============================================================] 4/4"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/plain": [
"'3yQO5NSISluI9Byjxzu7cKC'"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"fmap.add_layer(df)"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "a28d8824-d005-48e8-bd51-890f278328ef",
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"<iframe\n",
" width=\"99%\"\n",
" height=\"450px\"\n",
" style=\"border: 1px solid #415125; border-radius: 8px\"\n",
" title=\"Felt Map\"\n",
" src=\"https://felt.com/embed/map/jupyter-felt-FeltMap-r1FSyoI6TwigEYqKPQ7DrC\"\n",
" referrerpolicy=\"strict-origin-when-cross-origin\"\n",
" ></iframe>"
],
"text/plain": [
"<IPython.core.display.HTML object>"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"fmap.show()"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"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.8"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
import json
import os.path
import tempfile
import urllib.parse
import IPython.display
import pandas
import requests
API_BASE = "https://felt.com/api/v1/"
MAPS_URL = urllib.parse.urljoin(API_BASE, "maps")
LAYERS_TPL = urllib.parse.urljoin(API_BASE, "maps/{map_id}/layers")
FINISH_TPL = urllib.parse.urljoin(
API_BASE, "maps/{map_id}/layers/{layer_id}/finish_upload"
)
class FeltMap:
token: str
map_id: str
map_url: str
embed_url: str
http_headers: dict[str, str]
def __init__(self, api_token):
self.token = api_token
self.http_headers = {
"Authorization": f"Bearer {self.token}",
"Content-Type": "application/json",
}
# Create a new Felt map
resp = requests.post(
MAPS_URL,
headers=self.http_headers,
data=json.dumps({"title": "jupyter_felt.FeltMap"}),
)
assert resp.status_code in range(200, 299)
self.map_id = resp.json()["data"]["id"]
self.map_url = resp.json()["data"]["attributes"]["url"]
self.embed_url = self.map_url.replace("/map/", "/embed/map/")
def add_layer(self, df: pandas.DataFrame) -> str:
"""Upload DataFrame CSV to Felt Map and return its layer_id"""
filename = "dataframe.csv"
progress = IPython.display.ProgressBar(4)
progress.display()
next(progress)
# Ask Felt API to allow a file upload
resp1 = requests.post(
LAYERS_TPL.format(map_id=self.map_id),
headers=self.http_headers,
json={"file_names": [filename], "name": "DataFrame"},
)
assert resp1.status_code in range(200, 299)
next(progress)
layer_id = resp1.json()["data"]["attributes"]["layer_id"]
post_url = resp1.json()["data"]["attributes"]["url"]
attributes = resp1.json()["data"]["attributes"]["presigned_attributes"]
# Upload file content using Felt's presigned POST attributes
with tempfile.TemporaryDirectory() as tempdir:
filepath = os.path.join(tempdir, filename)
df.to_csv(filepath)
with open(filepath, "rb") as file:
resp2 = requests.post(post_url, files={**attributes, "file": file})
assert resp2.status_code in range(200, 299)
next(progress)
# Notify Felt that file content upload is done
resp3 = requests.post(
FINISH_TPL.format(map_id=self.map_id, layer_id=layer_id),
headers=self.http_headers,
json={"filename": filename},
)
assert resp3.status_code in range(200, 299)
try:
next(progress)
except StopIteration:
pass
return layer_id
def show(self) -> IPython.display.HTML:
"""Return HTML object for displaying map in IPython"""
return IPython.display.HTML(
f"""<iframe
width="99%"
height="450px"
style="border: 1px solid #415125; border-radius: 8px"
title="Felt Map"
src="{self.embed_url}"
referrerpolicy="strict-origin-when-cross-origin"
></iframe>"""
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment