https://documentation.spire.com/vessels-api/
curl -H "Authorization: Bearer $SPIRE_TOKEN" -H "Accept: application/json" 'https://api.sense.spire.com/messages?fields=decoded&limit=1'
curl -H "Authorization: Bearer $SPIRE_TOKEN" -H "Accept: application/json" 'https://api.sense.spire.com/messages??mmsi=311000912' # ODYSSEY OF THE SEAS
curl -H "Authorization: Bearer $SPIRE_TOKEN" -X GET "https://api.sense.spire.com/vessels?imo=9852676" # AMIS Queen
curl -H "Authorization: Bearer $SPIRE_TOKEN" -X GET "https://api.sense.spire.com/vessels?imo=9795737" # Oddysea of the seas
curl -H "Authorization: Bearer $SPIRE_TOKEN" -X GET "https://api.sense.spire.com/vessels?imo=9682875" # Harmony of the seas
curl -H "Authorization: Bearer $SPIRE_TOKEN" -X GET "https://api.sense.spire.com/vessels?imo=9893890" # EVER ACE
curl -H "Authorization: Bearer $SPIRE_TOKEN" -X GET "https://api.sense.spire.com/vessels?imo=9811000" # EVER GIVEN
curl -H "Authorization: Bearer $SPIRE_TOKEN" -X GET "https://api.sense.spire.com/vessels?imo=8019356" # NG EXPLORER
curl -H "Authorization: Bearer $SPIRE_TOKEN" -X GET "https://api.sense.spire.com/vessels?mmsi=311000912"
curl -H "Authorization: Bearer $SPIRE_TOKEN" -X GET "https://api.sense.spire.com/vessels?mmsi=311000396" # Harmony of the seas
curl -H "Authorization: Bearer $SPIRE_TOKEN" -X GET "https://api.sense.spire.com/vessels?mmsi=352986146" # EVER ACE
curl -H "Authorization: Bearer $SPIRE_TOKEN" -X GET "https://api.sense.spire.com/vessels?mmsi=353136000" # EVER GIVEN
curl -H "Authorization: Bearer $SPIRE_TOKEN" -X GET "https://api.sense.spire.com/vessels?mmsi=309336000" # NG EXPLORER
curl -H "Authorization: Bearer $SPIRE_TOKEN" -X GET "https://api.sense.spire.com/vessels/a5b738b4-faf0-4a7e-9a87-1c0ccfb123d2" # Harmony of the seas
curl -H "Authorization: Bearer $SPIRE_TOKEN" -X GET "https://api.sense.spire.com/vessels/350db884-9009-5b8d-9bec-aaa231f83e0b" # EVER ACE
curl -H "Authorization: Bearer $SPIRE_TOKEN" -X GET "https://api.sense.spire.com/vessels/871e3046-63a9-4f2e-9e32-1eda06e27d14" # EVER GIVEN
curl -H "Authorization: Bearer $SPIRE_TOKEN" -X GET "https://api.sense.spire.com/vessels/d6c4b271-3f89-4fbd-a24f-db8d7a5f07c6/positions/?timestamp_after=2022-01-01T00:00:00" # AMIS queen
curl -H "Authorization: Bearer $SPIRE_TOKEN" -X GET "https://api.sense.spire.com/vessels/a5b738b4-faf0-4a7e-9a87-1c0ccfb123d2/positions/?timestamp_after=2022-09-01T00:00:00" # Harmony of the seas
curl -H "Authorization: Bearer $SPIRE_TOKEN" -X GET "https://api.sense.spire.com/vessels/350db884-9009-5b8d-9bec-aaa231f83e0b/positions/?timestamp_after=2021-06-28T00:00:00" # Ever Ace
curl -H "Authorization: Bearer $SPIRE_TOKEN" -X GET "https://api.sense.spire.com/vessels/871e3046-63a9-4f2e-9e32-1eda06e27d14/positions/?timestamp_after=2018-12-06T00:00:00" # Ever Given
curl -H "Authorization: Bearer $SPIRE_TOKEN" -X GET https://api.sense.spire.com/vessels
curl -H "Authorization: Bearer $SPIRE_TOKEN" -X GET 'https://api.sense.spire.com/vessels/?ship_type=tanker'
curl -H "Authorization: Bearer $SPIRE_TOKEN" -X GET 'https://api.sense.spire.com/vessels/?ship_type=cargo'
curl -H "Authorization: Bearer $SPIRE_TOKEN" -X GET 'https://api.sense.spire.com/vessels/?ship_type=tanker&flag=IR&limit=1000'
curl -g -H "Authorization: Bearer $SPIRE_TOKEN" -X GET 'https://api.sense.spire.com/vessels/?ship_type=tanker&last_known_position_within=%7B%22type%22%3A%20%22Polygon%22%2C%22coordinates%22%3A%20%5B%5B%5B-5.9765625%2C51.31688050404585%5D%2C%5B12.12890625%2C51.31688050404585%5D%2C%5B12.12890625%2C61.39671887310411%5D%2C%5B-5.9765625%2C61.39671887310411%5D%2C%5B-5.9765625%2C51.31688050404585%5D%5D%5D%7D'
function get_vessels() {
local north=$1
local south=$2
local east=$3
local west=$4
local ship_type=$5
local polygon=$(cat <<EOF
{
"type": "Polygon",
"coordinates": [
[
[$west, $south],
[$east, $south],
[$east, $north],
[$west, $north],
[$west, $south]
]
]
}
EOF
)
local encoded_polygon=$(echo $polygon | jq -c . | jq -sRr @uri)
curl -s -g -H "Authorization: Bearer $SPIRE_TOKEN" -X GET "https://api.sense.spire.com/vessels/?ship_type=$ship_type&last_known_position_within=$encoded_polygon"
}
get_vessels 10 0 10 0 tanker
get_vessels 30 29 51 50 tanker # Kharg, Iran
import os
import math
import requests
from time import sleep
from tqdm.auto import tqdm
import pandas as pd
def spire_vessel_etl(df: pd.DataFrame) -> pd.DataFrame:
# Convert list columns to tuples
for col in [
"predictions",
"last_known_position.geometry.coordinates",
"most_recent_voyage.matched_port.center_point.coordinates",
"predicted_position.geometry.coordinates",
"predicted_route.start_position.coordinates",
"predicted_route.route.coordinates",
]:
df[col] = df[col].apply(tuple)
df["predicted_route.route.coordinates"] = df[
"predicted_route.route.coordinates"
].apply(lambda x: tuple(map(tuple, x)) if x != (None, None) else x)
df = df.drop(
columns=[
"last_known_position.geometry.type",
"most_recent_voyage.matched_port.center_point.type",
"predicted_position.geometry.type",
"predicted_route.start_position.type",
"predicted_route.route.type",
]
)
df = df[df.columns[~df.isna().all()]]
for col in ["predictions", "predicted_position.geometry.coordinates"]:
if (df[col] == ()).all():
df = df.drop(columns=[col])
df = df.drop_duplicates()
df = df.convert_dtypes(dtype_backend="pyarrow")
return df
with open(os.path.expanduser("~/.spirerc"), "r") as f:
SPIRE_TOKEN = f.read().rstrip("\n")
LIMIT = 1000
headers = {"Authorization": f"Bearer {SPIRE_TOKEN}"}
params = {"limit": LIMIT}
response = requests.get(
"https://api.sense.spire.com/vessels", headers=headers, params=params
)
n_pages = math.ceil(
response.json()["paging"]["total"] / response.json()["paging"]["limit"]
)
n_rows = n_pages * response.json()["paging"]["limit"]
df = spire_vessel_etl(pd.json_normalize(response.json()["data"]))
next_endpoint = response.json()["paging"]["next"]
for i in tqdm(range(n_pages + 1)):
params = {"next": next_endpoint, "limit": LIMIT}
sleep(5)
response = requests.get(
"https://api.sense.spire.com/vessels", headers=headers, params=params
)
df = pd.concat(
[df, spire_vessel_etl(pd.json_normalize(response.json()["data"]))],
ignore_index=True,
)
df = df.drop_duplicates()
next_endpoint = response.json()["paging"]["next"]
from __future__ import annotations
from datetime import datetime
from time import sleep
import geopandas as gpd
import pandas as pd
import requests
import s3fs
SLEEP_SECONDS = 10
URL = "https://api.sense.spire.com"
# VESSEL_ID = "350db884-9009-5b8d-9bec-aaa231f83e0b" # Ever Ace
VESSEL_ID = "871e3046-63a9-4f2e-9e32-1eda06e27d14" # Ever Given
VESSEL_LIMIT = 5000
# start = datetime(2021, 6, 20)
start = datetime(2022, 1, 1).isoformat()
end = datetime(2022, 9, 30).isoformat()
params: dict[str, str | int] = {
"timestamp_after": start,
"timestamp_before": end,
"limit": VESSEL_LIMIT,
}
with s3fs.S3FileSystem().open(
"s3://BUCKET/.spirerc", "r"
) as f:
SPIRE_TOKEN = f.read().rstrip("\n")
page = 1
dfs = []
print(f"requesting page: {page}")
response = requests.get(
f"{URL}/vessels/{VESSEL_ID}/positions",
headers={"Authorization": f"Bearer {SPIRE_TOKEN}"},
params=params,
)
df = pd.json_normalize(response.json()["data"])
print(df["timestamp"].max())
dfs.append(df)
while len(dfs[-1]) == VESSEL_LIMIT:
page += 1
next = response.json()["paging"]["next"]
sleep(SLEEP_SECONDS)
print(f"requesting page: {page}")
next_params: Params = {
"timestamp_after": start,
"timestamp_before": end,
"next": next,
"limit": VESSEL_LIMIT,
}
response = requests.get(
f"{URL}/vessels/{VESSEL_ID}/positions",
headers={"Authorization": f"Bearer {SPIRE_TOKEN}"},
params=next_params,
)
df = pd.json_normalize(response.json()["data"])
print(df["timestamp"].max())
dfs.append(df)
df = pd.concat(dfs, ignore_index=True)
df["timestamp"] = pd.to_datetime(df["timestamp"])
df["timestamp_iso"] = df["timestamp"].dt.strftime("%Y-%m-%dT%H:%M:%S.000Z")
df["created_at"] = pd.to_datetime(df["created_at"])
df["longitude"] = df["geometry.coordinates"].apply(lambda x: x[0])
df["latitude"] = df["geometry.coordinates"].apply(lambda x: x[1])
df = df.drop(
columns=[
"id",
"mmsi",
"vessel_id",
"_links.self",
"_links.vessel",
"geometry.type",
"geometry.coordinates",
]
)
df = df.rename(columns={"speed": "speed_knots", "rot": "rate_of_turn"})
gdf = gpd.GeoDataFrame(
df,
geometry=gpd.points_from_xy(df["longitude"], df["latitude"]),
crs="epsg:4326",
)
gdf.to_parquet(f"s3://{BUCKET}/spire_vessel_positions_{VESSEL_ID}_{start}-{end}.parquet")