tradedangerous 11.5.2__py3-none-any.whl → 12.0.0__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.

Files changed (39) hide show
  1. tradedangerous/cache.py +567 -395
  2. tradedangerous/cli.py +2 -2
  3. tradedangerous/commands/TEMPLATE.py +25 -26
  4. tradedangerous/commands/__init__.py +8 -16
  5. tradedangerous/commands/buildcache_cmd.py +40 -10
  6. tradedangerous/commands/buy_cmd.py +57 -46
  7. tradedangerous/commands/commandenv.py +0 -2
  8. tradedangerous/commands/export_cmd.py +78 -50
  9. tradedangerous/commands/import_cmd.py +70 -34
  10. tradedangerous/commands/market_cmd.py +52 -19
  11. tradedangerous/commands/olddata_cmd.py +120 -107
  12. tradedangerous/commands/rares_cmd.py +122 -110
  13. tradedangerous/commands/run_cmd.py +118 -66
  14. tradedangerous/commands/sell_cmd.py +52 -45
  15. tradedangerous/commands/shipvendor_cmd.py +49 -234
  16. tradedangerous/commands/station_cmd.py +55 -485
  17. tradedangerous/commands/update_cmd.py +56 -420
  18. tradedangerous/csvexport.py +173 -162
  19. tradedangerous/gui.py +2 -2
  20. tradedangerous/plugins/eddblink_plug.py +389 -252
  21. tradedangerous/plugins/spansh_plug.py +2488 -821
  22. tradedangerous/prices.py +124 -142
  23. tradedangerous/templates/TradeDangerous.sql +6 -6
  24. tradedangerous/tradecalc.py +1227 -1109
  25. tradedangerous/tradedb.py +533 -384
  26. tradedangerous/tradeenv.py +12 -1
  27. tradedangerous/version.py +1 -1
  28. {tradedangerous-11.5.2.dist-info → tradedangerous-12.0.0.dist-info}/METADATA +17 -4
  29. {tradedangerous-11.5.2.dist-info → tradedangerous-12.0.0.dist-info}/RECORD +33 -39
  30. {tradedangerous-11.5.2.dist-info → tradedangerous-12.0.0.dist-info}/WHEEL +1 -1
  31. tradedangerous/commands/update_gui.py +0 -721
  32. tradedangerous/jsonprices.py +0 -254
  33. tradedangerous/plugins/edapi_plug.py +0 -1071
  34. tradedangerous/plugins/journal_plug.py +0 -537
  35. tradedangerous/plugins/netlog_plug.py +0 -316
  36. tradedangerous/templates/database_changes.json +0 -6
  37. {tradedangerous-11.5.2.dist-info → tradedangerous-12.0.0.dist-info}/entry_points.txt +0 -0
  38. {tradedangerous-11.5.2.dist-info → tradedangerous-12.0.0.dist-info/licenses}/LICENSE +0 -0
  39. {tradedangerous-11.5.2.dist-info → tradedangerous-12.0.0.dist-info}/top_level.txt +0 -0
@@ -1,31 +1,31 @@
1
1
  from pathlib import Path
2
+ from sqlalchemy import inspect, text
2
3
  from .tradeexcept import TradeException
3
4
 
5
+ from .db import utils as db_utils
6
+
7
+
4
8
  import csv
5
9
  import os
6
- import sqlite3
7
10
 
8
11
  ######################################################################
9
12
  # TradeDangerous :: Modules :: CSV Exporter
10
13
  #
11
- # Generate a CSV files for a table of the database.
14
+ # Generate CSV files for database tables.
12
15
  #
13
- # Note: This routine makes some assumptions about the structure
14
- # of the database:
15
- # * The table should only have one UNIQUE index
16
- # * The referenced table must have one UNIQUE index
17
- # * The FK columns must have the same name in both tables
18
- # * One column primary keys will be handled by the database engine
16
+ # Assumptions:
17
+ # * Each table has at most one UNIQUE index.
18
+ # * Referenced tables also have a UNIQUE index.
19
+ # * Only single-column foreign keys are supported.
20
+ # * Single-column primary keys are inferred automatically by SQLAlchemy.
19
21
  #
