diff --git a/examples/worldstate/custom_models.py b/examples/worldstate/custom_models.py index e6647b5..5aac10c 100644 --- a/examples/worldstate/custom_models.py +++ b/examples/worldstate/custom_models.py @@ -5,7 +5,7 @@ from msgspec import field # use this to rename response keys from warframe.worldstate import WorldstateClient -from warframe.worldstate.common.base_objects import ( +from warframe.worldstate.common.core import ( SingleQueryModel, ) # this import might change @@ -29,9 +29,9 @@ class CustomCambionDrift(SingleQueryModel): async def main(): async with WorldstateClient() as client: - arbi = await client.query(CustomCambionDrift) + ccd = await client.query(CustomCambionDrift) - print(arbi) + print(ccd) if __name__ == "__main__": diff --git a/warframe/worldstate/client.py b/warframe/worldstate/client.py index f2731dc..6d7932b 100644 --- a/warframe/worldstate/client.py +++ b/warframe/worldstate/client.py @@ -108,7 +108,7 @@ async def query( async def query_list_of( self, cls: Type[SupportsMultiQuery], language: Optional[Language] = None - ) -> Optional[List[SupportsMultiQuery]]: + ) -> List[SupportsMultiQuery]: """Queries the model of type `MultiQueryModel` to return its corresponding object. Args: diff --git a/warframe/worldstate/common/__init__.py b/warframe/worldstate/common/__init__.py index 1ff66f4..21e5c21 100644 --- a/warframe/worldstate/common/__init__.py +++ b/warframe/worldstate/common/__init__.py @@ -1,2 +1,2 @@ from .types_ import * -from .base_objects import * +from .core import * diff --git a/warframe/worldstate/common/base_objects.py b/warframe/worldstate/common/base_objects.py deleted file mode 100644 index 8123457..0000000 --- a/warframe/worldstate/common/base_objects.py +++ /dev/null @@ -1,67 +0,0 @@ -from datetime import datetime -from typing import Any, List, Optional, Type, TypeVar, ClassVar - -import msgspec - - -__all__ = ["MultiQueryModel", "SingleQueryModel", "WorldstateObject"] - - -def _decode_hook(type: Type, obj: Any) -> Any: - if isinstance(type, datetime) and isinstance(obj, str): - return datetime.fromisoformat(obj.strip("Z")) - - return obj - - -class WorldstateObject(msgspec.Struct, rename="camel"): - """ - Base class for every model-related object. - """ - - pass - - -T = TypeVar("T", bound=WorldstateObject) - - -class MultiQueryModel(WorldstateObject): - """ - Base class for giving models an indicator whether they can only come from a JSON array. - """ - - __endpoint__: ClassVar[str] - - @classmethod - def _from_json(cls: Type[T], response: str) -> List[T]: - """Decodes a JSON string to an list of object of T. - - Args: - cls (Type[T]): The type T. - response (str): The raw JSON as string. - - Returns: - List[T]: A list of objects of T. - """ - return msgspec.json.decode(response, type=List[cls], dec_hook=_decode_hook) - - -class SingleQueryModel(WorldstateObject): - """ - Base class for giving models an indicator whether they can only come from a single JSON object. - """ - - __endpoint__: ClassVar[str] - - @classmethod - def _from_json(cls: Type[T], response: str) -> T: - """Decodes a JSON string to an object of T. - - Args: - cls (Type[T]): The type T. - response (str): The raw JSON as string. - - Returns: - T: The object of T - """ - return msgspec.json.decode(response, type=cls, dec_hook=_decode_hook) diff --git a/warframe/worldstate/common/core.py b/warframe/worldstate/common/core.py new file mode 100644 index 0000000..8e14d7d --- /dev/null +++ b/warframe/worldstate/common/core.py @@ -0,0 +1,119 @@ +from datetime import datetime, timezone +from typing import Any, List, Type, TypeVar, ClassVar + +import msgspec + + +__all__ = ["MultiQueryModel", "SingleQueryModel", "WorldstateObject", "TimedEvent"] + + +def _decode_hook(type: Type, obj: Any) -> Any: + if isinstance(type, datetime) and isinstance(obj, str): + return datetime.fromisoformat(obj.strip("Z")) + + return obj + + +def _get_short_format_time_string(dt: datetime) -> str: + """Returns a short time formatted string based on the now and the dt. + + Args: + dt (datetime): The time the Event XYZ started/ends + + Returns: + str: The short time formatted string of the time in between now and when the dt. + """ + now = datetime.now(tz=timezone.utc) + time_in_between = now - dt if now > dt else dt - now + + days = time_in_between.days + hours, remainder = divmod(time_in_between.seconds, 3600) + minutes, seconds = divmod(remainder, 60) + + time_components = [(days, "d"), (hours, "h"), (minutes, "m"), (seconds, "s")] + formatted_time = "" + + for t, suffix in time_components: + if t != 0: + formatted_time += f"{t}{suffix} " + + return formatted_time.strip() + + +class TimedEvent(msgspec.Struct, rename="camel"): + activation: datetime + "The time the event began" + + expiry: datetime + "The time the event ends" + + @property + def start_string(self) -> str: + "Short-time-formatted duration string representing the start of the event" + return f"{_get_short_format_time_string(self.activation)} ago" + + @property + def eta(self) -> str: + "Short-time-formatted duration string representing the end of the event / cycle" + return _get_short_format_time_string(self.expiry) + + @property + def expired(self) -> bool: + return self.activation >= self.expiry + + @property + def active(self) -> bool: + return not self.expired + + +class WorldstateObject(msgspec.Struct, rename="camel"): + """ + Base class for every model-related object. + """ + + pass + + +T = TypeVar("T", bound=WorldstateObject) + + +class MultiQueryModel(WorldstateObject): + """ + Base class for giving models an indicator whether they can only come from a JSON array. + """ + + __endpoint__: ClassVar[str] + + @classmethod + def _from_json(cls: Type[T], response: str) -> List[T]: + """Decodes a JSON string to an list of object of T. + + Args: + cls (Type[T]): The type T. + response (str): The raw JSON as string. + + Returns: + List[T]: A list of objects of T. + """ + return msgspec.json.decode(response, type=List[cls], dec_hook=_decode_hook) + + +class SingleQueryModel(WorldstateObject): + """ + Base class for giving models an indicator whether they can only come from a single JSON object. + """ + + __endpoint__: ClassVar[str] + + @classmethod + def _from_json(cls: Type[T], response: str) -> T: + """Decodes a JSON string to an object of T. + + Args: + cls (Type[T]): The type T. + response (str): The raw JSON as string. + + Returns: + T: The object of T + """ + return msgspec.json.decode(response, type=cls, dec_hook=_decode_hook) diff --git a/warframe/worldstate/models/alert.py b/warframe/worldstate/models/alert.py index a97ee7c..03811d2 100644 --- a/warframe/worldstate/models/alert.py +++ b/warframe/worldstate/models/alert.py @@ -1,37 +1,17 @@ -from datetime import datetime -from typing import List, Optional +from typing import List -from ..common import ItemRewardType, MultiQueryModel +from ..common import ItemRewardType, MultiQueryModel, TimedEvent from .mission import Mission __all__ = ["Alert"] -class Alert(MultiQueryModel): +class Alert(MultiQueryModel, TimedEvent): __endpoint__ = "/alerts" # required - activation: datetime - "The time the mission began" - - expiry: datetime - "The time the mission ends" - mission: Mission "The mission that corresponds to this Alert" reward_types: List[ItemRewardType] "A list of reward types" - - # optional - start_string: Optional[str] = None - "Short-time-formatted duration string representing the start of the alert" - - active: Optional[bool] = None - "Whether the alert is still active or not" - - eta: Optional[str] = None - "Short-formatted string estimating the time until the Alert is closed" - - expired: Optional[bool] = None - "Whether the mission is expired or not" diff --git a/warframe/worldstate/models/arbitration.py b/warframe/worldstate/models/arbitration.py index fe990c7..8698a33 100644 --- a/warframe/worldstate/models/arbitration.py +++ b/warframe/worldstate/models/arbitration.py @@ -1,37 +1,40 @@ -from datetime import datetime from typing import Optional from msgspec import field -from ..common import SingleQueryModel +from ..common import SingleQueryModel, TimedEvent from ..enums import Faction, MissionType __all__ = ["Arbitration"] -class Arbitration(SingleQueryModel): +class Arbitration(SingleQueryModel, TimedEvent): + """ + UNSTABLE + """ + __endpoint__ = "/arbitration" # required - activation: datetime - "The time the mission began." - expiry: datetime - "The time the mission ends." node: str + "The localized plain name for the node the Arbitration is on." + + node_key: str "The plain name for the node the Arbitration is on." + faction: Faction = field( name="enemy" ) # it's not called faction at the endpoint, instead it's called enemy "The Faction of the corresponding mission." + archwing_required: bool = field(name="archwing") "Whether an archwing is required in order to play the mission" + is_sharkwing: bool = field(name="sharkwing") "Whether the mission takes place in a submerssible mission" - # optional - start_string: Optional[str] = None - "Short-time-formatted duration string representing the start of the Arbitration." - active: Optional[bool] = None - "Whether the alert is still active or not." - mission_type: Optional[MissionType] = None + mission_type: str = field(name="type") + "The localized MissionType of the given mission (Capture, Spy, etc.)" + + mission_type_key: MissionType = field(name="typeKey") "The MissionType of the given mission (Capture, Spy, etc.)" diff --git a/warframe/worldstate/models/archon_hunt.py b/warframe/worldstate/models/archon_hunt.py index 408edd9..0db21f7 100644 --- a/warframe/worldstate/models/archon_hunt.py +++ b/warframe/worldstate/models/archon_hunt.py @@ -1,9 +1,7 @@ -from datetime import datetime -from typing import List, Optional +from typing import List -from ..common import SingleQueryModel +from ..common import SingleQueryModel, TimedEvent from ..enums import Faction, MissionType -from .reward import Reward __all__ = ["ArchonHunt"] @@ -12,55 +10,41 @@ class ArchonHuntMission(SingleQueryModel): # archon hunt's mission is pretty di # required node: str "The localized node string" - type: MissionType + + node_key: str + "The node string" + + type: str + "The localized MissionType of the given mission (Capture, Spy, etc.)" + + type_key: MissionType "The MissionType of the given mission (Capture, Spy, etc.)" - nightmare: bool - "Whether the mission is a nightmare mission" + archwing_required: bool "Whether an archwing is required in order to play the mission" - # optional - reward: Optional[Reward] = None - "The mission's reward" - is_sharkwing: Optional[bool] = None - "Whether the mission takes place in a submerssible mission" - enemy_spec: Optional[str] = None - "Enemy specification for the mission" - level_override: Optional[str] = None - "Override for the map on this mission" - advanced_spawners: Optional[List[str]] = None + advanced_spawners: List[str] "Array of strings denoting extra spawners for a mission" - required_items: Optional[List[str]] = None + + required_items: List[str] "Items required to enter the mission" - consume_required_items: Optional[bool] = None - "Whether the required items are consumed" - leaders_always_allowed: Optional[bool] = None - "Whether leaders are always allowed" - level_auras: Optional[List[str]] = None + + level_auras: List[str] "Affectors for this mission" + is_sharkwing: bool + "Whether the mission takes place in a submerssible mission" + -class ArchonHunt(SingleQueryModel): +class ArchonHunt(SingleQueryModel, TimedEvent): __endpoint__ = "/archonHunt" # required - activation: datetime - "The time the Archon Hunt started" - expiry: datetime - "The time the Archon Hunt ends" missions: List[ArchonHuntMission] "The list of missions that have to be played in order to queue up for the rewards" + boss: str "The archon you are going to fight against" + faction: Faction "The faction you're up against: Narmer" - expired: bool - "Whether the Archon Hunt has ended" - eta: str - "Short-formatted string estimating the time until the Alert is closed." - - # optional - start_string: Optional[str] = None - "Short-time-formatted duration string representing the start of the Archon Hunt" - active: Optional[bool] = None - "Whether the alert is still active or not" diff --git a/warframe/worldstate/models/cambion_drift.py b/warframe/worldstate/models/cambion_drift.py index 6b3e053..543bd82 100644 --- a/warframe/worldstate/models/cambion_drift.py +++ b/warframe/worldstate/models/cambion_drift.py @@ -1,23 +1,13 @@ -from datetime import datetime -from typing import Literal, Optional +from typing import Literal -from ..common import SingleQueryModel +from ..common import SingleQueryModel, TimedEvent __all__ = ["CambionDrift"] -class CambionDrift(SingleQueryModel): +class CambionDrift(SingleQueryModel, TimedEvent): __endpoint__ = "/cambionCycle" # required - expiry: datetime - "The time the Cycle ends" - - activation: datetime - "The time the new rotation of the cycle started" - state: Literal["vome", "fass"] 'The state of the Cambion Drift. ("vome" or "fass")' - - # optional - time_left: Optional[str] = None diff --git a/warframe/worldstate/models/cetus.py b/warframe/worldstate/models/cetus.py index 98c16f3..f24f879 100644 --- a/warframe/worldstate/models/cetus.py +++ b/warframe/worldstate/models/cetus.py @@ -1,34 +1,18 @@ from datetime import datetime from typing import Literal, Optional -from ..common import SingleQueryModel +from ..common import SingleQueryModel, TimedEvent __all__ = ["Cetus"] -class Cetus(SingleQueryModel): +class Cetus(SingleQueryModel, TimedEvent): __endpoint__ = "/cetusCycle" # required - expiry: datetime - "The time the Cycle ends" - is_day: bool "Whether it is currently day on the Plains of Eidolon" - time_left: str - "Short formatted time string on how much time is left on the current cycle" - - # optional - activation: Optional[datetime] = None - "The time the new rotation of the cycle started" - - start_string: Optional[str] = None - "Short-time-formatted duration string representing the start of the alert" - - short_string: Optional[str] = None - "Short-time-formatted duration string representing the start of the alert" - @property def state(self): """ diff --git a/warframe/worldstate/models/counted_item.py b/warframe/worldstate/models/counted_item.py index 4f0ebfe..417539b 100644 --- a/warframe/worldstate/models/counted_item.py +++ b/warframe/worldstate/models/counted_item.py @@ -1,4 +1,4 @@ -from ..common.base_objects import SingleQueryModel +from ..common import SingleQueryModel __all__ = ["CountedItem"] diff --git a/warframe/worldstate/models/daily_deal.py b/warframe/worldstate/models/daily_deal.py index cee5ba6..5468b46 100644 --- a/warframe/worldstate/models/daily_deal.py +++ b/warframe/worldstate/models/daily_deal.py @@ -1,12 +1,9 @@ -from datetime import datetime -from typing import Optional - -from ..common import MultiQueryModel +from ..common import MultiQueryModel, TimedEvent __all__ = ["DailyDeal"] -class DailyDeal(MultiQueryModel): +class DailyDeal(MultiQueryModel, TimedEvent): __endpoint__ = "/dailyDeals" # required @@ -30,9 +27,3 @@ class DailyDeal(MultiQueryModel): discount: int "The discount as %" - - expiry: datetime - "The time the Daily Deal ends ends" - - # optional - activation: Optional[datetime] = None diff --git a/warframe/worldstate/models/event.py b/warframe/worldstate/models/event.py index 8fc6fa2..50a2e95 100644 --- a/warframe/worldstate/models/event.py +++ b/warframe/worldstate/models/event.py @@ -5,24 +5,14 @@ from .reward import Reward -from ..common import MultiQueryModel +from ..common import MultiQueryModel, TimedEvent from ..enums import MissionType, Faction, Syndicate __all__ = ["Event"] -class Event(MultiQueryModel): - activation: Optional[datetime] = None - "The time the Event started" - - expiry: Optional[datetime] = None - "The time the Event" - - start_string: Optional[str] = None - "Short-time-formatted duration string of the start of the Fissure" - - active: Optional[bool] = None - "Whether the event is currently active" +class Event(MultiQueryModel, TimedEvent): + __endpoint__ = "/events" maximum_score: Optional[int] = None "Maximum score to complete the event" diff --git a/warframe/worldstate/models/fissure.py b/warframe/worldstate/models/fissure.py index c3439d9..e11c3b8 100644 --- a/warframe/worldstate/models/fissure.py +++ b/warframe/worldstate/models/fissure.py @@ -1,42 +1,40 @@ -from datetime import datetime -from typing import Optional - from msgspec import field -from ..common import MultiQueryModel, FissureTier, FissureTierNumber -from ..enums import MissionType, Faction + +from ..common import FissureTier, FissureTierNumber, MultiQueryModel, TimedEvent +from ..enums import Faction, MissionType __all__ = ["Fissure"] -class Fissure(MultiQueryModel): +class Fissure(MultiQueryModel, TimedEvent): __endpoint__ = "/fissures" # required - activation: datetime - "The time the Fissure started" - expiry: datetime - "The time the fissure ends" node: str "The localized node string" - expired: bool - "Whether the fissure is still present" - eta: str - "Short-formatted string estimating the time until the Fissure is closed" - mission_type: MissionType + + node: str + "The node string" + + mission_type: str + "The localized game mode that the mission/node houses" + + mission_type_key: MissionType = field(name="missionKey") "The game mode that the mission/node houses" + tier: FissureTier "Tier of the mission (Lith, Meso, etc.)" + tier_num: FissureTierNumber "The Numeric tier corresponding to the tier" - faction: Faction = field(name="enemy") + + faction: str = field(name="enemy") + "The localized faction that houses the node/mission" + + faction_key: Faction = field(name="enemyKey") "The faction that houses the node/mission" + is_steel_path: bool = field(name="isHard") "Whether the mission of the Fissure is on is on the Steel Path" is_railjack: bool = field(name="isStorm") "Whether the mssion of the Fissure is a Railjack mission" - - # optional - start_string: Optional[str] = None - "Short-time-formatted duration string of the start of the Fissure" - active: Optional[bool] = None - "Whether the fissure is currently active" diff --git a/warframe/worldstate/models/flash_sale.py b/warframe/worldstate/models/flash_sale.py index 1b76b65..7df2e64 100644 --- a/warframe/worldstate/models/flash_sale.py +++ b/warframe/worldstate/models/flash_sale.py @@ -3,21 +3,18 @@ from msgspec import field -from ..common import MultiQueryModel +from ..common import MultiQueryModel, TimedEvent __all__ = ["FlashSale"] -class FlashSale(MultiQueryModel): +class FlashSale(MultiQueryModel, TimedEvent): __endpoint__ = "/flashSales" # required item: str "The Item that is being sold" - eta: str - "Short-formatted string estimating the time until the Flash Sale ends" - discount: int "The discount of the item." @@ -31,11 +28,5 @@ class FlashSale(MultiQueryModel): is_popular: Optional[bool] = None # docs are wrong "Whether the item is popular" - expired: Optional[bool] = None - "Whether the item is expired or not" - is_in_market: Optional[bool] = field(name="isShownInMarket", default=None) "Whether the item is available/shown in the market. Most likely to be `True`" - - expiry: Optional[datetime] = None - "The time the Flash Sale ends" diff --git a/warframe/worldstate/models/invasion.py b/warframe/worldstate/models/invasion.py index 7151fe6..9d4fae5 100644 --- a/warframe/worldstate/models/invasion.py +++ b/warframe/worldstate/models/invasion.py @@ -3,7 +3,7 @@ from msgspec import field -from ..common import ItemRewardType, MultiQueryModel, WorldstateObject +from ..common import ItemRewardType, MultiQueryModel, TimedEvent, WorldstateObject from ..enums import Faction from .reward import Reward @@ -12,9 +12,12 @@ class InvasionMember(WorldstateObject): # optional - reward: Optional[Reward] = None + reward: Reward "The reward of the mission." - faction: Optional[Faction] = None + + faction: str + "The localized faction that houses the node/mission" + faction_key: Faction "The faction that houses the node/mission" @@ -32,33 +35,48 @@ class Invasion(MultiQueryModel): # required activation: datetime "The time the Invasion began" + completed: bool "Whether the Invasion is over" + completion_percentage: float = field(name="completion") "Percantage of the Invasion's completion" + count: int "How many fights have happened" + description: str = field(name="desc") "The Invasion's description" + eta: str "Short-formatted string estimating the time until the Invasion is closed" + node: str "The localized node string" + + node_key: str + "The node string" + required_runs: int "The amount of runs required to qualify for the reward. (most likely 3)" + vs_infested: bool = field(name="vsInfestation") "Whether the fight is against infested enemies" + attacker: Attacker + "The invading faction information" + + defender: Defender + "The defending faction information" # optional expiry: Optional[datetime] = None "The time the Invasion ends" + start_string: Optional[str] = None "Short-time-formatted duration string of the start of the Invasion" + active: Optional[bool] = None "Whether the invasion is currently active" - attacker: Optional[Attacker] = None - "The invading faction information" - defender: Optional[Defender] = None - "The defending faction information" + reward_types: Optional[List[ItemRewardType]] = None "A list of reward types" diff --git a/warframe/worldstate/models/mission.py b/warframe/worldstate/models/mission.py index c650353..c5d5ce2 100644 --- a/warframe/worldstate/models/mission.py +++ b/warframe/worldstate/models/mission.py @@ -19,7 +19,10 @@ class Mission(SingleQueryModel): faction: Faction "The faction that houses the node/mission" - type: MissionType + type: str + "The localized MissionType of the given mission (Capture, Spy, etc.)" + + type_key: MissionType "The MissionType of the given mission (Capture, Spy, etc.)" nightmare: bool diff --git a/warframe/worldstate/models/orb_vallis.py b/warframe/worldstate/models/orb_vallis.py index 79bd185..02816ab 100644 --- a/warframe/worldstate/models/orb_vallis.py +++ b/warframe/worldstate/models/orb_vallis.py @@ -1,27 +1,21 @@ from datetime import datetime from typing import Literal, Optional -from ..common import SingleQueryModel +from ..common import SingleQueryModel, TimedEvent __all__ = ["OrbVallis"] -class OrbVallis(SingleQueryModel): +class OrbVallis(SingleQueryModel, TimedEvent): __endpoint__ = "/vallisCycle" # required - expiry: datetime - "The time the Cycle ends" - - time_left: str - "Short formatted time string on how much time is left on the current cycle" - is_warm: bool "Whether it is currently warm" - # optional - activation: Optional[datetime] = None - "The time the new rotation of the cycle started" - - state: Optional[Literal["warm", "cold"]] = None - "The state of the vallis (warm/cold)" + @property + def state(self) -> Literal["warm", "cold"]: + """ + The state of the Plains of Eidolon ("warm" or "cold") + """ + return "warm" if self.is_warm else "cold" diff --git a/warframe/worldstate/models/sortie.py b/warframe/worldstate/models/sortie.py index 7699404..9170665 100644 --- a/warframe/worldstate/models/sortie.py +++ b/warframe/worldstate/models/sortie.py @@ -1,9 +1,8 @@ -from datetime import datetime from typing import List, Optional from msgspec import field -from ..common import SingleQueryModel +from ..common import SingleQueryModel, TimedEvent from ..enums import MissionType, Faction __all__ = ["Sortie"] @@ -11,10 +10,10 @@ class SortieMission(SingleQueryModel): node: str - "The node the mission is on" + "The localized node the mission is on" - type: MissionType = field(name="missionType") - "The Type of the mission" + type: str = field(name="missionType") + "The localized Type of the mission" modifier: str "The modifier applied to the mission" @@ -23,35 +22,15 @@ class SortieMission(SingleQueryModel): "The description of the modifier" -class Sortie(SingleQueryModel): +class Sortie(SingleQueryModel, TimedEvent): __endpoint__ = "/sortie" # required boss: str "The boss you're up against" - activation: datetime - "The time the Sortie started" - - expiry: datetime - "The time the Sortie ends" - missions: List[SortieMission] = field(name="variants") "The 3 missions you have to play in order to get the reward" faction: Faction "The Faction you're up against" - - expired: bool - "Whether the Sortie is still active" - - eta: str - "Short-formatted string estimating the time until the Sortie ends" - - # optional - start_string: Optional[str] = None - "Short-time-formatted duration string of the start of the Fissure" - - @property - def active(self): - return not self.expired diff --git a/warframe/worldstate/models/void_trader.py b/warframe/worldstate/models/void_trader.py index 5f05317..2a67cc1 100644 --- a/warframe/worldstate/models/void_trader.py +++ b/warframe/worldstate/models/void_trader.py @@ -3,7 +3,7 @@ from msgspec import field -from ..common import SingleQueryModel, WorldstateObject +from ..common import SingleQueryModel, WorldstateObject, TimedEvent __all__ = ["VoidTrader"] @@ -19,28 +19,12 @@ class InventoryItem(WorldstateObject): "The cost of credits" -class VoidTrader(SingleQueryModel): +class VoidTrader(SingleQueryModel, TimedEvent): __endpoint__ = "/voidTrader" # required - start_string: str - "Short-time-formatted duration string of the arrival of the Void Trader" - - active: bool - "Whether the Void Trader is currently active" - location: str "The Relay on which the Void Trader is on" inventory: List[InventoryItem] "The Void Trader's inventory" - - end_string: str - "Short-time-formatted duration string of the department of the Void Trader" - - # optional - arrival: Optional[datetime] = field(name="activation", default=None) - "The time the Void Trader arrived" - - department: Optional[datetime] = field(name="expiry", default=None) - "The time the Void Trader departs"