edfh-data 0.5.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- edfh_data/__init__.py +1 -0
- edfh_data/db/__init__.py +0 -0
- edfh_data/db/crud.py +218 -0
- edfh_data/db/models.py +164 -0
- edfh_data/db/utils.py +47 -0
- edfh_data/eddn/__init__.py +0 -0
- edfh_data/eddn/handlers.py +147 -0
- edfh_data/eddn/listener.py +96 -0
- edfh_data/exceptions.py +11 -0
- edfh_data/models.py +292 -0
- edfh_data-0.5.2.dist-info/METADATA +123 -0
- edfh_data-0.5.2.dist-info/RECORD +16 -0
- edfh_data-0.5.2.dist-info/WHEEL +5 -0
- edfh_data-0.5.2.dist-info/entry_points.txt +3 -0
- edfh_data-0.5.2.dist-info/licenses/LICENSE +9 -0
- edfh_data-0.5.2.dist-info/top_level.txt +1 -0
edfh_data/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.5.2"
|
edfh_data/db/__init__.py
ADDED
|
File without changes
|
edfh_data/db/crud.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import datetime as dt
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
from sqlmodel import and_
|
|
5
|
+
from sqlmodel import desc
|
|
6
|
+
from sqlmodel import select
|
|
7
|
+
from sqlmodel import Session
|
|
8
|
+
|
|
9
|
+
from edfh_data.db.models import FactionStateDB
|
|
10
|
+
from edfh_data.db.models import MarketCommodityDB
|
|
11
|
+
from edfh_data.db.models import StationDB
|
|
12
|
+
from edfh_data.db.models import SystemDB
|
|
13
|
+
from edfh_data.db.models import SystemFactionDB
|
|
14
|
+
from edfh_data.db.models import SystemFactionHistoryDB
|
|
15
|
+
from edfh_data.db.models import SystemPowerDB
|
|
16
|
+
from edfh_data.db.utils import common_keys_equal
|
|
17
|
+
from edfh_data.db.utils import engine
|
|
18
|
+
from edfh_data.models import Station
|
|
19
|
+
from edfh_data.models import StationMarket
|
|
20
|
+
from edfh_data.models import System
|
|
21
|
+
|
|
22
|
+
UTC = dt.timezone.utc
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def create_update_system(system: System) -> None:
|
|
26
|
+
|
|
27
|
+
system_data = system.model_dump(
|
|
28
|
+
exclude_none=True, exclude={"system_factions", "system_powers"}
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
with Session(engine) as session:
|
|
32
|
+
stmt = select(SystemDB).where(SystemDB.system_name == system.system_name)
|
|
33
|
+
res = session.exec(stmt)
|
|
34
|
+
system_db = res.one_or_none()
|
|
35
|
+
|
|
36
|
+
if system_db:
|
|
37
|
+
system_update = system.update_datetime.replace(tzinfo=UTC)
|
|
38
|
+
system_db_update = system_db.update_datetime.replace(tzinfo=UTC)
|
|
39
|
+
if system_update > system_db_update:
|
|
40
|
+
system_db = system_db | system_data
|
|
41
|
+
else:
|
|
42
|
+
return
|
|
43
|
+
else:
|
|
44
|
+
system_db = SystemDB(**system_data)
|
|
45
|
+
session.add(system_db)
|
|
46
|
+
session.commit()
|
|
47
|
+
session.refresh(system_db)
|
|
48
|
+
|
|
49
|
+
system_factions = []
|
|
50
|
+
for faction in system.system_factions:
|
|
51
|
+
|
|
52
|
+
faction_data = faction.model_dump(exclude_none=True, exclude={"states"})
|
|
53
|
+
|
|
54
|
+
states_data = None
|
|
55
|
+
if faction.states:
|
|
56
|
+
states_data = json.dumps(faction.model_dump(mode="json")["states"])
|
|
57
|
+
|
|
58
|
+
stmt = select(SystemFactionDB).where(
|
|
59
|
+
and_(
|
|
60
|
+
SystemFactionDB.system_id == system_db.system_id,
|
|
61
|
+
SystemFactionDB.faction_name == faction.faction_name,
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
res = session.exec(stmt)
|
|
65
|
+
system_faction = res.one_or_none()
|
|
66
|
+
|
|
67
|
+
if system_faction:
|
|
68
|
+
system_faction = system_faction | faction_data
|
|
69
|
+
else:
|
|
70
|
+
system_faction = SystemFactionDB(**faction_data)
|
|
71
|
+
|
|
72
|
+
system_faction.faction_states = [
|
|
73
|
+
FactionStateDB(**state.model_dump()) for state in faction.states
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
system_factions.append(system_faction)
|
|
77
|
+
|
|
78
|
+
# Get latest faction data from history
|
|
79
|
+
stmt = (
|
|
80
|
+
select(SystemFactionHistoryDB)
|
|
81
|
+
.where(
|
|
82
|
+
and_(
|
|
83
|
+
SystemFactionHistoryDB.system_id == system_db.system_id,
|
|
84
|
+
SystemFactionHistoryDB.faction_name == faction.faction_name,
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
.order_by(desc(SystemFactionHistoryDB.update_datetime))
|
|
88
|
+
)
|
|
89
|
+
res = session.exec(stmt)
|
|
90
|
+
system_faction_latest = res.first()
|
|
91
|
+
|
|
92
|
+
if system_faction_latest:
|
|
93
|
+
# Only update the update_datetime field if faction data has not changed
|
|
94
|
+
if common_keys_equal(faction_data, system_faction_latest.model_dump()):
|
|
95
|
+
system_faction_latest.update_datetime = system.update_datetime
|
|
96
|
+
session.add(system_faction_latest)
|
|
97
|
+
# Create new entry only if values have updated
|
|
98
|
+
else:
|
|
99
|
+
system_faction_latest = SystemFactionHistoryDB(
|
|
100
|
+
system_id=system_db.system_id, # type: ignore
|
|
101
|
+
update_datetime=system.update_datetime,
|
|
102
|
+
states=states_data,
|
|
103
|
+
**faction_data,
|
|
104
|
+
)
|
|
105
|
+
else:
|
|
106
|
+
system_faction_latest = SystemFactionHistoryDB(
|
|
107
|
+
system_id=system_db.system_id, # type: ignore
|
|
108
|
+
update_datetime=system.update_datetime,
|
|
109
|
+
states=states_data,
|
|
110
|
+
**faction_data,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
session.add(system_faction_latest)
|
|
114
|
+
|
|
115
|
+
system_db.system_factions = system_factions
|
|
116
|
+
|
|
117
|
+
system_powers = []
|
|
118
|
+
for power in system.system_powers:
|
|
119
|
+
|
|
120
|
+
stmt = select(SystemPowerDB).where(
|
|
121
|
+
and_(
|
|
122
|
+
SystemPowerDB.system_id == system_db.system_id,
|
|
123
|
+
SystemPowerDB.power == power,
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
res = session.exec(stmt)
|
|
127
|
+
system_power = res.one_or_none()
|
|
128
|
+
|
|
129
|
+
if not system_power:
|
|
130
|
+
system_power = SystemPowerDB(
|
|
131
|
+
system_id=system_db.system_id, # type: ignore
|
|
132
|
+
power=power,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
system_powers.append(system_power)
|
|
136
|
+
|
|
137
|
+
system_db.system_powers = system_powers
|
|
138
|
+
|
|
139
|
+
session.add(system_db)
|
|
140
|
+
session.commit()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def create_update_station(station: Station) -> None:
|
|
144
|
+
|
|
145
|
+
station_data = station.model_dump(exclude_none=True)
|
|
146
|
+
|
|
147
|
+
with Session(engine) as session:
|
|
148
|
+
# Get the parent system from the DB if it exists
|
|
149
|
+
stmt = select(SystemDB).where(SystemDB.system_name == station.system_name)
|
|
150
|
+
res = session.exec(stmt)
|
|
151
|
+
system_db = res.one_or_none()
|
|
152
|
+
|
|
153
|
+
# Stop if the system does not exist in the DB
|
|
154
|
+
if system_db is None:
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
# Get the station from the DB if it exists
|
|
158
|
+
stmt = select(StationDB).where(StationDB.market_id == station.market_id)
|
|
159
|
+
res = session.exec(stmt)
|
|
160
|
+
station_db = res.one_or_none()
|
|
161
|
+
|
|
162
|
+
if station_db:
|
|
163
|
+
station_update = station.update_datetime.replace(tzinfo=UTC)
|
|
164
|
+
station_db_update = station_db.update_datetime.replace(tzinfo=UTC)
|
|
165
|
+
if station_update > station_db_update:
|
|
166
|
+
station_db = station_db | station_data
|
|
167
|
+
station_db.system_id = system_db.system_id # type: ignore
|
|
168
|
+
else:
|
|
169
|
+
return
|
|
170
|
+
else:
|
|
171
|
+
station_db = StationDB(
|
|
172
|
+
system_id=system_db.system_id, # type: ignore
|
|
173
|
+
**station_data,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
session.add(station_db)
|
|
177
|
+
session.commit()
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def create_update_station_market(market: StationMarket):
|
|
181
|
+
|
|
182
|
+
with Session(engine) as session:
|
|
183
|
+
# Get the parent station from the DB
|
|
184
|
+
stmt = select(StationDB).where(StationDB.market_id == market.market_id)
|
|
185
|
+
res = session.exec(stmt)
|
|
186
|
+
station_db = res.one_or_none()
|
|
187
|
+
|
|
188
|
+
# Stop if the station does not exist in the DB
|
|
189
|
+
if station_db is None:
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
market_update = market.update_datetime.replace(tzinfo=UTC)
|
|
193
|
+
station_market_update = station_db.market_update_datetime
|
|
194
|
+
if (
|
|
195
|
+
station_market_update is None
|
|
196
|
+
or market_update > station_market_update.replace(tzinfo=UTC)
|
|
197
|
+
):
|
|
198
|
+
existing_commodities = {c.name: c for c in station_db.market_commodities}
|
|
199
|
+
commodities = []
|
|
200
|
+
|
|
201
|
+
for commodity in market.commodities:
|
|
202
|
+
if commodity.name in existing_commodities:
|
|
203
|
+
commodities.append(
|
|
204
|
+
existing_commodities[commodity.name] | commodity.model_dump()
|
|
205
|
+
)
|
|
206
|
+
else:
|
|
207
|
+
commodities.append(
|
|
208
|
+
MarketCommodityDB(
|
|
209
|
+
station_id=station_db.station_id, # type: ignore
|
|
210
|
+
**commodity.model_dump(),
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
station_db.market_update_datetime = market.update_datetime
|
|
215
|
+
station_db.market_commodities = commodities
|
|
216
|
+
|
|
217
|
+
session.add(station_db)
|
|
218
|
+
session.commit()
|
edfh_data/db/models.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import datetime as dt
|
|
2
|
+
from typing import Mapping, Self
|
|
3
|
+
|
|
4
|
+
from sqlmodel import BigInteger
|
|
5
|
+
from sqlmodel import Column
|
|
6
|
+
from sqlmodel import Field
|
|
7
|
+
from sqlmodel import Relationship
|
|
8
|
+
from sqlmodel import SQLModel
|
|
9
|
+
from sqlmodel import String
|
|
10
|
+
from sqlmodel import TIMESTAMP
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class UpdatableSQLModel(SQLModel):
|
|
14
|
+
|
|
15
|
+
def update(self, **kwargs: Mapping) -> Self:
|
|
16
|
+
"""Update the speficied fields of the model instance and return the instance."""
|
|
17
|
+
for field, value in kwargs.items():
|
|
18
|
+
if field in self.__class__.model_fields:
|
|
19
|
+
setattr(self, field, value)
|
|
20
|
+
return self
|
|
21
|
+
|
|
22
|
+
def __or__(self, other: Mapping) -> Self:
|
|
23
|
+
"""Update the speficied fields of the model instance from a dictionary using
|
|
24
|
+
the or ('|') operator"""
|
|
25
|
+
return self.update(**other)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SystemDB(UpdatableSQLModel, table=True):
|
|
29
|
+
__tablename__ = "systems" # type: ignore
|
|
30
|
+
system_id: int | None = Field(default=None, primary_key=True, sa_type=BigInteger)
|
|
31
|
+
system_name: str
|
|
32
|
+
system_address: int = Field(sa_type=BigInteger)
|
|
33
|
+
x: float
|
|
34
|
+
y: float
|
|
35
|
+
z: float
|
|
36
|
+
population: int = Field(sa_type=BigInteger)
|
|
37
|
+
system_factions: list["SystemFactionDB"] = Relationship(
|
|
38
|
+
back_populates="system", cascade_delete=True
|
|
39
|
+
)
|
|
40
|
+
update_datetime: dt.datetime = Field(
|
|
41
|
+
sa_column=Column(TIMESTAMP(timezone=True), nullable=False)
|
|
42
|
+
)
|
|
43
|
+
system_allegiance: str | None = Field(default=None)
|
|
44
|
+
controlling_faction: str | None = Field(default=None)
|
|
45
|
+
controlling_power: str | None = Field(default=None)
|
|
46
|
+
powerplay_state: str | None = Field(default=None)
|
|
47
|
+
powerplay_state_control_progress: float | None = Field(default=None)
|
|
48
|
+
powerplay_state_reinforcement: int | None = Field(default=None)
|
|
49
|
+
powerplay_state_undermining: int | None = Field(default=None)
|
|
50
|
+
primary_economy: str | None = Field(default=None)
|
|
51
|
+
secondary_economy: str | None = Field(default=None)
|
|
52
|
+
security: str | None = Field(default=None)
|
|
53
|
+
government: str | None = Field(default=None)
|
|
54
|
+
system_powers: list["SystemPowerDB"] = Relationship(
|
|
55
|
+
back_populates="system", cascade_delete=True
|
|
56
|
+
)
|
|
57
|
+
system_factions_history: list["SystemFactionHistoryDB"] = Relationship(
|
|
58
|
+
back_populates="system", cascade_delete=True
|
|
59
|
+
)
|
|
60
|
+
stations: list["StationDB"] = Relationship(
|
|
61
|
+
back_populates="system", cascade_delete=True
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class SystemFactionDB(UpdatableSQLModel, table=True):
|
|
66
|
+
__tablename__ = "system_factions" # type: ignore
|
|
67
|
+
system_faction_id: int | None = Field(
|
|
68
|
+
default=None, primary_key=True, sa_type=BigInteger
|
|
69
|
+
)
|
|
70
|
+
system_id: int = Field(
|
|
71
|
+
foreign_key="systems.system_id", index=True, sa_type=BigInteger
|
|
72
|
+
)
|
|
73
|
+
faction_name: str
|
|
74
|
+
influence: float
|
|
75
|
+
faction_states: list["FactionStateDB"] = Relationship(
|
|
76
|
+
back_populates="system_faction", cascade_delete=True
|
|
77
|
+
)
|
|
78
|
+
system: SystemDB = Relationship(back_populates="system_factions")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class SystemPowerDB(UpdatableSQLModel, table=True):
|
|
82
|
+
__tablename__ = "system_powers" # type: ignore
|
|
83
|
+
system_power_id: int | None = Field(
|
|
84
|
+
default=None, primary_key=True, sa_type=BigInteger
|
|
85
|
+
)
|
|
86
|
+
system_id: int = Field(
|
|
87
|
+
foreign_key="systems.system_id", index=True, sa_type=BigInteger
|
|
88
|
+
)
|
|
89
|
+
power: str
|
|
90
|
+
system: SystemDB = Relationship(back_populates="system_powers")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class SystemFactionHistoryDB(SQLModel, table=True):
|
|
94
|
+
__tablename__ = "system_factions_history" # type: ignore
|
|
95
|
+
system_faction_history_id: int | None = Field(
|
|
96
|
+
default=None, primary_key=True, sa_type=BigInteger
|
|
97
|
+
)
|
|
98
|
+
system_id: int = Field(
|
|
99
|
+
foreign_key="systems.system_id", index=True, sa_type=BigInteger
|
|
100
|
+
)
|
|
101
|
+
faction_name: str
|
|
102
|
+
influence: float
|
|
103
|
+
states: str | None = Field(default=None, sa_type=String(1024))
|
|
104
|
+
update_datetime: dt.datetime = Field(
|
|
105
|
+
sa_column=Column(TIMESTAMP(timezone=True), nullable=False)
|
|
106
|
+
)
|
|
107
|
+
system: SystemDB = Relationship(back_populates="system_factions_history")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class FactionStateDB(SQLModel, table=True):
|
|
111
|
+
__tablename__ = "faction_states" # type: ignore
|
|
112
|
+
faction_state_id: int | None = Field(
|
|
113
|
+
default=None, primary_key=True, sa_type=BigInteger
|
|
114
|
+
)
|
|
115
|
+
system_faction_id: int = Field(
|
|
116
|
+
foreign_key="system_factions.system_faction_id", index=True, sa_type=BigInteger
|
|
117
|
+
)
|
|
118
|
+
state_phase: str
|
|
119
|
+
state: str
|
|
120
|
+
system_faction: SystemFactionDB = Relationship(back_populates="faction_states")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class StationDB(UpdatableSQLModel, table=True):
|
|
124
|
+
__tablename__ = "stations" # type: ignore
|
|
125
|
+
station_id: int | None = Field(default=None, primary_key=True, sa_type=BigInteger)
|
|
126
|
+
system_id: int = Field(
|
|
127
|
+
foreign_key="systems.system_id", index=True, sa_type=BigInteger
|
|
128
|
+
)
|
|
129
|
+
station_name: str
|
|
130
|
+
distance_from_star: float
|
|
131
|
+
market_id: int = Field(unique=True, index=True, sa_type=BigInteger)
|
|
132
|
+
station_type: str
|
|
133
|
+
is_planetary: bool
|
|
134
|
+
max_landing_pad_size: str
|
|
135
|
+
primary_economy: str
|
|
136
|
+
secondary_economy: str | None = Field(default=None)
|
|
137
|
+
station_faction_name: str = Field(index=True)
|
|
138
|
+
update_datetime: dt.datetime = Field(
|
|
139
|
+
sa_column=Column(TIMESTAMP(timezone=True), nullable=False)
|
|
140
|
+
)
|
|
141
|
+
system: SystemDB = Relationship(back_populates="stations")
|
|
142
|
+
market_commodities: list["MarketCommodityDB"] = Relationship(
|
|
143
|
+
back_populates="station", cascade_delete=True
|
|
144
|
+
)
|
|
145
|
+
market_update_datetime: dt.datetime | None = Field(
|
|
146
|
+
default=None, sa_column=Column(TIMESTAMP(timezone=True))
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class MarketCommodityDB(UpdatableSQLModel, table=True):
|
|
151
|
+
__tablename__ = "market_commodities" # type: ignore
|
|
152
|
+
market_commodity_id: int | None = Field(
|
|
153
|
+
default=None, primary_key=True, sa_type=BigInteger
|
|
154
|
+
)
|
|
155
|
+
station_id: int = Field(
|
|
156
|
+
foreign_key="stations.station_id", index=True, sa_type=BigInteger
|
|
157
|
+
)
|
|
158
|
+
name: str
|
|
159
|
+
mean_price: int
|
|
160
|
+
buy_price: int
|
|
161
|
+
stock: int
|
|
162
|
+
sell_price: int
|
|
163
|
+
demand: int
|
|
164
|
+
station: StationDB = Relationship(back_populates="market_commodities")
|
edfh_data/db/utils.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Mapping
|
|
4
|
+
|
|
5
|
+
from sqlmodel import create_engine
|
|
6
|
+
from sqlmodel import SQLModel
|
|
7
|
+
|
|
8
|
+
if Path(".env").is_file():
|
|
9
|
+
from dotenv import load_dotenv
|
|
10
|
+
|
|
11
|
+
load_dotenv()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def db_connect_string():
|
|
15
|
+
"""Generate the DB connection string from different environment variables."""
|
|
16
|
+
user = os.getenv("DB_USER", "dbuser")
|
|
17
|
+
passwd = os.getenv("DB_PASSWD")
|
|
18
|
+
host = os.getenv("DB_HOST", "localhost")
|
|
19
|
+
port = os.getenv("DB_PORT", 3306)
|
|
20
|
+
name = os.getenv("DB_NAME")
|
|
21
|
+
|
|
22
|
+
return f"mysql+pymysql://{user}:{passwd}@{host}:{port}/{name}"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
engine = create_engine(db_connect_string())
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def init_db():
|
|
29
|
+
SQLModel.metadata.create_all(engine)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def update_entry_fields(entry: SQLModel, update: Mapping) -> SQLModel:
|
|
33
|
+
for field, value in update.items():
|
|
34
|
+
if field in entry.__class__.model_fields:
|
|
35
|
+
setattr(entry, field, value)
|
|
36
|
+
return entry
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def exclude_none(data):
|
|
40
|
+
return {k: v for k, v in data.items() if v is not None}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def common_keys_equal(first: Mapping, second: Mapping) -> bool:
|
|
44
|
+
"""Compares the values of the common keys in two dictionaries. Returns True if all
|
|
45
|
+
values are equal, False otherwise."""
|
|
46
|
+
common_keys = set(first.keys()) & set(second.keys())
|
|
47
|
+
return all(first[k] == second[k] for k in common_keys)
|
|
File without changes
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import datetime as dt
|
|
2
|
+
import os
|
|
3
|
+
from typing import Mapping
|
|
4
|
+
import zlib
|
|
5
|
+
|
|
6
|
+
import orjson
|
|
7
|
+
import pika
|
|
8
|
+
from sqlalchemy.exc import OperationalError
|
|
9
|
+
|
|
10
|
+
from edfh_data.db.crud import create_update_station
|
|
11
|
+
from edfh_data.db.crud import create_update_station_market
|
|
12
|
+
from edfh_data.db.crud import create_update_system
|
|
13
|
+
from edfh_data.db.utils import init_db
|
|
14
|
+
from edfh_data.exceptions import EventTooOldError
|
|
15
|
+
from edfh_data.models import construction_station_types
|
|
16
|
+
from edfh_data.models import Station
|
|
17
|
+
from edfh_data.models import StationMarket
|
|
18
|
+
from edfh_data.models import System
|
|
19
|
+
|
|
20
|
+
JOURNAL_V1_SCHEMA = "https://eddn.edcd.io/schemas/journal/1"
|
|
21
|
+
COMMODITY_V3_SCHEMA = "https://eddn.edcd.io/schemas/commodity/3"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def handle_journal_v1_fsdjump(message: Mapping) -> None:
|
|
25
|
+
"""Handle the journal/1 messages with event==FSDJump."""
|
|
26
|
+
|
|
27
|
+
system = System(**message)
|
|
28
|
+
|
|
29
|
+
create_update_system(system)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def handle_journal_v1_docked(message: Mapping) -> None:
|
|
33
|
+
"""Handle the journal/1 message with event==Docked."""
|
|
34
|
+
# Ignore colonisation ships & construction sites
|
|
35
|
+
if (
|
|
36
|
+
message["StationType"] in construction_station_types
|
|
37
|
+
or "colonisationship" in message["StationName"].lower()
|
|
38
|
+
):
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
station = Station(**message)
|
|
42
|
+
|
|
43
|
+
create_update_station(station)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def handle_commodity_v3(message: Mapping) -> None:
|
|
47
|
+
"""Handle the commodity/3 message."""
|
|
48
|
+
|
|
49
|
+
market = StationMarket(**message)
|
|
50
|
+
|
|
51
|
+
create_update_station_market(market)
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def handle_eddn_event(
|
|
56
|
+
event: dict, max_age: dt.timedelta = dt.timedelta(hours=1)
|
|
57
|
+
) -> bool:
|
|
58
|
+
"""Handle any eddn event."""
|
|
59
|
+
handled = False
|
|
60
|
+
message = event["message"]
|
|
61
|
+
|
|
62
|
+
now = dt.datetime.now(tz=dt.timezone.utc)
|
|
63
|
+
message_datetime = dt.datetime.fromisoformat(message["timestamp"]).replace(
|
|
64
|
+
tzinfo=dt.timezone.utc
|
|
65
|
+
)
|
|
66
|
+
messag_age = now - message_datetime
|
|
67
|
+
|
|
68
|
+
if messag_age > max_age:
|
|
69
|
+
raise EventTooOldError(
|
|
70
|
+
f"The event timestamp is too old: {message['timestamp']}"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
if event["$schemaRef"] == JOURNAL_V1_SCHEMA and message.get("event") == "FSDJump":
|
|
74
|
+
handle_journal_v1_fsdjump(message)
|
|
75
|
+
handled = True
|
|
76
|
+
|
|
77
|
+
elif event["$schemaRef"] == JOURNAL_V1_SCHEMA and message.get("event") == "Docked":
|
|
78
|
+
handle_journal_v1_docked(message)
|
|
79
|
+
handled = True
|
|
80
|
+
|
|
81
|
+
elif event["$schemaRef"] == COMMODITY_V3_SCHEMA:
|
|
82
|
+
handle_commodity_v3(message)
|
|
83
|
+
handled = True
|
|
84
|
+
|
|
85
|
+
return handled
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def run():
|
|
89
|
+
|
|
90
|
+
init_db()
|
|
91
|
+
|
|
92
|
+
RMQ_EXCHANGE_NAME = "eddn_raw"
|
|
93
|
+
RMQ_QUEUE_NAME = "eddn_raw_queue"
|
|
94
|
+
|
|
95
|
+
rmq_connection = pika.BlockingConnection(
|
|
96
|
+
pika.ConnectionParameters(
|
|
97
|
+
host=os.getenv("RMQ_HOST", "localhost"),
|
|
98
|
+
credentials=pika.PlainCredentials(
|
|
99
|
+
username=os.getenv("RMQ_USER", "guest"),
|
|
100
|
+
password=os.getenv("RMQ_PASSWD", "guest"),
|
|
101
|
+
),
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
rmq_channel = rmq_connection.channel()
|
|
106
|
+
rmq_channel.exchange_declare(exchange=RMQ_EXCHANGE_NAME, exchange_type="fanout")
|
|
107
|
+
|
|
108
|
+
rmq_channel.queue_declare(queue=RMQ_QUEUE_NAME, durable=True)
|
|
109
|
+
rmq_channel.queue_bind(exchange=RMQ_EXCHANGE_NAME, queue=RMQ_QUEUE_NAME)
|
|
110
|
+
|
|
111
|
+
def callback(ch, method, properties, body):
|
|
112
|
+
try:
|
|
113
|
+
event_json = zlib.decompress(body)
|
|
114
|
+
event = orjson.loads(event_json)
|
|
115
|
+
print(f" [x] Received {event["$schemaRef"]}")
|
|
116
|
+
|
|
117
|
+
handle_eddn_event(event, max_age=dt.timedelta(days=7))
|
|
118
|
+
|
|
119
|
+
ch.basic_ack(delivery_tag=method.delivery_tag)
|
|
120
|
+
|
|
121
|
+
except OperationalError as e:
|
|
122
|
+
if e.orig and e.orig.args[0] == 1020:
|
|
123
|
+
# OperationalError(1020, "Record has changed since last read in
|
|
124
|
+
# table 'systems'")
|
|
125
|
+
# Do not aknowledge and requeue for re-processing
|
|
126
|
+
ch.basic_nack(delivery_tag=method.delivery_tag, requeue=True)
|
|
127
|
+
else:
|
|
128
|
+
ch.basic_ack(delivery_tag=method.delivery_tag)
|
|
129
|
+
print(e)
|
|
130
|
+
|
|
131
|
+
except Exception as e:
|
|
132
|
+
ch.basic_ack(delivery_tag=method.delivery_tag)
|
|
133
|
+
print(e)
|
|
134
|
+
|
|
135
|
+
rmq_channel.basic_qos(prefetch_count=int(os.getenv("RMQ_HANDLER_PREFETCH", 1)))
|
|
136
|
+
rmq_channel.basic_consume(queue=RMQ_QUEUE_NAME, on_message_callback=callback)
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
rmq_channel.start_consuming()
|
|
140
|
+
except KeyboardInterrupt:
|
|
141
|
+
rmq_channel.stop_consuming()
|
|
142
|
+
|
|
143
|
+
rmq_connection.close()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
if __name__ == "__main__":
|
|
147
|
+
run()
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
import zlib
|
|
6
|
+
|
|
7
|
+
import orjson
|
|
8
|
+
import pika
|
|
9
|
+
import zmq
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def run():
|
|
13
|
+
|
|
14
|
+
if Path(".env").is_file():
|
|
15
|
+
from dotenv import load_dotenv
|
|
16
|
+
|
|
17
|
+
load_dotenv()
|
|
18
|
+
|
|
19
|
+
EDDN_RELAY_URL = "tcp://eddn.edcd.io:9500"
|
|
20
|
+
EDDN_SOCKET_TIMEOUT = 600000
|
|
21
|
+
RMQ_EXCHANGE_NAME = "eddn_raw"
|
|
22
|
+
|
|
23
|
+
context = zmq.Context()
|
|
24
|
+
subscriber = context.socket(zmq.SUB)
|
|
25
|
+
|
|
26
|
+
subscriber.setsockopt(zmq.SUBSCRIBE, b"")
|
|
27
|
+
subscriber.setsockopt(zmq.RCVTIMEO, EDDN_SOCKET_TIMEOUT)
|
|
28
|
+
|
|
29
|
+
rmq_credentials = pika.PlainCredentials(
|
|
30
|
+
username=os.getenv("RMQ_USER", "guest"),
|
|
31
|
+
password=os.getenv("RMQ_PASSWD", "guest"),
|
|
32
|
+
)
|
|
33
|
+
rmq_connect_params = pika.ConnectionParameters(
|
|
34
|
+
host=os.getenv("RMQ_HOST", "localhost"),
|
|
35
|
+
credentials=rmq_credentials,
|
|
36
|
+
heartbeat=20,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
while True:
|
|
40
|
+
try:
|
|
41
|
+
subscriber.connect(EDDN_RELAY_URL)
|
|
42
|
+
|
|
43
|
+
rmq_connection = None
|
|
44
|
+
rmq_connection = pika.BlockingConnection(rmq_connect_params)
|
|
45
|
+
rmq_channel = rmq_connection.channel()
|
|
46
|
+
rmq_channel.exchange_declare(
|
|
47
|
+
exchange=RMQ_EXCHANGE_NAME, exchange_type="fanout"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
while True:
|
|
51
|
+
try:
|
|
52
|
+
event_raw = subscriber.recv()
|
|
53
|
+
|
|
54
|
+
if event_raw is False:
|
|
55
|
+
subscriber.disconnect(EDDN_RELAY_URL)
|
|
56
|
+
break
|
|
57
|
+
|
|
58
|
+
rmq_channel.basic_publish(
|
|
59
|
+
exchange=RMQ_EXCHANGE_NAME,
|
|
60
|
+
routing_key="",
|
|
61
|
+
body=event_raw,
|
|
62
|
+
properties=pika.BasicProperties(
|
|
63
|
+
delivery_mode=pika.DeliveryMode.Persistent
|
|
64
|
+
),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
event_json = zlib.decompress(event_raw)
|
|
68
|
+
event = orjson.loads(event_json)
|
|
69
|
+
print(f" [x] Published {event.get("$schemaRef")}")
|
|
70
|
+
|
|
71
|
+
except KeyError as e:
|
|
72
|
+
print(e)
|
|
73
|
+
|
|
74
|
+
except KeyboardInterrupt:
|
|
75
|
+
subscriber.disconnect(EDDN_RELAY_URL)
|
|
76
|
+
rmq_connection.close()
|
|
77
|
+
sys.exit()
|
|
78
|
+
|
|
79
|
+
except Exception as e:
|
|
80
|
+
print(repr(e))
|
|
81
|
+
try:
|
|
82
|
+
subscriber.disconnect(EDDN_RELAY_URL)
|
|
83
|
+
except Exception:
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
if rmq_connection and rmq_connection.is_open:
|
|
88
|
+
rmq_connection.close()
|
|
89
|
+
except Exception:
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
time.sleep(5)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
if __name__ == "__main__":
|
|
96
|
+
run()
|
edfh_data/exceptions.py
ADDED
edfh_data/models.py
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import datetime as dt
|
|
2
|
+
from typing import Annotated, Literal, Self
|
|
3
|
+
|
|
4
|
+
from fdev_ids import load_table
|
|
5
|
+
from pydantic import AfterValidator
|
|
6
|
+
from pydantic import AliasPath
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
from pydantic import BeforeValidator
|
|
9
|
+
from pydantic import Field
|
|
10
|
+
from pydantic import model_validator
|
|
11
|
+
|
|
12
|
+
economy_ids = load_table("economy")
|
|
13
|
+
security_ids = load_table("security")
|
|
14
|
+
government_ids = load_table("government")
|
|
15
|
+
happiness_ids = load_table("happiness")
|
|
16
|
+
factionstate_ids = load_table("factionstate")
|
|
17
|
+
commodity_symbols = {c["symbol"].lower(): c for c in load_table("commodity").values()}
|
|
18
|
+
rare_commodity_symbols = {
|
|
19
|
+
c["symbol"].lower(): c for c in load_table("rare_commodity").values()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
planetary_station_types = (
|
|
23
|
+
"CraterOutpost",
|
|
24
|
+
"CraterPort",
|
|
25
|
+
"OnFootSettlement",
|
|
26
|
+
"SurfaceStation",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
construction_station_types = (
|
|
30
|
+
"PlanetaryConstructionDepot",
|
|
31
|
+
"SpaceConstructionDepot",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
stronghold_carrier_names = (
|
|
35
|
+
"Stronghold Carrier",
|
|
36
|
+
"Hochburg-Carrier",
|
|
37
|
+
"Porte-vaisseaux de forteresse",
|
|
38
|
+
"Portanaves bastión",
|
|
39
|
+
"Носитель-база",
|
|
40
|
+
"Transportadora da potência",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def replace_none(value: str | None) -> str | None:
|
|
45
|
+
"""Replace 'None' string and empty string values with None."""
|
|
46
|
+
if value in ("None", ""):
|
|
47
|
+
return None
|
|
48
|
+
return value
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def map_economy(economy_id: str) -> str | None:
|
|
52
|
+
return replace_none(economy_ids.get(economy_id))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def map_security(security_id: str) -> str | None:
|
|
56
|
+
return security_ids.get(security_id)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def map_government(government_id: str) -> str | None:
|
|
60
|
+
return replace_none(government_ids.get(government_id))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def map_happiness(happiness_id: str) -> str | None:
|
|
64
|
+
return happiness_ids.get(happiness_id)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def map_faction_state(factionstate_id: str) -> str | None:
|
|
68
|
+
return replace_none(factionstate_ids.get(factionstate_id))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def map_commodity(commodity_symbol: str) -> str | None:
|
|
72
|
+
all_commodities = commodity_symbols | rare_commodity_symbols
|
|
73
|
+
return all_commodities[commodity_symbol.lower()]["name"]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class FactionState(BaseModel):
|
|
77
|
+
"""A state of a faction in a system."""
|
|
78
|
+
|
|
79
|
+
state_phase: Literal["pending", "active", "recovering"]
|
|
80
|
+
state: Annotated[str, BeforeValidator(map_faction_state)]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class SystemFaction(BaseModel):
|
|
84
|
+
"""Status of a faction in a star system.
|
|
85
|
+
|
|
86
|
+
Aliases are defined so that an instance can be created from a record in the
|
|
87
|
+
'Factions' field of the message of a journal/1 event with `event=FSDJump`
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
faction_name: str = Field(alias="Name")
|
|
91
|
+
allegiance: str = Field(alias="Allegiance")
|
|
92
|
+
faction_state: str = Field(alias="FactionState")
|
|
93
|
+
government: str = Field(alias="Government")
|
|
94
|
+
Happiness: Annotated[str | None, BeforeValidator(map_happiness)] = Field(
|
|
95
|
+
default=None, alias="Happiness"
|
|
96
|
+
)
|
|
97
|
+
influence: float = Field(alias="Influence")
|
|
98
|
+
states: list[FactionState] = Field(default_factory=list)
|
|
99
|
+
|
|
100
|
+
@model_validator(mode="before")
|
|
101
|
+
@classmethod
|
|
102
|
+
def map_states(cls, data: dict) -> dict:
|
|
103
|
+
pending = [
|
|
104
|
+
FactionState(state=s["State"], state_phase="pending")
|
|
105
|
+
for s in data.get("PendingStates", list())
|
|
106
|
+
]
|
|
107
|
+
active = [
|
|
108
|
+
FactionState(state=s["State"], state_phase="active")
|
|
109
|
+
for s in data.get("ActiveStates", list())
|
|
110
|
+
]
|
|
111
|
+
recovering = [
|
|
112
|
+
FactionState(state=s["State"], state_phase="recovering")
|
|
113
|
+
for s in data.get("RecoveringStates", list())
|
|
114
|
+
]
|
|
115
|
+
data["states"] = pending + active + recovering
|
|
116
|
+
|
|
117
|
+
return data
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def influence_total_is_one(system_factions: list[SystemFaction]) -> list[SystemFaction]:
|
|
121
|
+
"""Verify that the sum of faction influences is one (within 0.00001)."""
|
|
122
|
+
is_one = 1.00001 >= sum(f.influence for f in system_factions) >= 0.99999
|
|
123
|
+
|
|
124
|
+
if not is_one:
|
|
125
|
+
raise ValueError("Sum of faction influences must be one")
|
|
126
|
+
|
|
127
|
+
return system_factions
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class System(BaseModel):
|
|
131
|
+
"""Star system.
|
|
132
|
+
|
|
133
|
+
Aliases are defined so that an instance can be created from the message of a
|
|
134
|
+
journal/1 event with `event=FSDJump`.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
system_name: str = Field(alias="StarSystem")
|
|
138
|
+
system_address: int = Field(alias="SystemAddress")
|
|
139
|
+
population: int = Field(alias="Population")
|
|
140
|
+
x: float = Field(validation_alias=AliasPath("StarPos", 0))
|
|
141
|
+
y: float = Field(validation_alias=AliasPath("StarPos", 1))
|
|
142
|
+
z: float = Field(validation_alias=AliasPath("StarPos", 2))
|
|
143
|
+
update_datetime: dt.datetime = Field(alias="timestamp")
|
|
144
|
+
system_allegiance: Annotated[str | None, AfterValidator(replace_none)] = Field(
|
|
145
|
+
default=None, alias="SystemAllegiance"
|
|
146
|
+
)
|
|
147
|
+
controlling_faction: Annotated[str | None, AfterValidator(replace_none)] = Field(
|
|
148
|
+
default=None, validation_alias=AliasPath("SystemFaction", "Name")
|
|
149
|
+
)
|
|
150
|
+
controlling_power: Annotated[str | None, AfterValidator(replace_none)] = Field(
|
|
151
|
+
default=None, alias="ControllingPower"
|
|
152
|
+
)
|
|
153
|
+
powerplay_state: Annotated[str | None, AfterValidator(replace_none)] = Field(
|
|
154
|
+
default=None, alias="PowerplayState"
|
|
155
|
+
)
|
|
156
|
+
powerplay_state_control_progress: float | None = Field(
|
|
157
|
+
default=None, alias="PowerplayStateControlProgress"
|
|
158
|
+
)
|
|
159
|
+
powerplay_state_reinforcement: int | None = Field(
|
|
160
|
+
default=None, alias="PowerplayStateReinforcement"
|
|
161
|
+
)
|
|
162
|
+
powerplay_state_undermining: int | None = Field(
|
|
163
|
+
default=None, alias="PowerplayStateUndermining"
|
|
164
|
+
)
|
|
165
|
+
primary_economy: Annotated[str | None, BeforeValidator(map_economy)] = Field(
|
|
166
|
+
default=None, alias="SystemEconomy"
|
|
167
|
+
)
|
|
168
|
+
secondary_economy: Annotated[str | None, BeforeValidator(map_economy)] = Field(
|
|
169
|
+
default=None, alias="SystemSecondEconomy"
|
|
170
|
+
)
|
|
171
|
+
security: Annotated[str | None, BeforeValidator(map_security)] = Field(
|
|
172
|
+
default=None, alias="SystemSecurity"
|
|
173
|
+
)
|
|
174
|
+
government: Annotated[str | None, BeforeValidator(map_government)] = Field(
|
|
175
|
+
default=None, alias="SystemGovernment"
|
|
176
|
+
)
|
|
177
|
+
system_factions: Annotated[
|
|
178
|
+
list[SystemFaction], AfterValidator(influence_total_is_one)
|
|
179
|
+
] = Field(default_factory=list, alias="Factions")
|
|
180
|
+
system_powers: list[str] = Field(default_factory=list, alias="Powers")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def construction_type_not_allowed(station_type: str) -> str:
|
|
184
|
+
"""Raise an exception if the station type is a construction depot."""
|
|
185
|
+
if station_type in construction_station_types:
|
|
186
|
+
raise ValueError("Construction site not allowed as station")
|
|
187
|
+
return station_type
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def colonisation_ship_not_allowed(station_name: str) -> str:
|
|
191
|
+
"""Raise an exception if the station name indicates a colonisation ship."""
|
|
192
|
+
if "colonisationship" in station_name.lower():
|
|
193
|
+
raise ValueError("Colonisation ship not allowed as station")
|
|
194
|
+
return station_name
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class Station(BaseModel):
|
|
198
|
+
"""Station.
|
|
199
|
+
|
|
200
|
+
Aliases are defined so that an instance can be created from the message of a
|
|
201
|
+
journal/1 event with `event=Docked`.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
system_name: str = Field(alias="StarSystem")
|
|
205
|
+
station_name: Annotated[str, AfterValidator(colonisation_ship_not_allowed)] = Field(
|
|
206
|
+
alias="StationName"
|
|
207
|
+
)
|
|
208
|
+
distance_from_star: float = Field(alias="DistFromStarLS")
|
|
209
|
+
market_id: int | None = Field(default=None, alias="MarketID")
|
|
210
|
+
station_type: Annotated[str, AfterValidator(construction_type_not_allowed)] = Field(
|
|
211
|
+
alias="StationType"
|
|
212
|
+
)
|
|
213
|
+
is_planetary: bool = Field(default=False)
|
|
214
|
+
max_landing_pad_size: Literal["L", "M", "S"]
|
|
215
|
+
primary_economy: Annotated[str, BeforeValidator(map_economy)] = Field(
|
|
216
|
+
alias="StationEconomy"
|
|
217
|
+
)
|
|
218
|
+
secondary_economy: str | None = Field(default=None)
|
|
219
|
+
station_faction_name: str = Field(
|
|
220
|
+
validation_alias=AliasPath("StationFaction", "Name")
|
|
221
|
+
)
|
|
222
|
+
update_datetime: dt.datetime = Field(alias="timestamp")
|
|
223
|
+
|
|
224
|
+
@model_validator(mode="before")
|
|
225
|
+
@classmethod
|
|
226
|
+
def _secondary_economy(cls, data: dict) -> dict:
|
|
227
|
+
if len(data["StationEconomies"]) >= 2:
|
|
228
|
+
data["secondary_economy"] = map_economy(data["StationEconomies"][1]["Name"])
|
|
229
|
+
else:
|
|
230
|
+
data["secondary_economy"] = None
|
|
231
|
+
return data
|
|
232
|
+
|
|
233
|
+
@model_validator(mode="before")
|
|
234
|
+
@classmethod
|
|
235
|
+
def _is_planetary(cls, data: dict) -> dict:
|
|
236
|
+
data["is_planetary"] = data["StationType"] in planetary_station_types
|
|
237
|
+
return data
|
|
238
|
+
|
|
239
|
+
@model_validator(mode="before")
|
|
240
|
+
@classmethod
|
|
241
|
+
def _max_landing_pad_size(cls, data: dict) -> dict:
|
|
242
|
+
pads = data["LandingPads"]
|
|
243
|
+
if pads.get("Large", 0) > 0:
|
|
244
|
+
size = "L"
|
|
245
|
+
elif pads.get("Medium", 0) > 0:
|
|
246
|
+
size = "M"
|
|
247
|
+
else:
|
|
248
|
+
size = "S"
|
|
249
|
+
data["max_landing_pad_size"] = size
|
|
250
|
+
return data
|
|
251
|
+
|
|
252
|
+
@model_validator(mode="after")
|
|
253
|
+
def update_stronghold_carrier(self) -> Self:
|
|
254
|
+
if self.station_name in stronghold_carrier_names:
|
|
255
|
+
self.station_name = "Stronghold Carrier"
|
|
256
|
+
self.station_type = "StrongholdCarrier"
|
|
257
|
+
self.is_planetary = False
|
|
258
|
+
return self
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class Commodity(BaseModel):
|
|
262
|
+
"""A single commodity.
|
|
263
|
+
|
|
264
|
+
Aliases are defined so that an instance can be created from a record in the
|
|
265
|
+
commodities field of the commodity/3 event message.
|
|
266
|
+
"""
|
|
267
|
+
|
|
268
|
+
name: Annotated[str, BeforeValidator(map_commodity)]
|
|
269
|
+
mean_price: int = Field(alias="meanPrice")
|
|
270
|
+
buy_price: int = Field(alias="buyPrice")
|
|
271
|
+
stock: int
|
|
272
|
+
sell_price: int = Field(alias="sellPrice")
|
|
273
|
+
demand: int
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def exclude_zero_commodities(commodities: list[Commodity]) -> list[Commodity]:
|
|
277
|
+
"""Exclude commodities with zero supply & demand."""
|
|
278
|
+
return [c for c in commodities if not all([c.stock == 0, c.demand == 0])]
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class StationMarket(BaseModel):
|
|
282
|
+
"""List of commodities in a station/market.
|
|
283
|
+
|
|
284
|
+
Aliases are defined so that an instance can be created the message of the
|
|
285
|
+
commodity/3 event.
|
|
286
|
+
"""
|
|
287
|
+
|
|
288
|
+
market_id: int = Field(alias="marketId")
|
|
289
|
+
commodities: Annotated[
|
|
290
|
+
list[Commodity], AfterValidator(exclude_zero_commodities)
|
|
291
|
+
] = Field(default_factory=list, alias="commodities")
|
|
292
|
+
update_datetime: dt.datetime = Field(alias="timestamp")
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: edfh-data
|
|
3
|
+
Version: 0.5.2
|
|
4
|
+
Summary: Python library for the E:D Faction Hub application backend data services
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://codeberg.org/jdlbt/edfh-data
|
|
7
|
+
Project-URL: Issue tracker, https://codeberg.org/jdlbt/edfh-data/issues
|
|
8
|
+
Project-URL: Source, https://codeberg.org/jdlbt/edfh-data
|
|
9
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Requires-Python: >=3.13
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: fdev-ids
|
|
19
|
+
Requires-Dist: pydantic
|
|
20
|
+
Provides-Extra: db
|
|
21
|
+
Requires-Dist: PyMySQL; extra == "db"
|
|
22
|
+
Requires-Dist: sqlmodel; extra == "db"
|
|
23
|
+
Provides-Extra: eddn
|
|
24
|
+
Requires-Dist: edfh-data[db]; extra == "eddn"
|
|
25
|
+
Requires-Dist: orjson; extra == "eddn"
|
|
26
|
+
Requires-Dist: pika; extra == "eddn"
|
|
27
|
+
Requires-Dist: zmq; extra == "eddn"
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: edfh-data[db,eddn]; extra == "dev"
|
|
30
|
+
Requires-Dist: alembic; extra == "dev"
|
|
31
|
+
Requires-Dist: black==25.9.0; extra == "dev"
|
|
32
|
+
Requires-Dist: build; extra == "dev"
|
|
33
|
+
Requires-Dist: flake8==7.3.0; extra == "dev"
|
|
34
|
+
Requires-Dist: isort==6.1.0; extra == "dev"
|
|
35
|
+
Requires-Dist: pre-commit; extra == "dev"
|
|
36
|
+
Requires-Dist: python-dotenv; extra == "dev"
|
|
37
|
+
Requires-Dist: twine; extra == "dev"
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
# edfh-data
|
|
41
|
+
|
|
42
|
+
Python library for the E:D Faction Hub application backend data services:
|
|
43
|
+
|
|
44
|
+
- EDDN listener service, receives messages from the EDDN stream and publishes them to a RabbitMQ exchange.
|
|
45
|
+
- EDDN handler service, polls message from a RabbitMQ queue, parses them and stores the relevant info in a relational database.
|
|
46
|
+
|
|
47
|
+
## Requirements
|
|
48
|
+
|
|
49
|
+
- Python 3.13 available in the sytem path.
|
|
50
|
+
- Running RabbitMQ and MariaDB instances (the included `docker-compose.yaml` file can set them up for development).
|
|
51
|
+
|
|
52
|
+
## Configuration
|
|
53
|
+
|
|
54
|
+
The following configuration variables are defined are defined as environment variables, or, for local development, specified in a `.env` configuration file:
|
|
55
|
+
|
|
56
|
+
```ini
|
|
57
|
+
DB_USER=dbuser
|
|
58
|
+
DB_PASSWD=<db_password>
|
|
59
|
+
DB_HOST=localhost
|
|
60
|
+
DB_PORT=3306
|
|
61
|
+
DB_NAME=fgs_infra_db
|
|
62
|
+
RMQ_USER=rmquser
|
|
63
|
+
RMQ_PASSWD=<rabbitmq_password>
|
|
64
|
+
RMQ_HOST=localhost
|
|
65
|
+
RMQ_HANDLER_PREFETCH=10
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Usage
|
|
69
|
+
|
|
70
|
+
Install the package with the `eddn` extra:
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
python -m pip install edfh-data[eddn]
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Launch the EDDN message handler service with:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
eddn-handle
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Launch the EDDN listener service with:
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
eddn-listen
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Development
|
|
89
|
+
|
|
90
|
+
### Python environment
|
|
91
|
+
|
|
92
|
+
Create a virtual environment using Python 3.13:
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
python3.13 -m venv .venv --prompt edfh-data --upgrade-deps
|
|
96
|
+
source .venv/bin/activate
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Install the package in development mode along with the development dependencies:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
python -m pip install -e .[dev]
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Install pre-commit hooks:
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
pre-commit install
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Database service
|
|
112
|
+
|
|
113
|
+
Migrate the database with:
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
alembic upgrade head
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Generate a new revision:
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
alembic revision --autogenerate -m "Revision message"
|
|
123
|
+
```
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
edfh_data/__init__.py,sha256=isJrmDBLRag7Zc2UK9ZovWGOv7ji1Oh-zJtJMNJFkXw,22
|
|
2
|
+
edfh_data/exceptions.py,sha256=AxBIxYYBciw17UrGspx2_r95ZBwNgBs7Q3Jz-tRdIq8,258
|
|
3
|
+
edfh_data/models.py,sha256=otcDn_G8by1PZDyhlxuXBVVkudZVWetWHA2x6yYXQ04,10051
|
|
4
|
+
edfh_data/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
edfh_data/db/crud.py,sha256=0vFWX2raeo1gzmu4BjHrX10h1HK81iOJP4WCc_z6ZsY,7786
|
|
6
|
+
edfh_data/db/models.py,sha256=3W9zI7S_Jda4vXK5-Cth7vQC-xcu32yEheYpFBrKe8w,6000
|
|
7
|
+
edfh_data/db/utils.py,sha256=lc403cOPDkbTbfGjMtf0V0WNLpj8j5scMkqZzNEyTJQ,1302
|
|
8
|
+
edfh_data/eddn/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
edfh_data/eddn/handlers.py,sha256=hvJPbdPvThIOadoZhlw44G4r0PxuA7-kV8-7FrDsfuA,4391
|
|
10
|
+
edfh_data/eddn/listener.py,sha256=-yGCrwiYWGrZSwS5JpyOORXNbsfTA1fDs0M-nTYt4uk,2592
|
|
11
|
+
edfh_data-0.5.2.dist-info/licenses/LICENSE,sha256=U1KpKZcXdpnNNT4Xh5cA-aRMvC0eaDEu_azpC70VT_I,1062
|
|
12
|
+
edfh_data-0.5.2.dist-info/METADATA,sha256=2fvxWecaFZ7CEwJ7D-d5FCfeLSUQX219XhA-ORsK4G8,3108
|
|
13
|
+
edfh_data-0.5.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
14
|
+
edfh_data-0.5.2.dist-info/entry_points.txt,sha256=NeJ61ITr5YFhdh-p2akshiPQF8LI9530iUD4SYko_CU,102
|
|
15
|
+
edfh_data-0.5.2.dist-info/top_level.txt,sha256=RsVo4mZIyr5Q_PZQ_-ix9ec1FHhvXra_8c-HIZA2nK8,10
|
|
16
|
+
edfh_data-0.5.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 jdlbt
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
edfh_data
|