tradedangerous 11.5.3__py3-none-any.whl → 12.0.1__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.
Potentially problematic release.
This version of tradedangerous might be problematic. Click here for more details.
- tradedangerous/cache.py +567 -395
- tradedangerous/cli.py +2 -2
- tradedangerous/commands/TEMPLATE.py +25 -26
- tradedangerous/commands/__init__.py +8 -16
- tradedangerous/commands/buildcache_cmd.py +40 -10
- tradedangerous/commands/buy_cmd.py +57 -46
- tradedangerous/commands/commandenv.py +0 -2
- tradedangerous/commands/export_cmd.py +78 -50
- tradedangerous/commands/import_cmd.py +67 -31
- tradedangerous/commands/market_cmd.py +52 -19
- tradedangerous/commands/olddata_cmd.py +120 -107
- tradedangerous/commands/rares_cmd.py +122 -110
- tradedangerous/commands/run_cmd.py +118 -66
- tradedangerous/commands/sell_cmd.py +52 -45
- tradedangerous/commands/shipvendor_cmd.py +49 -234
- tradedangerous/commands/station_cmd.py +55 -485
- tradedangerous/commands/update_cmd.py +56 -420
- tradedangerous/csvexport.py +173 -162
- tradedangerous/db/__init__.py +27 -0
- tradedangerous/db/adapter.py +191 -0
- tradedangerous/db/config.py +95 -0
- tradedangerous/db/engine.py +246 -0
- tradedangerous/db/lifecycle.py +332 -0
- tradedangerous/db/locks.py +208 -0
- tradedangerous/db/orm_models.py +455 -0
- tradedangerous/db/paths.py +112 -0
- tradedangerous/db/utils.py +661 -0
- tradedangerous/gui.py +2 -2
- tradedangerous/plugins/eddblink_plug.py +387 -251
- tradedangerous/plugins/spansh_plug.py +2488 -821
- tradedangerous/prices.py +124 -142
- tradedangerous/templates/TradeDangerous.sql +6 -6
- tradedangerous/tradecalc.py +1227 -1109
- tradedangerous/tradedb.py +533 -384
- tradedangerous/tradeenv.py +12 -1
- tradedangerous/version.py +1 -1
- {tradedangerous-11.5.3.dist-info → tradedangerous-12.0.1.dist-info}/METADATA +11 -7
- {tradedangerous-11.5.3.dist-info → tradedangerous-12.0.1.dist-info}/RECORD +42 -38
- {tradedangerous-11.5.3.dist-info → tradedangerous-12.0.1.dist-info}/WHEEL +1 -1
- tradedangerous/commands/update_gui.py +0 -721
- tradedangerous/jsonprices.py +0 -254
- tradedangerous/plugins/edapi_plug.py +0 -1071
- tradedangerous/plugins/journal_plug.py +0 -537
- tradedangerous/plugins/netlog_plug.py +0 -316
- {tradedangerous-11.5.3.dist-info → tradedangerous-12.0.1.dist-info}/entry_points.txt +0 -0
- {tradedangerous-11.5.3.dist-info → tradedangerous-12.0.1.dist-info/licenses}/LICENSE +0 -0
- {tradedangerous-11.5.3.dist-info → tradedangerous-12.0.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# tradedangerous/db/locks.py
|
|
2
|
+
# -----------------------------------------------------------------------------
|
|
3
|
+
# Advisory lock helpers (MariaDB/MySQL) — per-station serialization
|
|
4
|
+
#
|
|
5
|
+
# SQLite compatibility:
|
|
6
|
+
# - On SQLite (or any unsupported dialect), all helpers become NO-OPs and
|
|
7
|
+
# behave as if the lock was immediately acquired (yield True). This lets
|
|
8
|
+
# shared code run unchanged across backends.
|
|
9
|
+
#
|
|
10
|
+
# Usage (both writers must use the SAME key format):
|
|
11
|
+
# from tradedangerous.db.locks import station_advisory_lock
|
|
12
|
+
#
|
|
13
|
+
# with sa_session_local(session_factory) as s:
|
|
14
|
+
# # (optional) set isolation once per process elsewhere:
|
|
15
|
+
# # s.execute(text("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED")); s.commit()
|
|
16
|
+
# with station_advisory_lock(s, station_id, timeout_seconds=0.2, max_retries=4) as got:
|
|
17
|
+
# if not got:
|
|
18
|
+
# # processor: defer/requeue work for this station and continue
|
|
19
|
+
# return
|
|
20
|
+
# with s.begin():
|
|
21
|
+
# # do per-station writes here...
|
|
22
|
+
# pass
|
|
23
|
+
# -----------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import time
|
|
28
|
+
from contextlib import contextmanager
|
|
29
|
+
from typing import Iterator
|
|
30
|
+
|
|
31
|
+
from sqlalchemy import text
|
|
32
|
+
from sqlalchemy.orm import Session
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"station_advisory_lock",
|
|
36
|
+
"acquire_station_lock",
|
|
37
|
+
"release_station_lock",
|
|
38
|
+
"station_lock_key",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
# Precompiled SQL (MySQL/MariaDB only)
|
|
42
|
+
_SQL_GET_LOCK = text("SELECT GET_LOCK(:k, :t)")
|
|
43
|
+
_SQL_RELEASE_LOCK = text("SELECT RELEASE_LOCK(:k)")
|
|
44
|
+
|
|
45
|
+
def _is_lock_supported(session: Session) -> bool:
|
|
46
|
+
"""
|
|
47
|
+
Return True if the current SQLAlchemy session is bound to a backend that
|
|
48
|
+
supports advisory locks via GET_LOCK/RELEASE_LOCK (MySQL/MariaDB).
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
name = (session.get_bind().dialect.name or "").lower()
|
|
52
|
+
except Exception:
|
|
53
|
+
name = ""
|
|
54
|
+
return name in ("mysql", "mariadb")
|
|
55
|
+
|
|
56
|
+
def _ensure_read_committed(session: Session) -> None:
|
|
57
|
+
"""
|
|
58
|
+
Ensure the session is using READ COMMITTED for subsequent transactions.
|
|
59
|
+
- Applies only to MySQL/MariaDB.
|
|
60
|
+
- No-ops on SQLite/others.
|
|
61
|
+
- Only sets it if NOT already inside a transaction (affects next txn).
|
|
62
|
+
"""
|
|
63
|
+
if not _is_lock_supported(session):
|
|
64
|
+
return
|
|
65
|
+
try:
|
|
66
|
+
# Only set if we're not already in a transaction; otherwise it would
|
|
67
|
+
# affect the next transaction, not the current one.
|
|
68
|
+
if not session.in_transaction():
|
|
69
|
+
session.execute(text("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED"))
|
|
70
|
+
# No explicit commit needed; this is a session-level setting.
|
|
71
|
+
except Exception:
|
|
72
|
+
# Best-effort; if this fails we just proceed with the default isolation.
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
def station_lock_key(station_id: int) -> str:
|
|
76
|
+
"""
|
|
77
|
+
Return the advisory lock key used by both writers for the same station.
|
|
78
|
+
Keep this format identical in all writers (processor + Spansh).
|
|
79
|
+
"""
|
|
80
|
+
return f"td.station.{int(station_id)}"
|
|
81
|
+
|
|
82
|
+
def acquire_station_lock(session: Session, station_id: int, timeout_seconds: float) -> bool:
|
|
83
|
+
"""
|
|
84
|
+
Try to acquire the advisory lock for a station on THIS DB connection.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
True -> acquired within timeout (or NO-OP True on unsupported dialects)
|
|
88
|
+
False -> timed out (lock held elsewhere)
|
|
89
|
+
|
|
90
|
+
Notes:
|
|
91
|
+
- Advisory locks are per-connection. Use the same Session for acquire,
|
|
92
|
+
the critical section, and release.
|
|
93
|
+
- On SQLite/unsupported dialects, this is a NO-OP that returns True.
|
|
94
|
+
"""
|
|
95
|
+
if not _is_lock_supported(session):
|
|
96
|
+
return True # NO-OP on SQLite/unsupported backends
|
|
97
|
+
|
|
98
|
+
key = station_lock_key(station_id)
|
|
99
|
+
row = session.execute(_SQL_GET_LOCK, {"k": key, "t": float(timeout_seconds)}).first()
|
|
100
|
+
# MariaDB/MySQL GET_LOCK returns 1 (acquired), 0 (timeout), or NULL (error)
|
|
101
|
+
return bool(row and row[0] == 1)
|
|
102
|
+
|
|
103
|
+
def release_station_lock(session: Session, station_id: int) -> None:
|
|
104
|
+
"""
|
|
105
|
+
Release the advisory lock for a station on THIS DB connection.
|
|
106
|
+
Safe to call in finally; releasing a non-held lock is harmless.
|
|
107
|
+
|
|
108
|
+
On SQLite/unsupported dialects, this is a NO-OP.
|
|
109
|
+
"""
|
|
110
|
+
if not _is_lock_supported(session):
|
|
111
|
+
return # NO-OP on SQLite/unsupported backends
|
|
112
|
+
|
|
113
|
+
key = station_lock_key(station_id)
|
|
114
|
+
try:
|
|
115
|
+
session.execute(_SQL_RELEASE_LOCK, {"k": key})
|
|
116
|
+
except Exception:
|
|
117
|
+
# Intentionally swallow — RELEASE_LOCK may return 0/NULL if not held.
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
@contextmanager
|
|
121
|
+
def station_advisory_lock(
|
|
122
|
+
session: Session,
|
|
123
|
+
station_id: int,
|
|
124
|
+
timeout_seconds: float = 0.2,
|
|
125
|
+
max_retries: int = 4,
|
|
126
|
+
backoff_start_seconds: float = 0.05,
|
|
127
|
+
) -> Iterator[bool]:
|
|
128
|
+
"""
|
|
129
|
+
Context manager to acquire/retry/release a per-station advisory lock.
|
|
130
|
+
|
|
131
|
+
Resilience improvement:
|
|
132
|
+
- If no transaction is active on the Session, this helper will OPEN ONE,
|
|
133
|
+
so the lock is taken on the same physical connection the ensuing DML uses.
|
|
134
|
+
In that case, it will COMMIT on normal exit, or ROLLBACK if an exception
|
|
135
|
+
bubbles out of the context block.
|
|
136
|
+
- If a transaction is already active, this helper does NOT touch txn
|
|
137
|
+
boundaries; caller remains responsible for commit/rollback.
|
|
138
|
+
|
|
139
|
+
Yields:
|
|
140
|
+
acquired (bool): True if acquired within retry policy;
|
|
141
|
+
True immediately on unsupported dialects (NO-OP);
|
|
142
|
+
False if not acquired on supported backends.
|
|
143
|
+
"""
|
|
144
|
+
# Fast-path NO-OP for SQLite/unsupported dialects
|
|
145
|
+
if not _is_lock_supported(session):
|
|
146
|
+
try:
|
|
147
|
+
yield True
|
|
148
|
+
finally:
|
|
149
|
+
pass
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
# If we can still influence the next txn, prefer READ COMMITTED for shorter waits.
|
|
153
|
+
_ensure_read_committed(session)
|
|
154
|
+
|
|
155
|
+
# Pin a connection if caller hasn't already begun a transaction.
|
|
156
|
+
started_txn = False
|
|
157
|
+
txn_ctx = None
|
|
158
|
+
if not session.in_transaction():
|
|
159
|
+
txn_ctx = session.begin()
|
|
160
|
+
started_txn = True
|
|
161
|
+
|
|
162
|
+
got = False
|
|
163
|
+
try:
|
|
164
|
+
# Attempt with bounded retries + exponential backoff.
|
|
165
|
+
attempt = 0
|
|
166
|
+
while attempt < max_retries:
|
|
167
|
+
if acquire_station_lock(session, station_id, timeout_seconds):
|
|
168
|
+
got = True
|
|
169
|
+
break
|
|
170
|
+
time.sleep(backoff_start_seconds * (2 ** attempt))
|
|
171
|
+
attempt += 1
|
|
172
|
+
|
|
173
|
+
# Hand control to caller
|
|
174
|
+
yield got
|
|
175
|
+
|
|
176
|
+
# If we created the transaction and no exception occurred, commit it.
|
|
177
|
+
if started_txn and got:
|
|
178
|
+
try:
|
|
179
|
+
session.commit()
|
|
180
|
+
except Exception:
|
|
181
|
+
# If commit fails, make sure to roll back so we don't leak an open txn.
|
|
182
|
+
session.rollback()
|
|
183
|
+
raise
|
|
184
|
+
except Exception:
|
|
185
|
+
# If we created the transaction and an exception escaped the block, roll it back.
|
|
186
|
+
if started_txn and session.in_transaction():
|
|
187
|
+
try:
|
|
188
|
+
session.rollback()
|
|
189
|
+
except Exception:
|
|
190
|
+
# Swallow secondary rollback failures; original exception should propagate.
|
|
191
|
+
pass
|
|
192
|
+
raise
|
|
193
|
+
finally:
|
|
194
|
+
# Always release the advisory lock if we acquired it.
|
|
195
|
+
if got:
|
|
196
|
+
try:
|
|
197
|
+
release_station_lock(session, station_id)
|
|
198
|
+
except Exception:
|
|
199
|
+
# Lock releases are best-effort; don't mask user exceptions.
|
|
200
|
+
pass
|
|
201
|
+
|
|
202
|
+
# If we opened a txn context object (older SA versions), ensure it's closed.
|
|
203
|
+
# (Harmless if already committed/rolled back above.)
|
|
204
|
+
if started_txn and txn_ctx is not None:
|
|
205
|
+
try:
|
|
206
|
+
txn_ctx.close()
|
|
207
|
+
except Exception:
|
|
208
|
+
pass
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
# tradedangerous/db/orm_models.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from sqlalchemy import (
|
|
7
|
+
MetaData,
|
|
8
|
+
ForeignKey,
|
|
9
|
+
Integer,
|
|
10
|
+
BigInteger,
|
|
11
|
+
String,
|
|
12
|
+
CHAR,
|
|
13
|
+
Enum,
|
|
14
|
+
Index,
|
|
15
|
+
UniqueConstraint,
|
|
16
|
+
CheckConstraint,
|
|
17
|
+
text,
|
|
18
|
+
Column,
|
|
19
|
+
DateTime,
|
|
20
|
+
)
|
|
21
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, object_session
|
|
22
|
+
from sqlalchemy.sql import expression
|
|
23
|
+
from sqlalchemy.ext.compiler import compiles
|
|
24
|
+
from sqlalchemy.types import TypeDecorator
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ---- Dialect-aware time utilities (moved before model usage) ----
|
|
28
|
+
class now6(expression.FunctionElement):
|
|
29
|
+
"""CURRENT_TIMESTAMP with microseconds on MySQL/MariaDB; plain CURRENT_TIMESTAMP elsewhere."""
|
|
30
|
+
type = DateTime()
|
|
31
|
+
inherit_cache = True
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@compiles(now6, "mysql")
|
|
35
|
+
@compiles(now6, "mariadb")
|
|
36
|
+
def _mysql_now6(element, compiler, **kw):
|
|
37
|
+
return "CURRENT_TIMESTAMP(6)"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@compiles(now6)
|
|
41
|
+
def _default_now(element, compiler, **kw):
|
|
42
|
+
return "CURRENT_TIMESTAMP"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class DateTime6(TypeDecorator):
|
|
46
|
+
"""DATETIME that is DATETIME(6) on MySQL/MariaDB, generic DateTime elsewhere."""
|
|
47
|
+
impl = DateTime
|
|
48
|
+
cache_ok = True
|
|
49
|
+
|
|
50
|
+
def load_dialect_impl(self, dialect):
|
|
51
|
+
if dialect.name in ("mysql", "mariadb"):
|
|
52
|
+
from sqlalchemy.dialects.mysql import DATETIME as _MYSQL_DATETIME
|
|
53
|
+
return dialect.type_descriptor(_MYSQL_DATETIME(fsp=6))
|
|
54
|
+
return dialect.type_descriptor(DateTime())
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ---------- Dialect Helpers --------
|
|
58
|
+
class CIString(TypeDecorator):
|
|
59
|
+
"""
|
|
60
|
+
Case-insensitive string type.
|
|
61
|
+
- SQLite → uses NOCASE collation
|
|
62
|
+
- MySQL/MariaDB → uses utf8mb4_unicode_ci
|
|
63
|
+
- Others → plain String
|
|
64
|
+
"""
|
|
65
|
+
impl = String
|
|
66
|
+
cache_ok = True
|
|
67
|
+
|
|
68
|
+
def __init__(self, length, **kwargs):
|
|
69
|
+
super().__init__(length=length, **kwargs)
|
|
70
|
+
|
|
71
|
+
def load_dialect_impl(self, dialect):
|
|
72
|
+
if dialect.name == "sqlite":
|
|
73
|
+
return dialect.type_descriptor(String(self.impl.length, collation="NOCASE"))
|
|
74
|
+
elif dialect.name in ("mysql", "mariadb"):
|
|
75
|
+
return dialect.type_descriptor(String(self.impl.length, collation="utf8mb4_unicode_ci"))
|
|
76
|
+
else:
|
|
77
|
+
return dialect.type_descriptor(String(self.impl.length))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ---------- Naming & Base ----------
|
|
81
|
+
naming_convention = {
|
|
82
|
+
"ix": "ix_%(table_name)s__%(column_0_N_name)s",
|
|
83
|
+
"uq": "uq_%(table_name)s__%(column_0_N_name)s",
|
|
84
|
+
"ck": "ck_%(table_name)s__%(column_0_name)s", # use column name, not constraint_name
|
|
85
|
+
"fk": "fk_%(table_name)s__%(column_0_N_name)s__%(referred_table_name)s",
|
|
86
|
+
"pk": "pk_%(table_name)s",
|
|
87
|
+
}
|
|
88
|
+
metadata = MetaData(naming_convention=naming_convention)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class Base(DeclarativeBase):
|
|
92
|
+
metadata = metadata
|
|
93
|
+
|
|
94
|
+
# ---------- Enums ----------
|
|
95
|
+
TriState = Enum(
|
|
96
|
+
"Y",
|
|
97
|
+
"N",
|
|
98
|
+
"?",
|
|
99
|
+
native_enum=False,
|
|
100
|
+
create_constraint=True,
|
|
101
|
+
validate_strings=True,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
PadSize = Enum(
|
|
105
|
+
"S",
|
|
106
|
+
"M",
|
|
107
|
+
"L",
|
|
108
|
+
"?",
|
|
109
|
+
native_enum=False,
|
|
110
|
+
create_constraint=True,
|
|
111
|
+
validate_strings=True,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ---------- Core Domain ----------
|
|
116
|
+
class Added(Base):
|
|
117
|
+
__tablename__ = "Added"
|
|
118
|
+
|
|
119
|
+
added_id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
120
|
+
name: Mapped[str] = mapped_column(CIString(128), nullable=False, unique=True)
|
|
121
|
+
|
|
122
|
+
# Relationships
|
|
123
|
+
systems: Mapped[list["System"]] = relationship(back_populates="added")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class System(Base):
|
|
127
|
+
__tablename__ = "System"
|
|
128
|
+
|
|
129
|
+
system_id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
|
130
|
+
name: Mapped[str] = mapped_column(CIString(128), nullable=False)
|
|
131
|
+
pos_x: Mapped[float] = mapped_column(nullable=False)
|
|
132
|
+
pos_y: Mapped[float] = mapped_column(nullable=False)
|
|
133
|
+
pos_z: Mapped[float] = mapped_column(nullable=False)
|
|
134
|
+
added_id: Mapped[int | None] = mapped_column(
|
|
135
|
+
ForeignKey("Added.added_id", onupdate="CASCADE", ondelete="CASCADE")
|
|
136
|
+
)
|
|
137
|
+
modified: Mapped[str] = mapped_column(
|
|
138
|
+
DateTime6(),
|
|
139
|
+
server_default=now6(),
|
|
140
|
+
onupdate=now6(),
|
|
141
|
+
nullable=False,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Relationships
|
|
145
|
+
added: Mapped[Optional["Added"]] = relationship(back_populates="systems")
|
|
146
|
+
stations: Mapped[list["Station"]] = relationship(back_populates="system", cascade="all, delete-orphan")
|
|
147
|
+
|
|
148
|
+
__table_args__ = (
|
|
149
|
+
Index("idx_system_by_pos", "pos_x", "pos_y", "pos_z", "system_id"),
|
|
150
|
+
Index("idx_system_by_name", "name"),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class Station(Base):
|
|
157
|
+
__tablename__ = "Station"
|
|
158
|
+
|
|
159
|
+
station_id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
|
160
|
+
name: Mapped[str] = mapped_column(CIString(128), nullable=False)
|
|
161
|
+
|
|
162
|
+
# type widened; cascade semantics unchanged (DELETE only)
|
|
163
|
+
system_id: Mapped[int] = mapped_column(
|
|
164
|
+
BigInteger,
|
|
165
|
+
ForeignKey("System.system_id", ondelete="CASCADE"),
|
|
166
|
+
nullable=False,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
ls_from_star: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text("0"))
|
|
170
|
+
|
|
171
|
+
blackmarket: Mapped[str] = mapped_column(TriState, nullable=False, server_default=text("'?'"))
|
|
172
|
+
max_pad_size: Mapped[str] = mapped_column(PadSize, nullable=False, server_default=text("'?'"))
|
|
173
|
+
market: Mapped[str] = mapped_column(TriState, nullable=False, server_default=text("'?'"))
|
|
174
|
+
shipyard: Mapped[str] = mapped_column(TriState, nullable=False, server_default=text("'?'"))
|
|
175
|
+
outfitting: Mapped[str] = mapped_column(TriState, nullable=False, server_default=text("'?'"))
|
|
176
|
+
rearm: Mapped[str] = mapped_column(TriState, nullable=False, server_default=text("'?'"))
|
|
177
|
+
refuel: Mapped[str] = mapped_column(TriState, nullable=False, server_default=text("'?'"))
|
|
178
|
+
repair: Mapped[str] = mapped_column(TriState, nullable=False, server_default=text("'?'"))
|
|
179
|
+
planetary: Mapped[str] = mapped_column(TriState, nullable=False, server_default=text("'?'"))
|
|
180
|
+
|
|
181
|
+
type_id: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text("0"))
|
|
182
|
+
modified: Mapped[str] = mapped_column(DateTime6(), server_default=now6(), onupdate=now6(), nullable=False)
|
|
183
|
+
|
|
184
|
+
# Relationships
|
|
185
|
+
system: Mapped["System"] = relationship(back_populates="stations")
|
|
186
|
+
items: Mapped[list["StationItem"]] = relationship(back_populates="station", cascade="all, delete-orphan")
|
|
187
|
+
ship_vendors: Mapped[list["ShipVendor"]] = relationship(back_populates="station", cascade="all, delete-orphan")
|
|
188
|
+
upgrade_vendors: Mapped[list["UpgradeVendor"]] = relationship(back_populates="station", cascade="all, delete-orphan")
|
|
189
|
+
|
|
190
|
+
__table_args__ = (
|
|
191
|
+
Index("idx_station_by_system", "system_id"),
|
|
192
|
+
Index("idx_station_by_name", "name"),
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class Category(Base):
|
|
199
|
+
__tablename__ = "Category"
|
|
200
|
+
|
|
201
|
+
category_id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
202
|
+
name: Mapped[str] = mapped_column(CIString(128), nullable=False)
|
|
203
|
+
|
|
204
|
+
# Relationships
|
|
205
|
+
items: Mapped[list["Item"]] = relationship(back_populates="category")
|
|
206
|
+
|
|
207
|
+
__table_args__ = (Index("idx_category_by_name", "name"),)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class Item(Base):
|
|
211
|
+
__tablename__ = "Item"
|
|
212
|
+
|
|
213
|
+
item_id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
214
|
+
name: Mapped[str] = mapped_column(CIString(128), nullable=False)
|
|
215
|
+
category_id: Mapped[int] = mapped_column(
|
|
216
|
+
ForeignKey("Category.category_id", onupdate="CASCADE", ondelete="CASCADE"),
|
|
217
|
+
nullable=False,
|
|
218
|
+
)
|
|
219
|
+
ui_order: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text("0"))
|
|
220
|
+
avg_price: Mapped[int | None] = mapped_column(Integer)
|
|
221
|
+
fdev_id: Mapped[int | None] = mapped_column(Integer)
|
|
222
|
+
|
|
223
|
+
# Relationships
|
|
224
|
+
category: Mapped["Category"] = relationship(back_populates="items")
|
|
225
|
+
stations: Mapped[list["StationItem"]] = relationship(back_populates="item", cascade="all, delete-orphan")
|
|
226
|
+
|
|
227
|
+
__table_args__ = (
|
|
228
|
+
Index("idx_item_by_fdevid", "fdev_id"),
|
|
229
|
+
Index("idx_item_by_category", "category_id"),
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class StationItem(Base):
|
|
235
|
+
__tablename__ = "StationItem"
|
|
236
|
+
|
|
237
|
+
station_id: Mapped[int] = mapped_column(
|
|
238
|
+
BigInteger,
|
|
239
|
+
ForeignKey("Station.station_id", ondelete="CASCADE", onupdate="CASCADE"),
|
|
240
|
+
primary_key=True,
|
|
241
|
+
)
|
|
242
|
+
item_id: Mapped[int] = mapped_column(
|
|
243
|
+
ForeignKey("Item.item_id", ondelete="CASCADE", onupdate="CASCADE"),
|
|
244
|
+
primary_key=True,
|
|
245
|
+
)
|
|
246
|
+
demand_price: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
247
|
+
demand_units: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
248
|
+
demand_level: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
249
|
+
supply_price: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
250
|
+
supply_units: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
251
|
+
supply_level: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
252
|
+
modified: Mapped[str] = mapped_column(DateTime6(), server_default=now6(), onupdate=now6(), nullable=False)
|
|
253
|
+
from_live: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text("0"))
|
|
254
|
+
|
|
255
|
+
# Relationships
|
|
256
|
+
station: Mapped["Station"] = relationship(back_populates="items")
|
|
257
|
+
item: Mapped["Item"] = relationship(back_populates="stations")
|
|
258
|
+
|
|
259
|
+
__table_args__ = (
|
|
260
|
+
Index("si_mod_stn_itm", "modified", "station_id", "item_id"),
|
|
261
|
+
Index("si_itm_dmdpr", "item_id", "demand_price", sqlite_where=text("demand_price > 0")),
|
|
262
|
+
Index("si_itm_suppr", "item_id", "supply_price", sqlite_where=text("supply_price > 0")),
|
|
263
|
+
{"sqlite_with_rowid": False},
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class Ship(Base):
|
|
270
|
+
__tablename__ = "Ship"
|
|
271
|
+
|
|
272
|
+
ship_id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
273
|
+
name: Mapped[str] = mapped_column(CIString(128), nullable=False)
|
|
274
|
+
cost: Mapped[int | None] = mapped_column(Integer)
|
|
275
|
+
|
|
276
|
+
# Relationships
|
|
277
|
+
vendors: Mapped[list["ShipVendor"]] = relationship(back_populates="ship")
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class ShipVendor(Base):
|
|
281
|
+
__tablename__ = "ShipVendor"
|
|
282
|
+
|
|
283
|
+
ship_id: Mapped[int] = mapped_column(
|
|
284
|
+
ForeignKey("Ship.ship_id", ondelete="CASCADE", onupdate="CASCADE"),
|
|
285
|
+
primary_key=True,
|
|
286
|
+
)
|
|
287
|
+
station_id: Mapped[int] = mapped_column(
|
|
288
|
+
BigInteger,
|
|
289
|
+
ForeignKey("Station.station_id", ondelete="CASCADE", onupdate="CASCADE"),
|
|
290
|
+
primary_key=True,
|
|
291
|
+
)
|
|
292
|
+
modified: Mapped[str] = mapped_column(DateTime6(), server_default=now6(), onupdate=now6(), nullable=False)
|
|
293
|
+
|
|
294
|
+
# Relationships
|
|
295
|
+
ship: Mapped["Ship"] = relationship(back_populates="vendors")
|
|
296
|
+
station: Mapped["Station"] = relationship(back_populates="ship_vendors")
|
|
297
|
+
|
|
298
|
+
__table_args__ = (Index("idx_shipvendor_by_station", "station_id"),)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class Upgrade(Base):
|
|
302
|
+
__tablename__ = "Upgrade"
|
|
303
|
+
|
|
304
|
+
upgrade_id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
305
|
+
name: Mapped[str] = mapped_column(CIString(128), nullable=False)
|
|
306
|
+
class_: Mapped[int] = mapped_column("class", Integer, nullable=False)
|
|
307
|
+
rating: Mapped[str] = mapped_column(CHAR(1), nullable=False)
|
|
308
|
+
ship: Mapped[str | None] = mapped_column(CIString(128))
|
|
309
|
+
|
|
310
|
+
# Relationships
|
|
311
|
+
vendors: Mapped[list["UpgradeVendor"]] = relationship(back_populates="upgrade")
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
class UpgradeVendor(Base):
|
|
315
|
+
__tablename__ = "UpgradeVendor"
|
|
316
|
+
|
|
317
|
+
upgrade_id: Mapped[int] = mapped_column(
|
|
318
|
+
ForeignKey("Upgrade.upgrade_id", ondelete="CASCADE", onupdate="CASCADE"),
|
|
319
|
+
primary_key=True,
|
|
320
|
+
)
|
|
321
|
+
station_id: Mapped[int] = mapped_column(
|
|
322
|
+
BigInteger,
|
|
323
|
+
ForeignKey("Station.station_id", ondelete="CASCADE", onupdate="CASCADE"),
|
|
324
|
+
primary_key=True,
|
|
325
|
+
)
|
|
326
|
+
modified: Mapped[str] = mapped_column(DateTime6(), nullable=False, server_default=now6(), onupdate=now6())
|
|
327
|
+
|
|
328
|
+
# Relationships
|
|
329
|
+
upgrade: Mapped["Upgrade"] = relationship(back_populates="vendors")
|
|
330
|
+
station: Mapped["Station"] = relationship(back_populates="upgrade_vendors")
|
|
331
|
+
|
|
332
|
+
__table_args__ = (Index("idx_vendor_by_station_id", "station_id"),)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
class RareItem(Base):
|
|
336
|
+
__tablename__ = "RareItem"
|
|
337
|
+
|
|
338
|
+
rare_id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
339
|
+
station_id: Mapped[int] = mapped_column(
|
|
340
|
+
BigInteger,
|
|
341
|
+
ForeignKey("Station.station_id", ondelete="CASCADE", onupdate="CASCADE"),
|
|
342
|
+
nullable=False,
|
|
343
|
+
)
|
|
344
|
+
category_id: Mapped[int] = mapped_column(
|
|
345
|
+
ForeignKey("Category.category_id", onupdate="CASCADE", ondelete="CASCADE"),
|
|
346
|
+
nullable=False,
|
|
347
|
+
)
|
|
348
|
+
name: Mapped[str] = mapped_column(CIString(128), nullable=False)
|
|
349
|
+
cost: Mapped[int | None] = mapped_column(Integer)
|
|
350
|
+
max_allocation: Mapped[int | None] = mapped_column(Integer)
|
|
351
|
+
illegal: Mapped[str] = mapped_column(TriState, nullable=False, server_default=text("'?'"))
|
|
352
|
+
suppressed: Mapped[str] = mapped_column(TriState, nullable=False, server_default=text("'?'"))
|
|
353
|
+
|
|
354
|
+
__table_args__ = (UniqueConstraint("name", name="uq_rareitem_name"),)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
class FDevShipyard(Base):
|
|
360
|
+
__tablename__ = "FDevShipyard"
|
|
361
|
+
|
|
362
|
+
id = Column(Integer, primary_key=True, unique=True, nullable=False)
|
|
363
|
+
symbol = Column(CIString(128))
|
|
364
|
+
name = Column(CIString(128))
|
|
365
|
+
entitlement = Column(String(50))
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
class FDevOutfitting(Base):
|
|
369
|
+
__tablename__ = "FDevOutfitting"
|
|
370
|
+
|
|
371
|
+
id = Column(Integer, primary_key=True, unique=True, nullable=False)
|
|
372
|
+
symbol = Column(CIString(128))
|
|
373
|
+
category = Column(String(10))
|
|
374
|
+
name = Column(CIString(128))
|
|
375
|
+
mount = Column(String(20))
|
|
376
|
+
guidance = Column(String(20))
|
|
377
|
+
ship = Column(CIString(128))
|
|
378
|
+
class_ = Column("class", String(1), nullable=False)
|
|
379
|
+
rating = Column(String(1), nullable=False)
|
|
380
|
+
entitlement = Column(String(50))
|
|
381
|
+
|
|
382
|
+
__table_args__ = (
|
|
383
|
+
CheckConstraint(
|
|
384
|
+
"category IN ('hardpoint','internal','standard','utility')",
|
|
385
|
+
name="ck_fdo_category",
|
|
386
|
+
),
|
|
387
|
+
CheckConstraint(
|
|
388
|
+
"(mount IN ('Fixed','Gimballed','Turreted')) OR (mount IS NULL)",
|
|
389
|
+
name="ck_fdo_mount",
|
|
390
|
+
),
|
|
391
|
+
CheckConstraint(
|
|
392
|
+
"(guidance IN ('Dumbfire','Seeker','Swarm')) OR (guidance IS NULL)",
|
|
393
|
+
name="ck_fdo_guidance",
|
|
394
|
+
),
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
# ---------- Control & Staging ----------
|
|
399
|
+
class ExportControl(Base):
|
|
400
|
+
"""
|
|
401
|
+
Singleton control row for hybrid export/watermarking.
|
|
402
|
+
- id: always 1
|
|
403
|
+
- last_full_dump_time: watermark
|
|
404
|
+
- last_reset_key: optional cursor for chunked from_live resets
|
|
405
|
+
"""
|
|
406
|
+
__tablename__ = "ExportControl"
|
|
407
|
+
|
|
408
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True, server_default=text("1"))
|
|
409
|
+
last_full_dump_time: Mapped[str] = mapped_column(DateTime6(), nullable=False)
|
|
410
|
+
last_reset_key: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
class StationItemStaging(Base):
|
|
414
|
+
"""
|
|
415
|
+
Staging table for bulk loads (no FKs). Same columns as StationItem.
|
|
416
|
+
"""
|
|
417
|
+
__tablename__ = "StationItem_staging"
|
|
418
|
+
|
|
419
|
+
station_id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
|
420
|
+
item_id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
421
|
+
demand_price: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
422
|
+
demand_units: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
423
|
+
demand_level: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
424
|
+
supply_price: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
425
|
+
supply_units: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
426
|
+
supply_level: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
427
|
+
modified: Mapped[str] = mapped_column(DateTime6(), server_default=now6(), onupdate=now6(), nullable=False)
|
|
428
|
+
from_live: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text("0"))
|
|
429
|
+
|
|
430
|
+
__table_args__ = (Index("idx_sistaging_stn_itm", "station_id", "item_id"),)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
__all__ = [
|
|
436
|
+
# Base
|
|
437
|
+
"Base",
|
|
438
|
+
# Core
|
|
439
|
+
"Added",
|
|
440
|
+
"System",
|
|
441
|
+
"Station",
|
|
442
|
+
"Category",
|
|
443
|
+
"Item",
|
|
444
|
+
"StationItem",
|
|
445
|
+
"Ship",
|
|
446
|
+
"ShipVendor",
|
|
447
|
+
"Upgrade",
|
|
448
|
+
"UpgradeVendor",
|
|
449
|
+
"RareItem",
|
|
450
|
+
"FDevShipyard",
|
|
451
|
+
"FDevOutfitting",
|
|
452
|
+
# Control & staging
|
|
453
|
+
"ExportControl",
|
|
454
|
+
"StationItemStaging",
|
|
455
|
+
]
|