tradedangerous 12.7.6__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.
- py.typed +1 -0
- trade.py +49 -0
- tradedangerous/__init__.py +43 -0
- tradedangerous/cache.py +1381 -0
- tradedangerous/cli.py +136 -0
- tradedangerous/commands/TEMPLATE.py +74 -0
- tradedangerous/commands/__init__.py +244 -0
- tradedangerous/commands/buildcache_cmd.py +102 -0
- tradedangerous/commands/buy_cmd.py +427 -0
- tradedangerous/commands/commandenv.py +372 -0
- tradedangerous/commands/exceptions.py +94 -0
- tradedangerous/commands/export_cmd.py +150 -0
- tradedangerous/commands/import_cmd.py +222 -0
- tradedangerous/commands/local_cmd.py +243 -0
- tradedangerous/commands/market_cmd.py +207 -0
- tradedangerous/commands/nav_cmd.py +252 -0
- tradedangerous/commands/olddata_cmd.py +270 -0
- tradedangerous/commands/parsing.py +221 -0
- tradedangerous/commands/rares_cmd.py +298 -0
- tradedangerous/commands/run_cmd.py +1521 -0
- tradedangerous/commands/sell_cmd.py +262 -0
- tradedangerous/commands/shipvendor_cmd.py +60 -0
- tradedangerous/commands/station_cmd.py +68 -0
- tradedangerous/commands/trade_cmd.py +181 -0
- tradedangerous/commands/update_cmd.py +67 -0
- tradedangerous/corrections.py +55 -0
- tradedangerous/csvexport.py +234 -0
- tradedangerous/db/__init__.py +27 -0
- tradedangerous/db/adapter.py +192 -0
- tradedangerous/db/config.py +107 -0
- tradedangerous/db/engine.py +259 -0
- tradedangerous/db/lifecycle.py +332 -0
- tradedangerous/db/locks.py +208 -0
- tradedangerous/db/orm_models.py +500 -0
- tradedangerous/db/paths.py +113 -0
- tradedangerous/db/utils.py +661 -0
- tradedangerous/edscupdate.py +565 -0
- tradedangerous/edsmupdate.py +474 -0
- tradedangerous/formatting.py +210 -0
- tradedangerous/fs.py +156 -0
- tradedangerous/gui.py +1146 -0
- tradedangerous/mapping.py +133 -0
- tradedangerous/mfd/__init__.py +103 -0
- tradedangerous/mfd/saitek/__init__.py +3 -0
- tradedangerous/mfd/saitek/directoutput.py +678 -0
- tradedangerous/mfd/saitek/x52pro.py +195 -0
- tradedangerous/misc/checkpricebounds.py +287 -0
- tradedangerous/misc/clipboard.py +49 -0
- tradedangerous/misc/coord64.py +83 -0
- tradedangerous/misc/csvdialect.py +57 -0
- tradedangerous/misc/derp-sentinel.py +35 -0
- tradedangerous/misc/diff-system-csvs.py +159 -0
- tradedangerous/misc/eddb.py +81 -0
- tradedangerous/misc/eddn.py +349 -0
- tradedangerous/misc/edsc.py +437 -0
- tradedangerous/misc/edsm.py +121 -0
- tradedangerous/misc/importeddbstats.py +54 -0
- tradedangerous/misc/prices-json-exp.py +179 -0
- tradedangerous/misc/progress.py +194 -0
- tradedangerous/plugins/__init__.py +249 -0
- tradedangerous/plugins/edcd_plug.py +371 -0
- tradedangerous/plugins/eddblink_plug.py +861 -0
- tradedangerous/plugins/edmc_batch_plug.py +133 -0
- tradedangerous/plugins/spansh_plug.py +2647 -0
- tradedangerous/prices.py +211 -0
- tradedangerous/submit-distances.py +422 -0
- tradedangerous/templates/Added.csv +37 -0
- tradedangerous/templates/Category.csv +17 -0
- tradedangerous/templates/RareItem.csv +143 -0
- tradedangerous/templates/TradeDangerous.sql +338 -0
- tradedangerous/tools.py +40 -0
- tradedangerous/tradecalc.py +1302 -0
- tradedangerous/tradedb.py +2320 -0
- tradedangerous/tradeenv.py +313 -0
- tradedangerous/tradeenv.pyi +109 -0
- tradedangerous/tradeexcept.py +131 -0
- tradedangerous/tradeorm.py +183 -0
- tradedangerous/transfers.py +192 -0
- tradedangerous/utils.py +243 -0
- tradedangerous/version.py +16 -0
- tradedangerous-12.7.6.dist-info/METADATA +106 -0
- tradedangerous-12.7.6.dist-info/RECORD +87 -0
- tradedangerous-12.7.6.dist-info/WHEEL +5 -0
- tradedangerous-12.7.6.dist-info/entry_points.txt +3 -0
- tradedangerous-12.7.6.dist-info/licenses/LICENSE +373 -0
- tradedangerous-12.7.6.dist-info/top_level.txt +2 -0
- tradegui.py +24 -0
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
# tradedangerous/db/orm_models.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Optional
|
|
5
|
+
import datetime
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import (
|
|
8
|
+
MetaData,
|
|
9
|
+
ForeignKey,
|
|
10
|
+
Integer,
|
|
11
|
+
BigInteger,
|
|
12
|
+
String,
|
|
13
|
+
CHAR,
|
|
14
|
+
Enum,
|
|
15
|
+
Index,
|
|
16
|
+
UniqueConstraint,
|
|
17
|
+
CheckConstraint,
|
|
18
|
+
text,
|
|
19
|
+
Column,
|
|
20
|
+
DateTime,
|
|
21
|
+
)
|
|
22
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
|
23
|
+
from sqlalchemy.sql import expression
|
|
24
|
+
from sqlalchemy.ext.compiler import compiles
|
|
25
|
+
from sqlalchemy.types import TypeDecorator
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ---- Dialect-aware time utilities (moved before model usage) ----
|
|
29
|
+
class now6(expression.FunctionElement):
|
|
30
|
+
"""CURRENT_TIMESTAMP with microseconds on MySQL/MariaDB; plain CURRENT_TIMESTAMP elsewhere."""
|
|
31
|
+
type = DateTime()
|
|
32
|
+
inherit_cache = True
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@compiles(now6, "mysql")
|
|
36
|
+
@compiles(now6, "mariadb")
|
|
37
|
+
def _mysql_now6(element, compiler, **kw):
|
|
38
|
+
return "CURRENT_TIMESTAMP(6)"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@compiles(now6)
|
|
42
|
+
def _default_now(element, compiler, **kw):
|
|
43
|
+
return "CURRENT_TIMESTAMP"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class DateTime6(TypeDecorator):
|
|
47
|
+
"""DATETIME that is DATETIME(6) on MySQL/MariaDB, generic DateTime elsewhere. Always UTC."""
|
|
48
|
+
impl = DateTime
|
|
49
|
+
cache_ok = True
|
|
50
|
+
|
|
51
|
+
def load_dialect_impl(self, dialect):
|
|
52
|
+
if dialect.name in ("mysql", "mariadb"):
|
|
53
|
+
from sqlalchemy.dialects.mysql import DATETIME as _MYSQL_DATETIME
|
|
54
|
+
return dialect.type_descriptor(_MYSQL_DATETIME(fsp=6))
|
|
55
|
+
return dialect.type_descriptor(DateTime(timezone=True))
|
|
56
|
+
|
|
57
|
+
def process_result_value(self, value, dialect):
|
|
58
|
+
"""Ensure all datetimes loaded from DB are UTC-aware."""
|
|
59
|
+
if value is not None and value.tzinfo is None:
|
|
60
|
+
# Database stored naive datetime; treat it as UTC
|
|
61
|
+
return value.replace(tzinfo=datetime.timezone.utc)
|
|
62
|
+
return value
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ---------- Dialect Helpers --------
|
|
66
|
+
class CIString(TypeDecorator):
|
|
67
|
+
"""
|
|
68
|
+
Case-insensitive string type.
|
|
69
|
+
- SQLite → uses NOCASE collation
|
|
70
|
+
- MySQL/MariaDB → uses utf8mb4_unicode_ci
|
|
71
|
+
- Others → plain String
|
|
72
|
+
"""
|
|
73
|
+
impl = String
|
|
74
|
+
cache_ok = True
|
|
75
|
+
|
|
76
|
+
def __init__(self, length, **kwargs):
|
|
77
|
+
super().__init__(length=length, **kwargs)
|
|
78
|
+
|
|
79
|
+
def load_dialect_impl(self, dialect):
|
|
80
|
+
if dialect.name == "sqlite":
|
|
81
|
+
return dialect.type_descriptor(String(self.impl.length, collation="NOCASE"))
|
|
82
|
+
elif dialect.name in ("mysql", "mariadb"):
|
|
83
|
+
return dialect.type_descriptor(String(self.impl.length, collation="utf8mb4_unicode_ci"))
|
|
84
|
+
else:
|
|
85
|
+
return dialect.type_descriptor(String(self.impl.length))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ---------- Naming & Base ----------
|
|
89
|
+
naming_convention = {
|
|
90
|
+
"ix": "ix_%(table_name)s__%(column_0_N_name)s",
|
|
91
|
+
"uq": "uq_%(table_name)s__%(column_0_N_name)s",
|
|
92
|
+
"ck": "ck_%(table_name)s__%(column_0_name)s", # use column name, not constraint_name
|
|
93
|
+
"fk": "fk_%(table_name)s__%(column_0_N_name)s__%(referred_table_name)s",
|
|
94
|
+
"pk": "pk_%(table_name)s",
|
|
95
|
+
}
|
|
96
|
+
metadata = MetaData(naming_convention=naming_convention)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class Base(DeclarativeBase):
|
|
100
|
+
metadata = metadata
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ---------- Enums ----------
|
|
104
|
+
TriState = Enum(
|
|
105
|
+
"Y",
|
|
106
|
+
"N",
|
|
107
|
+
"?",
|
|
108
|
+
native_enum=False,
|
|
109
|
+
create_constraint=True,
|
|
110
|
+
validate_strings=True,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
PadSize = Enum(
|
|
114
|
+
"S",
|
|
115
|
+
"M",
|
|
116
|
+
"L",
|
|
117
|
+
"?",
|
|
118
|
+
native_enum=False,
|
|
119
|
+
create_constraint=True,
|
|
120
|
+
validate_strings=True,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ---------- Core Domain ----------
|
|
125
|
+
class Added(Base):
|
|
126
|
+
""" Added table was originally introduced to help identify whether things like
|
|
127
|
+
Systems represented data that was present in specific releases of the game,
|
|
128
|
+
such as pre-alpha, beta, etc. """
|
|
129
|
+
__tablename__ = "Added"
|
|
130
|
+
|
|
131
|
+
added_id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
132
|
+
name: Mapped[str] = mapped_column(CIString(128), nullable=False, unique=True)
|
|
133
|
+
|
|
134
|
+
# Relationships
|
|
135
|
+
systems: Mapped[list["System"]] = relationship(back_populates="added")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class System(Base):
|
|
139
|
+
""" System represents the game's concept of a Star System or a group of bodies
|
|
140
|
+
orbiting a barycenter - or in game terms, things you can FSD jump between. """
|
|
141
|
+
__tablename__ = "System"
|
|
142
|
+
|
|
143
|
+
system_id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
|
144
|
+
name: Mapped[str] = mapped_column(CIString(128), nullable=False)
|
|
145
|
+
pos_x: Mapped[float] = mapped_column(nullable=False)
|
|
146
|
+
pos_y: Mapped[float] = mapped_column(nullable=False)
|
|
147
|
+
pos_z: Mapped[float] = mapped_column(nullable=False)
|
|
148
|
+
added_id: Mapped[int | None] = mapped_column(
|
|
149
|
+
ForeignKey("Added.added_id", onupdate="CASCADE", ondelete="CASCADE")
|
|
150
|
+
)
|
|
151
|
+
modified: Mapped[str] = mapped_column(
|
|
152
|
+
DateTime6(),
|
|
153
|
+
server_default=now6(),
|
|
154
|
+
onupdate=now6(),
|
|
155
|
+
nullable=False,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def dbname(self) -> str:
|
|
159
|
+
return f"{self.name.upper()}/"
|
|
160
|
+
|
|
161
|
+
# Relationships
|
|
162
|
+
added: Mapped[Optional["Added"]] = relationship(back_populates="systems")
|
|
163
|
+
stations: Mapped[list["Station"]] = relationship(back_populates="system", cascade="all, delete-orphan")
|
|
164
|
+
|
|
165
|
+
__table_args__ = (
|
|
166
|
+
Index("idx_system_by_pos", "pos_x", "pos_y", "pos_z", "system_id"),
|
|
167
|
+
Index("idx_system_by_name", "name"),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class Station(Base):
|
|
172
|
+
""" Station represents a facility you can land/dock at and do things like trade, etc. """
|
|
173
|
+
__tablename__ = "Station"
|
|
174
|
+
|
|
175
|
+
station_id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
|
176
|
+
name: Mapped[str] = mapped_column(CIString(128), nullable=False)
|
|
177
|
+
|
|
178
|
+
def dbname(self) -> str:
|
|
179
|
+
return f"{self.system.name}/{self.name}"
|
|
180
|
+
|
|
181
|
+
# type widened; cascade semantics unchanged (DELETE only)
|
|
182
|
+
system_id: Mapped[int] = mapped_column(
|
|
183
|
+
BigInteger,
|
|
184
|
+
ForeignKey("System.system_id", ondelete="CASCADE"),
|
|
185
|
+
nullable=False,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
ls_from_star: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text("0"))
|
|
189
|
+
|
|
190
|
+
blackmarket: Mapped[str] = mapped_column(TriState, nullable=False, server_default=text("'?'"))
|
|
191
|
+
max_pad_size: Mapped[str] = mapped_column(PadSize, nullable=False, server_default=text("'?'"))
|
|
192
|
+
market: Mapped[str] = mapped_column(TriState, nullable=False, server_default=text("'?'"))
|
|
193
|
+
shipyard: Mapped[str] = mapped_column(TriState, nullable=False, server_default=text("'?'"))
|
|
194
|
+
outfitting: Mapped[str] = mapped_column(TriState, nullable=False, server_default=text("'?'"))
|
|
195
|
+
rearm: Mapped[str] = mapped_column(TriState, nullable=False, server_default=text("'?'"))
|
|
196
|
+
refuel: Mapped[str] = mapped_column(TriState, nullable=False, server_default=text("'?'"))
|
|
197
|
+
repair: Mapped[str] = mapped_column(TriState, nullable=False, server_default=text("'?'"))
|
|
198
|
+
planetary: Mapped[str] = mapped_column(TriState, nullable=False, server_default=text("'?'"))
|
|
199
|
+
|
|
200
|
+
type_id: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text("0"))
|
|
201
|
+
modified: Mapped[str] = mapped_column(DateTime6(), server_default=now6(), onupdate=now6(), nullable=False)
|
|
202
|
+
|
|
203
|
+
# Relationships
|
|
204
|
+
system: Mapped["System"] = relationship(back_populates="stations")
|
|
205
|
+
items: Mapped[list["StationItem"]] = relationship(back_populates="station", cascade="all, delete-orphan")
|
|
206
|
+
ship_vendors: Mapped[list["ShipVendor"]] = relationship(back_populates="station", cascade="all, delete-orphan")
|
|
207
|
+
upgrade_vendors: Mapped[list["UpgradeVendor"]] = relationship(back_populates="station", cascade="all, delete-orphan")
|
|
208
|
+
|
|
209
|
+
__table_args__ = (
|
|
210
|
+
Index("idx_station_by_system", "system_id"),
|
|
211
|
+
Index("idx_station_by_name", "name"),
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class Category(Base):
|
|
216
|
+
""" Category provides groupings used by tradeable commodities: Food, Minerals, ... """
|
|
217
|
+
__tablename__ = "Category"
|
|
218
|
+
|
|
219
|
+
category_id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
220
|
+
name: Mapped[str] = mapped_column(CIString(128), nullable=False)
|
|
221
|
+
|
|
222
|
+
# Relationships
|
|
223
|
+
items: Mapped[list["Item"]] = relationship(back_populates="category")
|
|
224
|
+
|
|
225
|
+
__table_args__ = (Index("idx_category_by_name", "name"),)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class Item(Base):
|
|
229
|
+
""" Item represents the types of in-game tradeable commodities. """
|
|
230
|
+
__tablename__ = "Item"
|
|
231
|
+
|
|
232
|
+
item_id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
233
|
+
name: Mapped[str] = mapped_column(CIString(128), nullable=False)
|
|
234
|
+
category_id: Mapped[int] = mapped_column(
|
|
235
|
+
ForeignKey("Category.category_id", onupdate="CASCADE", ondelete="CASCADE"),
|
|
236
|
+
nullable=False,
|
|
237
|
+
)
|
|
238
|
+
ui_order: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text("0"))
|
|
239
|
+
avg_price: Mapped[int | None] = mapped_column(Integer)
|
|
240
|
+
fdev_id: Mapped[int | None] = mapped_column(Integer)
|
|
241
|
+
|
|
242
|
+
# Relationships
|
|
243
|
+
category: Mapped["Category"] = relationship(back_populates="items")
|
|
244
|
+
stations: Mapped[list["StationItem"]] = relationship(back_populates="item", cascade="all, delete-orphan")
|
|
245
|
+
|
|
246
|
+
# Helper fields
|
|
247
|
+
def dbname(self, detail: int | bool = 0) -> str:
|
|
248
|
+
if detail:
|
|
249
|
+
return f"{self.category.name}/{self.name}"
|
|
250
|
+
return self.name
|
|
251
|
+
|
|
252
|
+
__table_args__ = (
|
|
253
|
+
Index("idx_item_by_fdevid", "fdev_id"),
|
|
254
|
+
Index("idx_item_by_category", "category_id"),
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class StationItem(Base):
|
|
259
|
+
""" StationItem represents the tradeability of a commodity (Item) at a particular
|
|
260
|
+
market facility (Station).
|
|
261
|
+
|
|
262
|
+
Originally data was manually input into a text-file designed to look like
|
|
263
|
+
the in-game Market screen where the 30-40 items available were listed
|
|
264
|
+
with side-by-side sell/buy prices. This visual equivalence made data-entry
|
|
265
|
+
efficient.
|
|
266
|
+
|
|
267
|
+
The collection of those forms made the ".prices" file, which was originally
|
|
268
|
+
Source of Truth for TradeDangerous.
|
|
269
|
+
|
|
270
|
+
The fact we have buying and selling prices adjacent to each other in this
|
|
271
|
+
table is a vestigial hangover of that early design. """
|
|
272
|
+
|
|
273
|
+
__tablename__ = "StationItem"
|
|
274
|
+
|
|
275
|
+
station_id: Mapped[int] = mapped_column(
|
|
276
|
+
BigInteger,
|
|
277
|
+
ForeignKey("Station.station_id", ondelete="CASCADE", onupdate="CASCADE"),
|
|
278
|
+
primary_key=True,
|
|
279
|
+
)
|
|
280
|
+
item_id: Mapped[int] = mapped_column(
|
|
281
|
+
ForeignKey("Item.item_id", ondelete="CASCADE", onupdate="CASCADE"),
|
|
282
|
+
primary_key=True,
|
|
283
|
+
)
|
|
284
|
+
demand_price: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
285
|
+
demand_units: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
286
|
+
demand_level: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
287
|
+
supply_price: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
288
|
+
supply_units: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
289
|
+
supply_level: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
290
|
+
modified: Mapped[str] = mapped_column(DateTime6(), server_default=now6(), onupdate=now6(), nullable=False)
|
|
291
|
+
from_live: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text("0"))
|
|
292
|
+
|
|
293
|
+
# Relationships
|
|
294
|
+
station: Mapped["Station"] = relationship(back_populates="items")
|
|
295
|
+
item: Mapped["Item"] = relationship(back_populates="stations")
|
|
296
|
+
|
|
297
|
+
__table_args__ = (
|
|
298
|
+
Index("si_mod_stn_itm", "modified", "station_id", "item_id"),
|
|
299
|
+
Index("si_itm_dmdpr", "item_id", "demand_price", sqlite_where=text("demand_price > 0")),
|
|
300
|
+
Index("si_itm_suppr", "item_id", "supply_price", sqlite_where=text("supply_price > 0")),
|
|
301
|
+
{"sqlite_with_rowid": False},
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
class Ship(Base):
|
|
306
|
+
""" Ship provides the fundamental classes of ships that the player can purchase in-game. """
|
|
307
|
+
__tablename__ = "Ship"
|
|
308
|
+
|
|
309
|
+
ship_id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
310
|
+
name: Mapped[str] = mapped_column(CIString(128), nullable=False)
|
|
311
|
+
cost: Mapped[int | None] = mapped_column(Integer)
|
|
312
|
+
|
|
313
|
+
# Relationships
|
|
314
|
+
vendors: Mapped[list["ShipVendor"]] = relationship(back_populates="ship")
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class ShipVendor(Base):
|
|
318
|
+
""" ShipVendor is used to track where specific ships can be purchased. """
|
|
319
|
+
__tablename__ = "ShipVendor"
|
|
320
|
+
|
|
321
|
+
ship_id: Mapped[int] = mapped_column(
|
|
322
|
+
ForeignKey("Ship.ship_id", ondelete="CASCADE", onupdate="CASCADE"),
|
|
323
|
+
primary_key=True,
|
|
324
|
+
)
|
|
325
|
+
station_id: Mapped[int] = mapped_column(
|
|
326
|
+
BigInteger,
|
|
327
|
+
ForeignKey("Station.station_id", ondelete="CASCADE", onupdate="CASCADE"),
|
|
328
|
+
primary_key=True,
|
|
329
|
+
)
|
|
330
|
+
modified: Mapped[str] = mapped_column(DateTime6(), server_default=now6(), onupdate=now6(), nullable=False)
|
|
331
|
+
|
|
332
|
+
# Relationships
|
|
333
|
+
ship: Mapped["Ship"] = relationship(back_populates="vendors")
|
|
334
|
+
station: Mapped["Station"] = relationship(back_populates="ship_vendors")
|
|
335
|
+
|
|
336
|
+
__table_args__ = (Index("idx_shipvendor_by_station", "station_id"),)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
class Upgrade(Base):
|
|
340
|
+
""" Upgrade represents what Frontier call 'Outfitting', components that can
|
|
341
|
+
be acquired to upgrade your instance of a ship. """
|
|
342
|
+
__tablename__ = "Upgrade"
|
|
343
|
+
|
|
344
|
+
upgrade_id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
345
|
+
name: Mapped[str] = mapped_column(CIString(128), nullable=False)
|
|
346
|
+
class_: Mapped[int] = mapped_column("class", Integer, nullable=False)
|
|
347
|
+
rating: Mapped[str] = mapped_column(CHAR(1), nullable=False)
|
|
348
|
+
ship: Mapped[str | None] = mapped_column(CIString(128))
|
|
349
|
+
|
|
350
|
+
# Relationships
|
|
351
|
+
vendors: Mapped[list["UpgradeVendor"]] = relationship(back_populates="upgrade")
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
class UpgradeVendor(Base):
|
|
355
|
+
""" UpgradeVendor tracks all the locations where Outfitting upgrades can be
|
|
356
|
+
acquired in the game universe. """
|
|
357
|
+
__tablename__ = "UpgradeVendor"
|
|
358
|
+
|
|
359
|
+
upgrade_id: Mapped[int] = mapped_column(
|
|
360
|
+
ForeignKey("Upgrade.upgrade_id", ondelete="CASCADE", onupdate="CASCADE"),
|
|
361
|
+
primary_key=True,
|
|
362
|
+
)
|
|
363
|
+
station_id: Mapped[int] = mapped_column(
|
|
364
|
+
BigInteger,
|
|
365
|
+
ForeignKey("Station.station_id", ondelete="CASCADE", onupdate="CASCADE"),
|
|
366
|
+
primary_key=True,
|
|
367
|
+
)
|
|
368
|
+
modified: Mapped[str] = mapped_column(DateTime6(), nullable=False, server_default=now6(), onupdate=now6())
|
|
369
|
+
|
|
370
|
+
# Relationships
|
|
371
|
+
upgrade: Mapped["Upgrade"] = relationship(back_populates="vendors")
|
|
372
|
+
station: Mapped["Station"] = relationship(back_populates="upgrade_vendors")
|
|
373
|
+
|
|
374
|
+
__table_args__ = (Index("idx_vendor_by_station_id", "station_id"),)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
class RareItem(Base): # [[deprecated]]
|
|
378
|
+
""" RareItem is used to track specialized commodities that Frontier introduced during the
|
|
379
|
+
early days of the game.
|
|
380
|
+
@deprecated These are now just included in the standard Item catalog. """
|
|
381
|
+
__tablename__ = "RareItem"
|
|
382
|
+
|
|
383
|
+
rare_id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
384
|
+
station_id: Mapped[int] = mapped_column(
|
|
385
|
+
BigInteger,
|
|
386
|
+
ForeignKey("Station.station_id", ondelete="CASCADE", onupdate="CASCADE"),
|
|
387
|
+
nullable=False,
|
|
388
|
+
)
|
|
389
|
+
category_id: Mapped[int] = mapped_column(
|
|
390
|
+
ForeignKey("Category.category_id", onupdate="CASCADE", ondelete="CASCADE"),
|
|
391
|
+
nullable=False,
|
|
392
|
+
)
|
|
393
|
+
name: Mapped[str] = mapped_column(CIString(128), nullable=False)
|
|
394
|
+
cost: Mapped[int | None] = mapped_column(Integer)
|
|
395
|
+
max_allocation: Mapped[int | None] = mapped_column(Integer)
|
|
396
|
+
illegal: Mapped[str] = mapped_column(TriState, nullable=False, server_default=text("'?'"))
|
|
397
|
+
suppressed: Mapped[str] = mapped_column(TriState, nullable=False, server_default=text("'?'"))
|
|
398
|
+
|
|
399
|
+
__table_args__ = (UniqueConstraint("name", name="uq_rareitem_name"),)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
class FDevShipyard(Base):
|
|
403
|
+
""" FDevShipyard is a vestigial bridge between originally crowd-sourced ship information,
|
|
404
|
+
and the data that is now available thanks to frontier's journal logs. """
|
|
405
|
+
__tablename__ = "FDevShipyard"
|
|
406
|
+
|
|
407
|
+
id = Column(Integer, primary_key=True, unique=True, nullable=False)
|
|
408
|
+
symbol = Column(CIString(128))
|
|
409
|
+
name = Column(CIString(128))
|
|
410
|
+
entitlement = Column(String(50))
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
class FDevOutfitting(Base):
|
|
414
|
+
""" FDevOutfitting is a vestigial bridge between originally crowd-sourced outfitting (upgrade)
|
|
415
|
+
information and the data that has been auto-scraped from frontier's journal logs. """
|
|
416
|
+
__tablename__ = "FDevOutfitting"
|
|
417
|
+
|
|
418
|
+
id = Column(Integer, primary_key=True, unique=True, nullable=False)
|
|
419
|
+
symbol = Column(CIString(128))
|
|
420
|
+
category = Column(String(10))
|
|
421
|
+
name = Column(CIString(128))
|
|
422
|
+
mount = Column(String(20))
|
|
423
|
+
guidance = Column(String(20))
|
|
424
|
+
ship = Column(CIString(128))
|
|
425
|
+
class_ = Column("class", String(1), nullable=False)
|
|
426
|
+
rating = Column(String(1), nullable=False)
|
|
427
|
+
entitlement = Column(String(50))
|
|
428
|
+
|
|
429
|
+
__table_args__ = (
|
|
430
|
+
CheckConstraint(
|
|
431
|
+
"category IN ('hardpoint','internal','standard','utility')",
|
|
432
|
+
name="ck_fdo_category",
|
|
433
|
+
),
|
|
434
|
+
CheckConstraint(
|
|
435
|
+
"(mount IN ('Fixed','Gimballed','Turreted')) OR (mount IS NULL)",
|
|
436
|
+
name="ck_fdo_mount",
|
|
437
|
+
),
|
|
438
|
+
CheckConstraint(
|
|
439
|
+
"(guidance IN ('Dumbfire','Seeker','Swarm')) OR (guidance IS NULL)",
|
|
440
|
+
name="ck_fdo_guidance",
|
|
441
|
+
),
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
# ---------- Control & Staging ----------
|
|
446
|
+
class ExportControl(Base):
|
|
447
|
+
"""
|
|
448
|
+
Singleton control row for hybrid export/watermarking.
|
|
449
|
+
- id: always 1
|
|
450
|
+
- last_full_dump_time: watermark
|
|
451
|
+
- last_reset_key: optional cursor for chunked from_live resets
|
|
452
|
+
"""
|
|
453
|
+
__tablename__ = "ExportControl"
|
|
454
|
+
|
|
455
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True, server_default=text("1"))
|
|
456
|
+
last_full_dump_time: Mapped[str] = mapped_column(DateTime6(), nullable=False)
|
|
457
|
+
last_reset_key: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
class StationItemStaging(Base):
|
|
461
|
+
"""
|
|
462
|
+
Staging table for bulk loads (no FKs). Same columns as StationItem.
|
|
463
|
+
"""
|
|
464
|
+
__tablename__ = "StationItem_staging"
|
|
465
|
+
|
|
466
|
+
station_id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
|
467
|
+
item_id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
468
|
+
demand_price: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
469
|
+
demand_units: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
470
|
+
demand_level: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
471
|
+
supply_price: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
472
|
+
supply_units: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
473
|
+
supply_level: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
474
|
+
modified: Mapped[str] = mapped_column(DateTime6(), server_default=now6(), onupdate=now6(), nullable=False)
|
|
475
|
+
from_live: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text("0"))
|
|
476
|
+
|
|
477
|
+
__table_args__ = (Index("idx_sistaging_stn_itm", "station_id", "item_id"),)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
__all__ = [
|
|
481
|
+
# Base
|
|
482
|
+
"Base",
|
|
483
|
+
# Core
|
|
484
|
+
"Added",
|
|
485
|
+
"System",
|
|
486
|
+
"Station",
|
|
487
|
+
"Category",
|
|
488
|
+
"Item",
|
|
489
|
+
"StationItem",
|
|
490
|
+
"Ship",
|
|
491
|
+
"ShipVendor",
|
|
492
|
+
"Upgrade",
|
|
493
|
+
"UpgradeVendor",
|
|
494
|
+
"RareItem",
|
|
495
|
+
"FDevShipyard",
|
|
496
|
+
"FDevOutfitting",
|
|
497
|
+
# Control & staging
|
|
498
|
+
"ExportControl",
|
|
499
|
+
"StationItemStaging",
|
|
500
|
+
]
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# tradedangerous/db/paths.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
import configparser
|
|
10
|
+
except Exception: # pragma: no cover
|
|
11
|
+
configparser = None # type: ignore
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"ensure_dir",
|
|
15
|
+
"resolve_data_dir",
|
|
16
|
+
"resolve_tmp_dir",
|
|
17
|
+
"resolve_db_config_path",
|
|
18
|
+
"get_sqlite_db_path",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
# --------------------------
|
|
22
|
+
# Helpers that tolerate either ConfigParser or dict
|
|
23
|
+
# --------------------------
|
|
24
|
+
|
|
25
|
+
def _is_cfg(obj: Any) -> bool:
|
|
26
|
+
return hasattr(obj, "has_option") and hasattr(obj, "get")
|
|
27
|
+
|
|
28
|
+
def _get_opt(cfg: Any, section: str, key: str, default: str | None = None) -> str | None:
|
|
29
|
+
"""Return option from either a ConfigParser-like object or a nested dict.
|
|
30
|
+
Falls back to [database] overlay for convenience, matching legacy behaviour.
|
|
31
|
+
Safe even if cfg is None.
|
|
32
|
+
"""
|
|
33
|
+
if cfg is None:
|
|
34
|
+
return default
|
|
35
|
+
# ConfigParser branch
|
|
36
|
+
if _is_cfg(cfg):
|
|
37
|
+
try:
|
|
38
|
+
if cfg.has_option(section, key):
|
|
39
|
+
return cfg.get(section, key) # type: ignore[arg-type]
|
|
40
|
+
if cfg.has_option("database", key):
|
|
41
|
+
return cfg.get("database", key) # type: ignore[arg-type]
|
|
42
|
+
except Exception:
|
|
43
|
+
return default
|
|
44
|
+
return default
|
|
45
|
+
# Mapping/dict branch
|
|
46
|
+
try:
|
|
47
|
+
sec = (cfg or {}).get(section, {}) or {}
|
|
48
|
+
if key in sec and sec[key] not in (None, ""):
|
|
49
|
+
return sec[key]
|
|
50
|
+
db = (cfg or {}).get("database", {}) or {}
|
|
51
|
+
if key in db and db[key] not in (None, ""):
|
|
52
|
+
return db[key]
|
|
53
|
+
except Exception:
|
|
54
|
+
pass
|
|
55
|
+
return default
|
|
56
|
+
|
|
57
|
+
def _resolve_dir(default_rel: str, env_key: str, cfg_value: str | None) -> Path:
|
|
58
|
+
cand = os.getenv(env_key) or (cfg_value or default_rel)
|
|
59
|
+
p = Path(cand).expanduser()
|
|
60
|
+
return p if p.is_absolute() else (Path.cwd() / p)
|
|
61
|
+
|
|
62
|
+
# --------------------------
|
|
63
|
+
# Public API
|
|
64
|
+
# --------------------------
|
|
65
|
+
|
|
66
|
+
def ensure_dir(pathlike: os.PathLike | str) -> Path:
|
|
67
|
+
"""Create directory if missing (idempotent) and return the Path."""
|
|
68
|
+
p = Path(pathlike)
|
|
69
|
+
p.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
return p
|
|
71
|
+
|
|
72
|
+
def resolve_data_dir(cfg: Any = None) -> Path:
|
|
73
|
+
"""Resolve the persistent data directory.
|
|
74
|
+
|
|
75
|
+
Precedence: TD_DATA env > cfg[paths|database].data_dir > ./data
|
|
76
|
+
Always creates the directory.
|
|
77
|
+
"""
|
|
78
|
+
val = _get_opt(cfg, "paths", "data_dir") or _get_opt(cfg, "database", "data_dir")
|
|
79
|
+
p = _resolve_dir("./data", "TD_DATA", val)
|
|
80
|
+
return ensure_dir(p)
|
|
81
|
+
|
|
82
|
+
def resolve_tmp_dir(cfg: Any = None) -> Path:
|
|
83
|
+
"""Resolve the temporary directory.
|
|
84
|
+
|
|
85
|
+
Precedence: TD_TMP env > cfg[paths|database].tmp_dir > ./tmp
|
|
86
|
+
Always creates the directory.
|
|
87
|
+
"""
|
|
88
|
+
val = _get_opt(cfg, "paths", "tmp_dir") or _get_opt(cfg, "database", "tmp_dir")
|
|
89
|
+
p = _resolve_dir("./tmp", "TD_TMP", val)
|
|
90
|
+
return ensure_dir(p)
|
|
91
|
+
|
|
92
|
+
def get_sqlite_db_path(cfg: Any = None) -> Path:
|
|
93
|
+
"""Return full path to the SQLite DB file (does not create the file).
|
|
94
|
+
|
|
95
|
+
Data dir is resolved via resolve_data_dir(cfg). Filename comes from:
|
|
96
|
+
cfg[sqlite].sqlite_filename or cfg[database].sqlite_filename or legacy default 'TradeDangerous.db'.
|
|
97
|
+
"""
|
|
98
|
+
data_dir = resolve_data_dir(cfg)
|
|
99
|
+
filename = (
|
|
100
|
+
_get_opt(cfg, "sqlite", "sqlite_filename")
|
|
101
|
+
or _get_opt(cfg, "database", "sqlite_filename")
|
|
102
|
+
or "TradeDangerous.db" # legacy default matches shipped tests/fixtures
|
|
103
|
+
)
|
|
104
|
+
return (data_dir / filename).resolve()
|
|
105
|
+
|
|
106
|
+
def resolve_db_config_path(default_name: str = "db_config.ini") -> Path:
|
|
107
|
+
"""Honor TD_DB_CONFIG env var for config file path, else default_name in CWD.
|
|
108
|
+
Does not read or validate contents; just returns a Path.
|
|
109
|
+
"""
|
|
110
|
+
cand = os.getenv("TD_DB_CONFIG") or default_name
|
|
111
|
+
p = Path(cand).expanduser()
|
|
112
|
+
return p if p.is_absolute() else (Path.cwd() / p)
|
|
113
|
+
|