Full implementation
This commit is contained in:
parent
f2b227f4b6
commit
ac2e8b1f32
12 changed files with 363 additions and 74 deletions
36
doc/example_ships.json
Normal file
36
doc/example_ships.json
Normal 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"
|
||||
}
|
||||
]
|
|
@ -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" },
|
||||
]
|
||||
|
|
|
@ -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
|
||||
|
|
33
src/pycruisemapper/classes/cruise.py
Normal file
33
src/pycruisemapper/classes/cruise.py
Normal 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__()
|
13
src/pycruisemapper/classes/flag.py
Normal file
13
src/pycruisemapper/classes/flag.py
Normal 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__()
|
|
@ -9,4 +9,4 @@ class HTTPRequest(Request):
|
|||
self.headers.update(REQUIRED_HEADERS)
|
||||
|
||||
def open(self):
|
||||
return urlopen(self)
|
||||
return urlopen(self)
|
||||
|
|
10
src/pycruisemapper/classes/location.py
Normal file
10
src/pycruisemapper/classes/location.py
Normal 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__()
|
199
src/pycruisemapper/classes/ship.py
Normal file
199
src/pycruisemapper/classes/ship.py
Normal 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__()
|
15
src/pycruisemapper/classes/shipline.py
Normal file
15
src/pycruisemapper/classes/shipline.py
Normal 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__()
|
|
@ -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]
|
|
@ -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"
|
Loading…
Reference in a new issue