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.
Files changed (87) hide show
  1. py.typed +1 -0
  2. trade.py +49 -0
  3. tradedangerous/__init__.py +43 -0
  4. tradedangerous/cache.py +1381 -0
  5. tradedangerous/cli.py +136 -0
  6. tradedangerous/commands/TEMPLATE.py +74 -0
  7. tradedangerous/commands/__init__.py +244 -0
  8. tradedangerous/commands/buildcache_cmd.py +102 -0
  9. tradedangerous/commands/buy_cmd.py +427 -0
  10. tradedangerous/commands/commandenv.py +372 -0
  11. tradedangerous/commands/exceptions.py +94 -0
  12. tradedangerous/commands/export_cmd.py +150 -0
  13. tradedangerous/commands/import_cmd.py +222 -0
  14. tradedangerous/commands/local_cmd.py +243 -0
  15. tradedangerous/commands/market_cmd.py +207 -0
  16. tradedangerous/commands/nav_cmd.py +252 -0
  17. tradedangerous/commands/olddata_cmd.py +270 -0
  18. tradedangerous/commands/parsing.py +221 -0
  19. tradedangerous/commands/rares_cmd.py +298 -0
  20. tradedangerous/commands/run_cmd.py +1521 -0
  21. tradedangerous/commands/sell_cmd.py +262 -0
  22. tradedangerous/commands/shipvendor_cmd.py +60 -0
  23. tradedangerous/commands/station_cmd.py +68 -0
  24. tradedangerous/commands/trade_cmd.py +181 -0
  25. tradedangerous/commands/update_cmd.py +67 -0
  26. tradedangerous/corrections.py +55 -0
  27. tradedangerous/csvexport.py +234 -0
  28. tradedangerous/db/__init__.py +27 -0
  29. tradedangerous/db/adapter.py +192 -0
  30. tradedangerous/db/config.py +107 -0
  31. tradedangerous/db/engine.py +259 -0
  32. tradedangerous/db/lifecycle.py +332 -0
  33. tradedangerous/db/locks.py +208 -0
  34. tradedangerous/db/orm_models.py +500 -0
  35. tradedangerous/db/paths.py +113 -0
  36. tradedangerous/db/utils.py +661 -0
  37. tradedangerous/edscupdate.py +565 -0
  38. tradedangerous/edsmupdate.py +474 -0
  39. tradedangerous/formatting.py +210 -0
  40. tradedangerous/fs.py +156 -0
  41. tradedangerous/gui.py +1146 -0
  42. tradedangerous/mapping.py +133 -0
  43. tradedangerous/mfd/__init__.py +103 -0
  44. tradedangerous/mfd/saitek/__init__.py +3 -0
  45. tradedangerous/mfd/saitek/directoutput.py +678 -0
  46. tradedangerous/mfd/saitek/x52pro.py +195 -0
  47. tradedangerous/misc/checkpricebounds.py +287 -0
  48. tradedangerous/misc/clipboard.py +49 -0
  49. tradedangerous/misc/coord64.py +83 -0
  50. tradedangerous/misc/csvdialect.py +57 -0
  51. tradedangerous/misc/derp-sentinel.py +35 -0
  52. tradedangerous/misc/diff-system-csvs.py +159 -0
  53. tradedangerous/misc/eddb.py +81 -0
  54. tradedangerous/misc/eddn.py +349 -0
  55. tradedangerous/misc/edsc.py +437 -0
  56. tradedangerous/misc/edsm.py +121 -0
  57. tradedangerous/misc/importeddbstats.py +54 -0
  58. tradedangerous/misc/prices-json-exp.py +179 -0
  59. tradedangerous/misc/progress.py +194 -0
  60. tradedangerous/plugins/__init__.py +249 -0
  61. tradedangerous/plugins/edcd_plug.py +371 -0
  62. tradedangerous/plugins/eddblink_plug.py +861 -0
  63. tradedangerous/plugins/edmc_batch_plug.py +133 -0
  64. tradedangerous/plugins/spansh_plug.py +2647 -0
  65. tradedangerous/prices.py +211 -0
  66. tradedangerous/submit-distances.py +422 -0
  67. tradedangerous/templates/Added.csv +37 -0
  68. tradedangerous/templates/Category.csv +17 -0
  69. tradedangerous/templates/RareItem.csv +143 -0
  70. tradedangerous/templates/TradeDangerous.sql +338 -0
  71. tradedangerous/tools.py +40 -0
  72. tradedangerous/tradecalc.py +1302 -0
  73. tradedangerous/tradedb.py +2320 -0
  74. tradedangerous/tradeenv.py +313 -0
  75. tradedangerous/tradeenv.pyi +109 -0
  76. tradedangerous/tradeexcept.py +131 -0
  77. tradedangerous/tradeorm.py +183 -0
  78. tradedangerous/transfers.py +192 -0
  79. tradedangerous/utils.py +243 -0
  80. tradedangerous/version.py +16 -0
  81. tradedangerous-12.7.6.dist-info/METADATA +106 -0
  82. tradedangerous-12.7.6.dist-info/RECORD +87 -0
  83. tradedangerous-12.7.6.dist-info/WHEEL +5 -0
  84. tradedangerous-12.7.6.dist-info/entry_points.txt +3 -0
  85. tradedangerous-12.7.6.dist-info/licenses/LICENSE +373 -0
  86. tradedangerous-12.7.6.dist-info/top_level.txt +2 -0
  87. 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
+