Full implementation

This commit is contained in:
Kumi 2022-09-15 16:54:31 +00:00
parent f2b227f4b6
commit ac2e8b1f32
Signed by: kumi
GPG key ID: ECBCC9082395383F
12 changed files with 363 additions and 74 deletions

36
doc/example_ships.json Normal file
View file

@ -0,0 +1,36 @@
[
{
"ship_name": "Mein Schiff 1",
"ship_flag": "mt",
"ship_line_id": "32",
"ship_line_title": "TUI Cruises",
"imo": "9783564",
"mmsi": "248513000",
"lat": "46.81366",
"lon": "-71.19978",
"cog": 276,
"sog": 0,
"heading": "185",
"tst": "1663245194",
"icon": "128",
"hover": "Mein Schiff 1",
"destination": "Quebec City"
},
{
"ship_name": "Le Bellot",
"ship_flag": "wf",
"ship_line_id": "40",
"ship_line_title": "Ponant Cruises",
"imo": "9852418",
"mmsi": "578001500",
"lat": "46.81735",
"lon": "-71.19906",
"cog": 285,
"sog": 0,
"heading": "194",
"tst": "1663245891",
"icon": "1024",
"hover": "Le Bellot",
"destination": "CAQBE"
}
]

View file

@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "pycruisemapper"
version = "0.0.1"
version = "0.9.0"
authors = [
{ name="Kumi Mitterer", email="pycruisemapper@kumi.email" },
]

View file