20
- ######################################################################
21
- # CAUTION: If the database structure gets changed this script might
22
- # need some corrections.
22
+ # CAUTION: If the schema changes this module may require updates.
23
23
  ######################################################################
24
24
 
25
25
  ######################################################################
26
26
  # Default values
27
27
 
28
- # for some tables the first two columns will be reversed
28
+ # For some tables the first two columns will be reversed
29
29
  reverseList = []
30
30
 
31
31
  ######################################################################
@@ -38,183 +38,194 @@ def search_keyList(items, val):
38
38
  return row
39
39
  return None
40
40
 
41
- def getUniqueIndex(conn, tableName):
42
- """ return all unique columns """
43
- idxCursor = conn.cursor()
41
+
42
+ def getUniqueIndex(session, tableName):
43
+ """Return all unique columns via SQLAlchemy inspector."""
44
+ inspector = inspect(session.get_bind())
44
45
  unqIndex = []
45
- for idxRow in idxCursor.execute("PRAGMA index_list('%s')" % tableName):
46
- if idxRow['unique']:
47
- # it's a unique index
48
- unqCursor = conn.cursor()
49
- for unqRow in unqCursor.execute("PRAGMA index_info('%s')" % idxRow['name']):
50
- unqIndex.append(unqRow['name'])
46
+ for idx in inspector.get_indexes(tableName):
47
+ if idx.get("unique"):
48
+ unqIndex.extend(idx.get("column_names", []))
51
49
  return unqIndex
52
50
 
53
- def getFKeyList(conn, tableName):
54
- """ get all single column FKs """
51
+
52
+ def getFKeyList(session, tableName):
53
+ """Return all single-column foreign keys via SQLAlchemy inspector."""
54
+ inspector = inspect(session.get_bind())
55
55
  keyList = []
56
- keyCount = -1
57
- keyCursor = conn.cursor()
58
- for keyRow in keyCursor.execute("PRAGMA foreign_key_list('%s')" % tableName):
59
- if keyRow['seq'] == 0:
60
- keyCount += 1
61
- keyList.append( {'table': keyRow['table'],
62
- 'from': keyRow['from'],
63
- 'to': keyRow['to']}
64
- )
65
- if keyRow['seq'] == 1:
66
- # if there is a second column, remove it from the list
67
- keyList.remove( keyList[keyCount] )
68
- keyCount -= 1
69
-
56
+ for fk in inspector.get_foreign_keys(tableName):
57
+ cols = fk.get("constrained_columns", [])
58
+ referred = fk.get("referred_columns", [])
59
+ if len(cols) == 1 and len(referred) == 1:
60
+ keyList.append({
61
+ "table": fk.get("referred_table"),
62
+ "from": cols[0],
63
+ "to": referred[0],
64
+ })
70
65
  return keyList
71
66
 
72
- def buildFKeyStmt(conn, tableName, key):
67
+
68
+ def buildFKeyStmt(session, tableName, key):
73
69
  """
74
- resolve the FK constrain with the UNIQUE index
75
- multicolumn UNIQUEs are allowed as long as the last one
76
- will be a single column one
70
+ Resolve the FK constraint against the UNIQUE index of the
71
+ referenced table.
72
+
73
+ Multicolumn UNIQUEs are allowed, but only the last column
74
+ may be treated as a single-column join target.
77
75
  """
78
- unqIndex = getUniqueIndex(conn, key['table'])
79
- keyList = getFKeyList(conn, key['table'])
76
+ unqIndex = getUniqueIndex(session, key["table"])
77
+ keyList = getFKeyList(session, key["table"])
80
78
  keyStmt = []
79
+
81
80
  for colName in unqIndex:
82
- # check if the column is a foreign key
81
+ # If this unique column is itself a foreign key, recurse
83
82
  keyKey = search_keyList(keyList, colName)
84
83
  if keyKey:
85
- newStmt = buildFKeyStmt(conn, key['table'], keyKey)
86
- for row in newStmt:
87
- keyStmt.append(row)
84
+ keyStmt.extend(buildFKeyStmt(session, key["table"], keyKey))
88
85
  else:
89
86
  keyStmt.append({
90
- 'table': tableName,
91
- 'column': colName,
92
- 'joinTable': key['table'],
93
- 'joinColumn': key['to']
87
+ "table": tableName,
88
+ "column": colName,
89
+ "joinTable": key["table"],
90
+ "joinColumn": key["to"],
94
91
  })
95
-
92
+
96
93
  return keyStmt
97
94
 
95
+
98
96
  ######################################################################
99
97
  # Code
100
98
  ######################################################################
101
99
 
102
- def exportTableToFile(tdb, tdenv, tableName, csvPath=None):
100
+ def exportTableToFile(tdb_or_session, tdenv, tableName, csvPath=None):
103
101
  """
104
- Generate the csv file for tableName in csvPath
105
- returns lineCount, exportPath
102
+ Generate the CSV file for tableName in csvPath.
103
+ Returns (lineCount, exportPath).
104
+
105
+ Behaviour:
106
+ - Prefix unique columns with "unq:".
107
+ - Foreign keys are exported as "<col>@<joinTable>.<uniqueCol>".
108
+ - Datetime-like values for 'modified' columns are exported as
109
+ "YYYY-MM-DD HH:MM:SS" (no microseconds).
110
+
111
+ Compatible with either:
112
+ * a SQLAlchemy Session
113
+ * a TradeDB wrapper exposing .engine
106
114
  """
107
-
108
- # path for csv file
109
- csvPath = csvPath or tdb.csvPath
110
- if not csvPath.is_dir():
111
- raise TradeException("Save location '{}' not found.".format(str(csvPath)))
112
-
113
- # connect to the database
114
- conn = tdb.getDB()
115
- conn.row_factory = sqlite3.Row
116
-
117
- # prefix for unique/ignore columns
115
+ from sqlalchemy.orm import Session
116
+
117
+ # --- Resolve a SQLAlchemy session ---
118
+ if hasattr(tdb_or_session, "engine"):
119
+ # Likely a TradeDB instance
120
+ engine = tdb_or_session.engine
121
+ session = Session(engine)
122
+ elif hasattr(tdb_or_session, "get_bind"):
123
+ # Already a Session
124
+ session = tdb_or_session
125
+ else:
126
+ raise TradeException(
127
+ f"Unsupported DB object passed to exportTableToFile: {type(tdb_or_session)}"
128
+ )
129
+
130
+ csvPath = csvPath or Path(tdenv.csvDir)
131
+ if not Path(csvPath).is_dir():
132
+ raise TradeException(f"Save location '{csvPath}' not found.")
133
+
118
134
  uniquePfx = "unq:"
119
- ignorePfx = "!"
120
-
121
- # create CSV files
122
- exportPath = (csvPath / Path(tableName)).with_suffix(".csv")
123
- tdenv.DEBUG0("Export Table '{table}' to '{file}'".format(
124
- table=tableName, file=str(exportPath)
125
- ))
126
-
135
+ exportPath = (Path(csvPath) / Path(tableName)).with_suffix(".csv")
136
+ tdenv.DEBUG0(f"Export Table '{tableName}' to '{exportPath}'")
137
+
138
+ def _fmt_ts(val):
139
+ if hasattr(val, "strftime"):
140
+ try:
141
+ return val.strftime("%Y-%m-%d %H:%M:%S")
142
+ except Exception:
143
+ pass
144
+ if isinstance(val, str):
145
+ s = val
146
+ if len(s) >= 19 and s[10] == "T":
147
+ s = s[:10] + " " + s[11:]
148
+ if len(s) >= 19 and s[4] == "-" and s[7] == "-" and s[10] == " " and s[13] == ":" and s[16] == ":":
149
+ return s[:19]
150
+ return val
151
+
127
152
  lineCount = 0