@ -1,5 +1,5 @@
from .http import HTTPRequest
from .vessel import Vessel, Cruise, ShipLine, Flag
from .ship import Ship
from ..const import SHIPS_URL, SHIP_URL
from urllib.parse import urlencode
@ -10,14 +10,14 @@ import json
class CruiseMapper:
def request_vessels(self, **kwargs) -> List[Dict]:
def request_ships(self, **kwargs) -> List[Dict]:
payload = {
"minLat": kwargs.get("min_lat", -90),
"maxLat": kwargs.get("max_lat", 90),
"minLon": kwargs.get("min_lon", -180),
"maxLon": kwargs.get("max_lon", 180),
"filter": ",".join(kwargs.get("filter", [str(i) for i in range(100)])),
"zoom": "",
"zoom": kwargs.get("zoom", ""),
"imo": kwargs.get("imo", ""),
"mmsi": kwargs.get("mmsi", ""),
"t": int(kwargs.get("timestamp", datetime.now().timestamp()))
@ -27,22 +27,57 @@ class CruiseMapper:
return json.loads(request.open().read())
def request_vessel(self, **kwargs) -> Dict:
def request_ship(self, **kwargs) -> Dict:
payload = {
"imo": kwargs.get("imo", ""),
"mmsi": kwargs.get("mmsi", ""),
"zoom": ""
"zoom": kwargs.get("zoom", "")
}
request = HTTPRequest(f"{SHIP_URL}?{urlencode(payload)}")
return json.loads(request.open().read())
def get_vessels(self, **kwargs) -> List[Vessel]:
pass
def get_ships(self, **kwargs) -> List[Ship]:
"""Get data on all ships using ships.json endpoint
def get_vessel(self, **kwargs) -> Vessel:
pass
Note that **kwargs don't seem to have any influence here, so if you
need a particular vessel by IMO or MMSI, first get all ships by simply
calling get_ships(), then use a filter on the output like this:
def fill_vessel(self, vessel: Vessel):
pass
MeinSchiff1 = list(filter(lambda x: x.imo == 9783564))[0]
Returns:
List[Ship]: A list of Ship objects for all ships
"""
return [Ship.from_dict(d) for d in self.request_ships(**kwargs)]
def get_ship(self, **kwargs) -> Ship:
"""Get data on a single ship using ship.json endpoint.
Note that this lacks some important information, so you would almost
always want to get all ships through get_ships(), then pass the returned
Ship object to fill_ship() to add the information retrieved by get_ship().
Returns:
Ship: Ship object with the data returned by ship.json
"""
return Ship.from_dict(self.request_ship(**kwargs))
def fill_ship(self, ship: Ship) -> Ship:
"""Add missing data to Ship object retrieved from get_ships
Args:
ship (Ship): "Raw" Ship object as returned from get_ships
Returns:
Ship: Ship object with data from both get_ships and get_ship
"""
if not (ship.imo or ship.mmsi):
raise ValueError("Ship object has no identifier, cannot process")
details = self.get_ship(mmsi=ship.mmsi, imo=ship.imo)
ship.__dict__.update({k: v for k, v in details.__dict__.items() if v is not None})
return ship

View file

@ -0,0 +1,33 @@
from typing import Optional
class Cruise:
name: Optional[str]
url: Optional[str]
start_date: Optional[datetime]
end_date: Optional[datetime]
itinerary: Optional[List[Optional[Tuple[str, str]]]]
@property
def days(self) -> Optional[int]:
if self.end_date and self.start_date:
return (self.end_date - self.start_date).days
@classmethod
def from_dict(cls, indict: dict):
obj = cls()
obj.name = indict.get("name")
obj.url = indict.get("url")
obj.start_date = indict.get("start_date")
obj.end_date = indict.get("end_date")
if "itinerary" in indict:
obj.itinerary = []
for item in indict["itinerary"].values():
obj.itinerary.append((item["port"], item["date"]))
return obj
def __repr__(self):
return self.__dict__.__repr__()

View file

@ -0,0 +1,13 @@
from typing import Optional
class Flag:
code: str
name: str
def __init__(self, code: str, name: Optional[str] = None):
self.code = code
self.name = name
def __repr__(self):
return self.__dict__.__repr__()

View file

@ -9,4 +9,4 @@ class HTTPRequest(Request):
self.headers.update(REQUIRED_HEADERS)
def open(self):
return urlopen(self)
return urlopen(self)

View file

@ -0,0 +1,10 @@
class Location:
latitude: float
longitude: float
def __init__(self, latitude: float, longitude: float):
self.latitude = latitude
self.longitude = longitude
def __repr__(self):
return self.__dict__.__repr__()

View file

@ -0,0 +1,199 @@
from datetime import datetime, timedelta
from typing import Optional, List, Tuple
from locale import setlocale, LC_ALL
import re
from .location import Location
from .cruise import Cruise
from .flag import Flag
from .shipline import ShipLine
from ..const import IMAGE_BASE_PATH, TEMP_REGEX, HTML_REGEX, DEGREES_REGEX, SPEED_REGEX, GUST_REGEX
class Ship:
id: Optional[int]
name: Optional[str]
url: Optional[str]
url_deckplans: Optional[str]
url_staterooms: Optional[str]
image: Optional[str]
flag: Optional[Flag]
line: Optional[ShipLine]
spec_length: Optional[int] # stored in meters
spec_passengers: Optional[int]
year_built: Optional[int]
last_report: Optional[str]
imo: Optional[int]
mmsi: Optional[int]
latitude: Optional[float]
longitude: Optional[float]
cog: Optional[int] # Course over Ground
sog: Optional[int] # Speed over Ground
heading: Optional[int]
timestamp: Optional[datetime]
icon: Optional[int]
hover: Optional[str]
cruise: Optional[Cruise]
path: Optional[List[Optional[Location]]]
ports: Optional[List[Optional[Tuple[Optional[datetime], Optional[Location]]]]]
destination: Optional[str]
eta: Optional[datetime]
current_temperature: Optional[float] # Celsius
minimum_temperature: Optional[float] # Celsius
maximum_temperature: Optional[float] # Celsius
wind_degrees: Optional[float]
wind_speed: Optional[float] # m/s
wind_gust: Optional[float] # m/s
localtime: Optional[str]
@classmethod
def from_dict(cls, indict: dict):
obj = cls()
obj.id = indict.get("id")
obj.name = indict.get("name", indict.get("ship_name"))
obj.url = indict.get("url")
obj.url_deckplans = indict.get("url_deckplans")
obj.url_staterooms = indict.get("url_staterooms")
if "image" in indict:
obj.image = f"{IMAGE_BASE_PATH}{indict.get('image')}"
if "flag" in indict:
obj.flag = Flag(
code=indict["flag"].get("code"),
name=indict["flag"].get("name")
)
elif "ship_flag" in indict:
obj.flag = Flag(
code=indict.get("ship_flag")
)
if "line" in indict:
obj.line = ShipLine(
id=int(indict["line"]["id"]),
title=indict["line"].get("title"),
url=indict["line"].get("url")
)
elif "ship_line_id" in indict:
obj.line = ShipLine(
id=indict.get("ship_line_id"),
title=indict.get("ship_line_title")
)
if "spec_length" in indict:
parts = indict["spec_length"].split("/")
for part in parts:
if "m" in part:
try:
obj.spec_length = float(part.strip().split()[0])
except:
pass
if "spec_passengers" in indict:
obj.spec_passengers = int(indict["spec_passengers"])
if "year_of_built" in indict: # Those field names... 🤦
obj.year_built = int(indict["year_of_built"])
obj.last_report = indict.get("last_report")
if "imo" in indict:
obj.imo = int(indict["imo"])
if "mmsi" in indict:
obj.mmsi = int(indict["mmsi"])
if "lat" in indict and "lon" in indict:
obj.location = Location(indict["lat"], indict["lon"])
if "cog" in indict:
obj.cog = int(indict["cog"])
if "sog" in indict:
obj.sog = int(indict["sog"])
if "heading" in indict:
obj.heading = int(indict["heading"])
if "ts" in indict:
obj.timestamp = datetime.fromtimestamp(indict[ts])
obj.icon = indict.get("icon")
obj.hover = indict.get("hover")
if "cruise" in indict:
obj.cruise = Cruise.from_dict(indict["cruise"])
if "path" in indict:
if "points" in indict["path"]:
obj.path = list()
for point in indict["path"]["points"]:
lon, lat = point
obj.path.append(Location(lat, lon))
if "ports" in indict["path"]:
obj.ports = list()
for port in indict["path"]["ports"]:
if "dep_datetime" in port:
departure = datetime.strptime(
port["dep_datetime"], "%Y-%m-%d %H:%M:%S")
else:
departure = None
if "lat" in port and "lon" in port:
location = Location(port["lat"], port["lon"])
else:
location = None
obj.ports.append(departure, location)
obj.destination = indict.get("destination")
if "eta" in indict:
try:
previous = setlocale(LC_ALL)
setlocale(LC_ALL, "C")
obj.eta = datetime.strptime(date_string, "%d %B, %H:%M")
setlocale(LC_ALL, previous)
except:
obj.eta = None
if "weather" in indict:
temp_regex = re.compile(TEMP_REGEX)
html_regex = re.compile(HTML_REGEX)
degrees_regex = re.compile(DEGREES_REGEX)
speed_regex = re.compile(SPEED_REGEX)
gust_regex = re.compile(GUST_REGEX)
if "temperature" in indict["weather"]:
obj.current_temperature = float(re.search(temp_regex, re.sub(
html_regex, "", indict["weather"]["temperature"])).groups()[0])
if "wind" in indict["weather"]:
try:
obj.wind_degrees = int(
re.search(degrees_regex, indict["weather"]["wind"]).groups()[0])
except:
pass
try:
obj.wind_speed = float(
re.search(speed_regex, indict["weather"]["wind"]).groups()[0])
except:
pass
try:
obj.wind_gust = float(
re.search(speed_regex, indict["weather"]["wind"]).groups()[0])
except:
pass
obj.localtime = indict["weather"].get("localtime")
return obj
def __repr__(self):
return self.__dict__.__repr__()

View file

@ -0,0 +1,15 @@
from typing import Optional
class ShipLine:
id: int
title: str
url: Optional[str]
def __init__(self, id: int, title: str, url: Optional[str] = None):
self.id = id
self.title = title
self.url = url
def __repr__(self):
return self.__dict__.__repr__()

View file

@ -1,60 +0,0 @@
from datetime import datetime, timedelta
from typing import Optional, List, Tuple
class Cruise:
name: Optional[str]
url: Optional[str]
start_date: Optional[datetime]
end_date: Optional[datetime]
itinerary: Optional[List[Optional[Tuple[str, str]]]]
@property
def days(self) -> Optional[int]:
if self.end_date and self.start_date:
return (self.end_date - self.start_date).days
class Flag:
code: str
name: str
class ShipLine:
title: str
id: int
url: Optional[str]
class Vessel:
id: Optional[int]
name: Optional[str]
url: Optional[str]
url_deckplans: Optional[str]
url_staterooms: Optional[str]
image: Optional[str]
flag: Flag
line: Optional[ShipLine]
spec_length: Optional[int] # stored in meters
spec_passengers: Optional[int]
year_built: Optional[int]
last_report: Optional[str]
imo: int
mmsi: int
latitude: float
longitude: float
cog: int # Course over Ground
sog: int # Speed over Ground
heading: int
timestamp: datetime
icon: int
hover: str
cruise: Optional[Cruise]
path: Optional[List[Optional[Tuple[float, float]]]]
ports: Optional[List[Optional[Tuple[datetime, float, float]]]]
destination: str
eta: Optional[datetime]
current_temperature: Optional[float] # Celsius
minimum_temperature: Optional[float] # Celsius
maximum_temperature: Optional[float] # Celsius
wind_degrees: Optional[float]
wind_speed: Optional[float] # m/s
wind_gust: Optional[float] # m/s
utc_offset: Optional[timedelta]

View file

@ -1,5 +1,13 @@
IMAGE_BASE_PATH = "https://www.cruisemapper.com/"
SHIPS_URL = "https://www.cruisemapper.com/map/ships.json"
SHIP_URL = "https://www.cruisemapper.com/map/ships.json"
SHIP_URL = "https://www.cruisemapper.com/map/ship.json"
REQUIRED_HEADERS = {"X-Requested-With": "XMLHttpRequest",
"User-Agent": "Mozilla/5.0 (compatible: pyCruiseMapper; https://kumig.it/kumitterer/pycruisemapper)"}
TEMP_REGEX = "(-?\d+\.?\d*) ?°C"
HTML_REGEX = "<.*?>"
DEGREES_REGEX = "(\d+) ?°"
SPEED_REGEX = "\/ ?(\d+.?\d*) ?m\/s"
GUST_REGEX = "Gust: (\d+.?\d*) ?m\/s"