128
- with exportPath.open("w", encoding='utf-8', newline="\n") as exportFile:
129
- exportOut = csv.writer(exportFile, delimiter=",", quotechar="'", doublequote=True, quoting=csv.QUOTE_NONNUMERIC, lineterminator="\n")
130
-
131
- cur = conn.cursor()
132
-
133
- # check for single PRIMARY KEY
134
- pkCount = 0
135
- for columnRow in cur.execute("PRAGMA table_info('%s')" % tableName):
136
- # count the columns of the primary key
137
- if columnRow['pk'] > 0:
138
- pkCount += 1
139
-
140
- # build column list
141
- columnList = []
142
- for columnRow in cur.execute("PRAGMA table_info('%s')" % tableName):
143
- # if there is only one PK column, ignore it
144
- # if columnRow['pk'] > 0 and pkCount == 1: continue
145
- columnList.append(columnRow)
146
-
147
- if len(columnList) == 0:
148
- raise TradeException("No columns to export for table '{}'.".format(tableName))
149
-
150
- # reverse the first two columns for some tables
151
- if tableName in reverseList:
152
- columnList[0], columnList[1] = columnList[1], columnList[0]
153
-
154
- # initialize helper lists
155
- csvHead = []
153
+ with exportPath.open("w", encoding="utf-8", newline="\n") as exportFile:
154
+ exportOut = csv.writer(
155
+ exportFile,
156
+ delimiter=",",
157
+ quotechar="'",
158
+ doublequote=True,
159
+ quoting=csv.QUOTE_NONNUMERIC,
160
+ lineterminator="\n",
161
+ )
162
+
163
+ bind = session.get_bind()
164
+ inspector = inspect(bind)
165
+
166
+ try:
167
+ unique_cols = db_utils.get_unique_columns(session, tableName)
168
+ fk_list = db_utils.get_foreign_keys(session, tableName)
169
+ except Exception as e:
170
+ raise TradeException(f"Failed to introspect table '{tableName}': {e!r}")
171
+
172
+ csvHead = []
156
173
  stmtColumn = []
157
- stmtTable = [ tableName ]
158
- stmtOrder = []
159
- unqIndex = getUniqueIndex(conn, tableName)
160
- keyList = getFKeyList(conn, tableName)
161
-
162
- tdenv.DEBUG1('UNIQUE: ' + ", ".join(unqIndex))
163
-
164
- # iterate over all columns of the table
165
- for col in columnList:
166
- # check if the column is a foreign key
167
- key = search_keyList(keyList, col['name'])
168
- if key:
169
- # make the join statement
170
- keyStmt = buildFKeyStmt(conn, tableName, key)
171
- for keyRow in keyStmt:
172
- tdenv.DEBUG1('FK-Stmt: {}'.format(list(keyRow)))
173
- # is the join for the same table
174
- if keyRow['table'] == tableName:
175
- csvPfx = ''
176
- joinStmt = 'USING({})'.format(keyRow['joinColumn'])
177
- else:
178
- # this column must be ignored by the importer, it's only
179
- # used to resolve the FK relation
180
- csvPfx = ignorePfx
181
- joinStmt = 'ON {}.{} = {}.{}'.format(keyRow['table'], keyRow['joinColumn'], keyRow['joinTable'], keyRow['joinColumn'])
182
- if col['name'] in unqIndex:
183
- # column is part of an unique index
184
- csvPfx = uniquePfx + csvPfx
185
- csvHead += [ "{}{}@{}.{}".format(csvPfx, keyRow['column'], keyRow['joinTable'], keyRow['joinColumn']) ]
186
- stmtColumn += [ "{}.{}".format(keyRow['joinTable'], keyRow['column']) ]
187
- if col['notnull']:
188
- stmtTable += [ 'INNER JOIN {} {}'.format(keyRow['joinTable'], joinStmt) ]
189
- else:
190
- stmtTable += [ 'LEFT OUTER JOIN {} {}'.format(keyRow['joinTable'], joinStmt) ]
191
- stmtOrder += [ "{}.{}".format(keyRow['joinTable'], keyRow['column']) ]
174
+ stmtTable = [tableName]
175
+ stmtOrder = []
176
+ is_modified_col = []
177
+
178
+ for col in inspector.get_columns(tableName):
179
+ col_name = col["name"]
180
+ fk = next((fk for fk in fk_list if fk["from"] == col_name), None)
181
+ if fk:
182
+ joinTable = fk["table"]
183
+ joinColumn = fk["to"]
184
+ join_unique_cols = db_utils.get_unique_columns(session, joinTable)
185
+ if not join_unique_cols:
186
+ raise TradeException(
187
+ f"No unique column found in referenced table '{joinTable}'"
188
+ )
189
+ export_col = join_unique_cols[0]
190
+ csvPfx = uniquePfx if col_name in unique_cols else ""
191
+ csvHead.append(f"{csvPfx}{col_name}@{joinTable}.{export_col}")
192
+ stmtColumn.append(f"{joinTable}.{export_col}")
193
+ is_modified_col.append(export_col == "modified")
194
+ nullable = bool(col.get("nullable", True))
195
+ join_type = "LEFT OUTER JOIN" if nullable else "INNER JOIN"
196
+ stmtTable.append(
197
+ f"{join_type} {joinTable} ON {tableName}.{col_name} = {joinTable}.{joinColumn}"
198
+ )
199
+ stmtOrder.append(f"{joinTable}.{export_col}")
192
200
  else:
193
- # ordinary column
194
- if col['name'] in unqIndex:
195
- # column is part of an unique index
196
- csvHead += [ uniquePfx + col['name'] ]
197
- stmtOrder += [ "{}.{}".format(tableName, col['name']) ]
201
+ if col_name in unique_cols:
202
+ csvHead.append(uniquePfx + col_name)
203
+ stmtOrder.append(f"{tableName}.{col_name}")
198
204
  else:
199
- csvHead += [ col['name'] ]
200
- stmtColumn += [ "{}.{}".format(tableName, col['name']) ]
201
-
202
- # build the SQL statement
203
- sqlStmt = "SELECT {} FROM {}".format(",".join(stmtColumn), " ".join(stmtTable))
204
- if len(stmtOrder) > 0:
205
- sqlStmt += " ORDER BY {}".format(",".join(stmtOrder))
206
- tdenv.DEBUG1("SQL: %s" % sqlStmt)
207
-
208
- # finally generate the csv file
209
- # write header line without quotes
210
- exportFile.write("{}\n".format(",".join(csvHead)))
211
- for line in cur.execute(sqlStmt):
205
+ csvHead.append(col_name)
206
+ stmtColumn.append(f"{tableName}.{col_name}")
207
+ is_modified_col.append(col_name == "modified")
208
+
209
+ sqlStmt = f"SELECT {','.join(stmtColumn)} FROM {' '.join(stmtTable)}"
210
+ if stmtOrder:
211
+ sqlStmt += f" ORDER BY {','.join(stmtOrder)}"
212
+ tdenv.DEBUG1(f"SQL: {sqlStmt}")
213
+
214
+ exportFile.write(f"{','.join(csvHead)}\n")
215
+
216
+ for row in session.execute(text(sqlStmt)):
212
217
  lineCount += 1
213
- tdenv.DEBUG2("{count}: {values}".format(count=lineCount, values=list(line)))
214
- exportOut.writerow(list(line))
215
- tdenv.DEBUG1("{count} {table}s exported".format(count=lineCount, table=tableName))
216
-
217
- # Update the DB file so we don't regenerate it.
218
- os.utime(str(tdb.dbPath))
219
-
218
+ row_out = [
219
+ _fmt_ts(val) if is_modified_col[i] else val
220
+ for i, val in enumerate(row)
221
+ ]
222
+ tdenv.DEBUG2(f"{lineCount}: {row_out}")
223
+ exportOut.writerow(row_out)
224
+
225
+ tdenv.DEBUG1(f"{lineCount} {tableName}s exported")
226
+
227
+ # Close session if we created it
228
+ if hasattr(tdb_or_session, "engine"):
229
+ session.close()
230
+
220
231
  return lineCount, exportPath
tradedangerous/gui.py CHANGED
@@ -941,9 +941,9 @@ def main(argv = None):
941
941
  sys.argv = ['trade']
942
942
  if not argv:
943
943
  argv = sys.argv
944
- if sys.hexversion < 0x03040200:
944
+ if sys.hexversion < 0x30813F0:
945
945
  raise SystemExit(
946
- "Sorry: TradeDangerous requires Python 3.4.2 or higher.\n"
946
+ "Sorry: TradeDangerous requires Python 3.8.19 or higher.\n"
947
947
  "For assistance, see:\n"
948
948
  "\tBug Tracker: https://github.com/eyeonus/Trade-Dangerous/issues\n"
949
949
  "\tDocumentation: https://github.com/eyeonus/Trade-Dangerous/wiki\n"