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.

Files changed (47) 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 +67 -31
  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/db/__init__.py +27 -0
  20. tradedangerous/db/adapter.py +191 -0
  21. tradedangerous/db/config.py +95 -0
  22. tradedangerous/db/engine.py +246 -0
  23. tradedangerous/db/lifecycle.py +332 -0
  24. tradedangerous/db/locks.py +208 -0
  25. tradedangerous/db/orm_models.py +455 -0
  26. tradedangerous/db/paths.py +112 -0
  27. tradedangerous/db/utils.py +661 -0
  28. tradedangerous/gui.py +2 -2
  29. tradedangerous/plugins/eddblink_plug.py +387 -251
  30. tradedangerous/plugins/spansh_plug.py +2488 -821
  31. tradedangerous/prices.py +124 -142
  32. tradedangerous/templates/TradeDangerous.sql +6 -6
  33. tradedangerous/tradecalc.py +1227 -1109
  34. tradedangerous/tradedb.py +533 -384
  35. tradedangerous/tradeenv.py +12 -1
  36. tradedangerous/version.py +1 -1
  37. {tradedangerous-11.5.3.dist-info → tradedangerous-12.0.1.dist-info}/METADATA +11 -7
  38. {tradedangerous-11.5.3.dist-info → tradedangerous-12.0.1.dist-info}/RECORD +42 -38
  39. {tradedangerous-11.5.3.dist-info → tradedangerous-12.0.1.dist-info}/WHEEL +1 -1
  40. tradedangerous/commands/update_gui.py +0 -721
  41. tradedangerous/jsonprices.py +0 -254
  42. tradedangerous/plugins/edapi_plug.py +0 -1071
  43. tradedangerous/plugins/journal_plug.py +0 -537
  44. tradedangerous/plugins/netlog_plug.py +0 -316
  45. {tradedangerous-11.5.3.dist-info → tradedangerous-12.0.1.dist-info}/entry_points.txt +0 -0
  46. {tradedangerous-11.5.3.dist-info → tradedangerous-12.0.1.dist-info/licenses}/LICENSE +0 -0
  47. {tradedangerous-11.5.3.dist-info → tradedangerous-12.0.1.dist-info}/top_level.txt +0 -0
tradedangerous/tradedb.py CHANGED
@@ -1,7 +1,8 @@
1
1
  # --------------------------------------------------------------------
2
2
  # Copyright (C) Oliver 'kfsone' Smith 2014 <oliver@kfs.org>:
3
3
  # Copyright (C) Bernd 'Gazelle' Gollesch 2016, 2017
4
- # Copyright (C) Jonathan 'eyeonus' Jones 2018, 2019
4
+ # Copyright (C) Stefan 'Tromador' Morrell 2025
5
+ # Copyright (C) Jonathan 'eyeonus' Jones 2018 - 2025
5
6
  #
6
7
  # You are free to use, redistribute, or even print and eat a copy of
7
8
  # this software so long as you include this copyright notice.
@@ -59,7 +60,6 @@ import heapq
59
60
  import itertools
60
61
  import locale
61
62
  import re
62
- import sqlite3
63
63
  import sys
64
64
  import typing
65
65
 
@@ -74,6 +74,44 @@ if typing.TYPE_CHECKING:
74
74
 
75
75
  locale.setlocale(locale.LC_ALL, '')
76
76
 
77
+ from sqlalchemy import func, select
78
+ from sqlalchemy.orm import Session
79
+ from .db import make_engine_from_config, get_session_factory, healthcheck
80
+ from .db.orm_models import (
81
+ System, Station, Item, Category, Ship, Upgrade, RareItem,
82
+ StationItem, ShipVendor, UpgradeVendor, Added, ExportControl, StationItemStaging
83
+ )
84
+ from .db.utils import age_in_days
85
+
86
+ # --------------------------------------------------------------------
87
+ # SQLAlchemy ORM imports (aliased to avoid clashing with legacy wrappers).
88
+ # These map to the actual database tables via SQLAlchemy and are used
89
+ # internally in loaders/writers to replace raw sqlite3 queries.
90
+ #
91
+ # NOTE: We still instantiate and use legacy wrapper classes defined in
92
+ # this file (System, Station, Item, etc.) to maintain API compatibility
93
+ # across the rest of the codebase (Pass 1 migration).
94
+ #
95
+ # In a possible future cleanup (Pass 2), the wrappers may be removed
96
+ # entirely, and code updated to use ORM models directly.
97
+ # --------------------------------------------------------------------
98
+
99
+ from .db.orm_models import (
100
+ Added as SA_Added,
101
+ System as SA_System,
102
+ Station as SA_Station,
103
+ Item as SA_Item,
104
+ Category as SA_Category,
105
+ StationItem as SA_StationItem,
106
+ RareItem as SA_RareItem,
107
+ Ship as SA_Ship,
108
+ ShipVendor as SA_ShipVendor,
109
+ Upgrade as SA_Upgrade,
110
+ UpgradeVendor as SA_UpgradeVendor,
111
+ ExportControl as SA_ExportControl,
112
+ StationItemStaging as SA_StationItemStaging,
113
+ )
114
+
77
115
 
78
116
  ######################################################################
79
117
  # Classes
@@ -570,53 +608,91 @@ class TradeDB:
570
608
  load=True,
571
609
  debug=None,
572
610
  ):
573
- self.conn: sqlite3.Connection = None
611
+ # --- SQLAlchemy engine/session (replaces sqlite3.Connection) ---
612
+ self.engine = None
613
+ self.Session = None
574
614
  self.tradingCount = None
575
-
615
+
616
+ # Environment
576
617
  tdenv = tdenv or TradeEnv(debug=(debug or 0))
577
618
  self.tdenv = tdenv
578
-
619
+
620
+ # --- Path setup (unchanged) ---
579
621
  self.templatePath = Path(tdenv.templateDir).resolve()
580
622
  self.dataPath = dataPath = fs.ensurefolder(tdenv.dataDir)
581
623
  self.csvPath = fs.ensurefolder(tdenv.csvDir)
582
-
583
- fs.copy_if_newer((self.templatePath / Path("Added.csv")), (self.csvPath / Path("Added.csv")))
584
- fs.copy_if_newer((self.templatePath / Path("RareItem.csv")), (self.csvPath / Path("RareItem.csv")))
585
- fs.copy_if_newer((self.templatePath / Path("Category.csv")), (self.csvPath / Path("Category.csv")))
586
- fs.copy_if_newer((self.templatePath / Path("TradeDangerous.sql")), (self.dataPath / Path("TradeDangerous.sql")))
587
-
624
+
625
+ fs.copy_if_newer(self.templatePath / "Added.csv", self.csvPath / "Added.csv")
626
+ fs.copy_if_newer(self.templatePath / "RareItem.csv", self.csvPath / "RareItem.csv")
627
+ fs.copy_if_newer(self.templatePath / "Category.csv", self.csvPath / "Category.csv")
628
+ fs.copy_if_newer(self.templatePath / "TradeDangerous.sql", self.dataPath / "TradeDangerous.sql")
629
+
588
630
  self.dbPath = Path(tdenv.dbFilename or dataPath / TradeDB.defaultDB)
589
631
  self.sqlPath = dataPath / Path(tdenv.sqlFilename or TradeDB.defaultSQL)
590
- pricePath = Path(tdenv.pricesFilename or TradeDB.defaultPrices)
632
+ pricePath = Path(tdenv.pricesFilename or TradeDB.defaultPrices)
591
633
  self.pricesPath = dataPath / pricePath
634
+
592
635
  self.importTables = [
593
636
  (str(self.csvPath / Path(fn)), tn)
594
637
  for fn, tn in TradeDB.defaultTables
595
638
  ]
596
639
  self.importPaths = {tn: tp for tp, tn in self.importTables}
597
-
598
- self.dbFilename = str(self.dbPath)
599
- self.sqlFilename = str(self.sqlPath)
640
+
641
+ self.dbFilename = str(self.dbPath)
642
+ self.sqlFilename = str(self.sqlPath)
600
643
  self.pricesFilename = str(self.pricesPath)
601
-
644
+
645
+ # --- Cache attributes (unchanged) ---
602
646
  self.avgSelling, self.avgBuying = None, None
603
647
  self.tradingStationCount = 0
604
- self.addedByID = None
605
- self.systemByID = None
606
- self.systemByName = None
607
- self.stellarGrid = None
608
- self.stationByID = None
609
- self.shipByID = None
610
- self.categoryByID = None
611
- self.itemByID = None
612
- self.itemByName = None
613
- self.itemByFDevID = None
614
- self.rareItemByID = None
648
+ self.addedByID = None
649
+ self.systemByID = None
650
+ self.systemByName = None
651
+ self.stellarGrid = None
652
+ self.stationByID = None
653
+ self.shipByID = None
654
+ self.categoryByID = None
655
+ self.itemByID = None
656
+ self.itemByName = None
657
+ self.itemByFDevID = None
658
+ self.rareItemByID = None
615
659
  self.rareItemByName = None
616
-
660
+
661
+ # --- Engine bootstrap ---
662
+ from .db import make_engine_from_config, get_session_factory
663
+ import os
664
+
665
+ cfg = getattr(tdenv, "dbConfig", None)
666
+ if not cfg:
667
+ cfg = os.environ.get("TD_DB_CONFIG", "db_config.ini")
668
+
669
+ self.engine = make_engine_from_config(cfg)
670
+ self.Session = get_session_factory(self.engine)
671
+
672
+
673
+ # --- Initial load ---
617
674
  if load:
618
675
  self.reloadCache()
619
676
  self.load(maxSystemLinkLy=tdenv.maxSystemLinkLy)
677
+
678
+ # ------------------------------------------------------------------
679
+ # Legacy compatibility dataPath shim
680
+ # ------------------------------------------------------------------
681
+ @property
682
+ def dataDir(self):
683
+ """
684
+ Legacy alias for self.dataPath (removed in SQLAlchemy refactor).
685
+ Falls back to './data' if configuration not yet loaded.
686
+ """
687
+ # Try the modern attribute first
688
+ if hasattr(self, "dataPath") and self.dataPath:
689
+ return self.dataPath
690
+ # If we have an environment object, use its dataDir
691
+ if hasattr(self, "tdenv") and getattr(self.tdenv, "dataDir", None):
692
+ return self.tdenv.dataDir
693
+ # Final fallback (first run, pre-bootstrap)
694
+ return Path("./data")
695
+
620
696
 
621
697
  @staticmethod
622
698
  def calculateDistance2(lx, ly, lz, rx, ry, rz):
@@ -637,85 +713,85 @@ class TradeDB:
637
713
  ############################################################
638
714
  # Access to the underlying database.
639
715
 
640
- def getDB(self) -> sqlite3.Connection:
641
- if self.conn:
642
- return self.conn
643
- self.tdenv.DEBUG1("Connecting to DB")
644
- conn = sqlite3.connect(self.dbFilename)
645
- conn.execute("PRAGMA foreign_keys=ON")
646
- conn.execute("PRAGMA synchronous=OFF")
647
- conn.execute("PRAGMA temp_store=MEMORY")
648
- conn.execute("PRAGMA auto_vacuum=INCREMENTAL")
649
-
650
- conn.create_function('dist2', 6, TradeDB.calculateDistance2)
651
- self.conn = conn
652
- return conn
653
-
654
- def query(self, *args):
655
- """ Perform an SQL query on the DB and return the cursor. """
656
- return self.getDB().execute(*args)
657
-
658
- def queryColumn(self, *args):
659
- """ perform an SQL query and return a single column. """
660
- return self.query(args).fetchone()[0]
716
+ def getDB(self):
717
+ """
718
+ Return a new SQLAlchemy Session bound to this TradeDB engine.
719
+ """
720
+ if not self.engine:
721
+ raise TradeException("Database engine not initialised")
722
+ return self.Session()
723
+
724
+ def query(self, sql: str, *params):
725
+ """
726
+ Execute a SQL statement via the SQLAlchemy engine and return the result cursor.
727
+ """
728
+ from sqlalchemy import text
729
+ with self.engine.connect() as conn:
730
+ return conn.execute(text(sql), params)
731
+
732
+ def queryColumn(self, sql: str, *params):
733
+ """
734
+ Execute a SQL statement and return the first column of the first row.
735
+ """
736
+ result = self.query(sql, *params).first()
737
+ return result[0] if result else None
738
+
661
739
 
662
740
  def reloadCache(self):
663
741
  """
664
- Checks if the .sql, .prices or *.csv files are newer than the cache.
742
+ Ensure DB is present and minimally populated using the central policy.
743
+
744
+ Delegates sanity checks to lifecycle.ensure_fresh_db (seconds-only checks):
745
+ - core tables exist (System, Station, Category, Item, StationItem)
746
+ - each has a primary key
747
+ - seed rows exist (Category > 0, System > 0)
748
+ - cheap connectivity probe
749
+
750
+ If checks fail (or lifecycle decides to force), it will call buildCache(self, self.tdenv)
751
+ to reset/populate via the authoritative path. Otherwise it is a no-op.
665
752
  """
666
-
667
- if self.dbPath.exists():
668
- dbFileStamp = self.dbPath.stat().st_mtime
669
-
670
- paths = [self.sqlPath]
671
- paths += [Path(f) for (f, _) in self.importTables]
672
-
673
- changedPaths = [
674
- [path, path.stat().st_mtime]
675
- for path in paths
676
- if path.exists() and path.stat().st_mtime > dbFileStamp
677
- ]
678
-
679
- if not changedPaths:
680
- # Do we need to reload the .prices file?
681
- if not self.pricesPath.exists():
682
- self.tdenv.DEBUG1("No .prices file to load")
683
- return
684
-
685
- pricesStamp = self.pricesPath.stat().st_mtime
686
- if pricesStamp <= dbFileStamp:
687
- self.tdenv.DEBUG1("DB Cache is up to date.")
688
- return
689
-
690
- self.tdenv.DEBUG0(".prices has changed: re-importing")
691
- cache.importDataFromFile(
692
- self, self.tdenv, self.pricesPath, reset=True
693
- )
694
- return
695
-
696
- self.tdenv.DEBUG0("Rebuilding DB Cache [{}]", str(changedPaths))
697
- else:
698
- self.tdenv.DEBUG0("Building DB Cache")
699
-
700
- cache.buildCache(self, self.tdenv)
753
+ from tradedangerous.db.lifecycle import ensure_fresh_db
754
+
755
+ self.tdenv.DEBUG0("reloadCache: engine URL = {}", str(self.engine.url))
756
+
757
+ try:
758
+ summary = ensure_fresh_db(
759
+ backend=self.engine.dialect.name,
760
+ engine=self.engine,
761
+ data_dir=self.dataPath,
762
+ metadata=None,
763
+ mode="auto",
764
+ tdb=self,
765
+ tdenv=self.tdenv,
766
+ )
767
+ action = summary.get("action", "kept")
768
+ reason = summary.get("reason")
769
+ if reason:
770
+ self.tdenv.DEBUG0("reloadCache: ensure_fresh_db → {} (reason: {})", action, reason)
771
+ else:
772
+ self.tdenv.DEBUG0("reloadCache: ensure_fresh_db → {}", action)
773
+ except Exception as e:
774
+ self.tdenv.WARN("reloadCache: ensure_fresh_db failed: {}", e)
775
+ self.tdenv.DEBUG0("reloadCache: Falling back to buildCache()")
776
+ from tradedangerous import cache
777
+ cache.buildCache(self, self.tdenv)
778
+
779
+
701
780
 
702
781
  ############################################################
703
782
  # Load "added" data.
704
783
 
705
784
  def _loadAdded(self):
706
785
  """
707
- Loads the Added table as a simple dictionary
708
- """
709
- stmt = """
710
- SELECT added_id, name
711
- FROM Added
786
+ Loads the Added table as a simple dictionary.
712
787
  """
713
788
  addedByID = {}
714
- with closing(self.query(stmt)) as cur:
715
- for ID, name in cur:
716
- addedByID[ID] = name
789
+ with self.Session() as session:
790
+ for row in session.query(Added.added_id, Added.name):
791
+ addedByID[row.added_id] = row.name
717
792
  self.addedByID = addedByID
718
793
  self.tdenv.DEBUG1("Loaded {:n} Addeds", len(addedByID))
794
+
719
795
 
720
796
  def lookupAdded(self, name):
721
797
  name = name.lower()
@@ -733,24 +809,33 @@ class TradeDB:
733
809
 
734
810
  def _loadSystems(self):
735
811
  """
736
- Initial load the (raw) list of systems.
812
+ Initial load of the list of systems via SQLAlchemy.
737
813
  CAUTION: Will orphan previously loaded objects.
738
814
  """
739
- stmt = """
740
- SELECT system_id,
741
- name, pos_x, pos_y, pos_z,
742
- added_id
743
- FROM System
744
- """
745
-
746
815
  systemByID, systemByName = {}, {}
747
- with closing(self.getDB().execute(stmt)) as cur:
748
- for (ID, name, posX, posY, posZ, addedID) in cur:
749
- system = System(ID, name, posX, posY, posZ, addedID)
750
- systemByID[ID] = systemByName[name.upper()] = system
751
-
816
+ with self.Session() as session:
817
+ for row in session.query(
818
+ SA_System.system_id,
819
+ SA_System.name,
820
+ SA_System.pos_x,
821
+ SA_System.pos_y,
822
+ SA_System.pos_z,
823
+ SA_System.added_id,
824
+ ):
825
+ system = System(
826
+ row.system_id,
827
+ row.name,
828
+ row.pos_x,
829
+ row.pos_y,
830
+ row.pos_z,
831
+ row.added_id,
832
+ )
833
+ systemByID[row.system_id] = system
834
+ systemByName[row.name.upper()] = system
835
+
752
836
  self.systemByID, self.systemByName = systemByID, systemByName
753
837
  self.tdenv.DEBUG1("Loaded {:n} Systems", len(systemByID))
838
+
754
839
 
755
840
  def lookupSystem(self, key):
756
841
  """
@@ -769,40 +854,43 @@ class TradeDB:
769
854
  self,
770
855
  name,
771
856
  x, y, z,
772
- added="Local",
773
857
  modified='now',
774
858
  commit=True,
775
859
  ):
776
860
  """
777
- Add a system to the local cache and memory copy.
778
- """
779
-
780
- db = self.getDB()
781
- cur = db.cursor()
782
- cur.execute("""
783
- INSERT INTO System (
784
- name, pos_x, pos_y, pos_z, added_id, modified
785
- ) VALUES (
786
- ?, ?, ?, ?,
787
- (SELECT added_id FROM Added WHERE name = ?),
788
- DATETIME(?)
789
- )
790
- """, [
791
- name, x, y, z, added, modified,
792
- ])
793
- ID = cur.lastrowid
794
- system = System(ID, name.upper(), x, y, z, 0)
861
+ Add a system to the local cache and memory copy using SQLAlchemy.
862
+ Note: 'added' field has been deprecated and is no longer populated.
863
+ """
864
+ with self.Session() as session:
865
+ # Create ORM System row (added_id is deprecated → NULL)
866
+ orm_system = SA_System(
867
+ name=name,
868
+ pos_x=x,
869
+ pos_y=y,
870
+ pos_z=z,
871
+ added_id=None,
872
+ modified=None if modified == 'now' else modified,
873
+ )
874
+ session.add(orm_system)
875
+ if commit:
876
+ session.commit()
877
+ else:
878
+ session.flush()
879
+
880
+ ID = orm_system.system_id
881
+
882
+ # Maintain legacy wrapper + caches (added_id always None now)
883
+ system = System(ID, name.upper(), x, y, z, None)
795
884
  self.systemByID[ID] = system
796
885
  self.systemByName[system.dbname] = system
797
- if commit:
798
- db.commit()
886
+
799
887
  self.tdenv.NOTE(
800
888
  "Added new system #{}: {} [{},{},{}]",
801
889
  ID, name, x, y, z
802
890
  )
803
- # Invalidate the grid
804
891
  self.stellarGrid = None
805
892
  return system
893
+
806
894
 
807
895
  def updateLocalSystem(
808
896
  self, system,
@@ -811,69 +899,92 @@ class TradeDB:
811
899
  commit=True,
812
900
  ):
813
901
  """
814
- Updates an entry for a local system.
902
+ Update an entry for a local system using SQLAlchemy.
815
903
  """
816
904
  oldname = system.dbname
817
905
  dbname = name.upper()
906
+
818
907
  if not force:
819
- if oldname == dbname and \
820
- system.posX == x and \
821
- system.posY == y and \
822
- system.posZ == z:
908
+ if (oldname == dbname and
909
+ system.posX == x and
910
+ system.posY == y and
911
+ system.posZ == z):
823
912
  return False
913
+
824
914
  del self.systemByName[oldname]
825
- db = self.getDB()
826
- db.execute("""
827
- UPDATE System
828
- SET name=?,
829
- pos_x=?, pos_y=?, pos_z=?,
830
- added_id=(SELECT added_id FROM Added WHERE name = ?),
831
- modified=DATETIME(?)
832
- WHERE system_id = ?
833
- """, [
834
- dbname, x, y, z, added, modified,
835
- system.ID,
836
- ])
837
- if commit:
838
- db.commit()
915
+
916
+ with self.Session() as session:
917
+ # Find Added row for added_id
918
+ added_row = session.query(Added).filter(Added.name == added).first()
919
+ if not added_row:
920
+ raise TradeException(f"Added entry not found: {added}")
921
+
922
+ # Load ORM System row
923
+ orm_system = session.get(SA_System, system.ID)
924
+ if not orm_system:
925
+ raise TradeException(f"System ID not found: {system.ID}")
926
+
927
+ # Apply updates
928
+ orm_system.name = dbname
929
+ orm_system.pos_x = x
930
+ orm_system.pos_y = y
931
+ orm_system.pos_z = z
932
+ orm_system.added_id = added_row.added_id
933
+ orm_system.modified = None if modified == 'now' else modified
934
+
935
+ if commit:
936
+ session.commit()
937
+ else:
938
+ session.flush()
939
+
839
940
  self.tdenv.NOTE(
840
941
  "{} (#{}) updated in {}: {}, {}, {}, {}, {}, {}",
841
942
  oldname, system.ID,
842
943
  self.dbPath if self.tdenv.detail > 1 else "local db",
843
- dbname,
844
- x, y, z,
845
- added, modified,
944
+ dbname, x, y, z, added, modified,
846
945
  )
946
+
947
+ # Update wrapper caches
948
+ system.name = dbname
949
+ system.posX, system.posY, system.posZ = x, y, z
950
+ system.addedID = added_row.added_id
847
951
  self.systemByName[dbname] = system
848
-
952
+
849
953
  return True
954
+
850
955
 
851
956
  def removeLocalSystem(
852
957
  self, system,
853
958
  commit=True,
854
959
  ):
855
- """ Removes a system and it's stations from the local DB. """
960
+ """Remove a system and its stations from the local DB using SQLAlchemy."""
961
+ # First remove stations attached to this system
856
962
  for stn in self.stations():
857
- self.removeLocalStation(stn, commit=False)
858
- db = self.getDB()
859
- db.execute("""
860
- DELETE FROM System WHERE system_id = ?
861
- """, [
862
- system.ID
863
- ])
864
- if commit:
865
- db.commit()
963
+ if stn.system == system:
964
+ self.removeLocalStation(stn, commit=False)
965
+
966
+ with self.Session() as session:
967
+ orm_system = session.get(SA_System, system.ID)
968
+ if orm_system:
969
+ session.delete(orm_system)
970
+ if commit:
971
+ session.commit()
972
+ else:
973
+ session.flush()
974
+
975
+ # Update caches
866
976
  del self.systemByName[system.dbname]
867
977
  del self.systemByID[system.ID]
868
-
978
+
869
979
  self.tdenv.NOTE(
870
980
  "{} (#{}) deleted from {}",
871
- system.name(), system.ID,
981
+ system.name, system.ID,
872
982
  self.dbPath if self.tdenv.detail > 1 else "local db",
873
983
  )
874
-
984
+
875
985
  system.dbname = "DELETED " + system.dbname
876
986
  del system
987
+
877
988
 
878
989
  def __buildStellarGrid(self):
879
990
  """
@@ -1134,61 +1245,80 @@ class TradeDB:
1134
1245
 
1135
1246
  def _loadStations(self):
1136
1247
  """
1137
- Populate the Station list.
1248
+ Populate the Station list using SQLAlchemy.
1138
1249
  Station constructor automatically adds itself to the System object.
1139
1250
  CAUTION: Will orphan previously loaded objects.
1140
1251
  """
1141
- stmt = """
1142
- SELECT station_id, system_id, name,
1143
- ls_from_star, market, blackmarket, shipyard,
1144
- max_pad_size, outfitting, rearm, refuel, repair, planetary, type_id
1145
- FROM Station
1146
- """
1147
-
1252
+ # NOTE: Requires module-level import:
1253
+ # from tradedangerous.db.utils import age_in_days
1148
1254
  stationByID = {}
1149
1255
  systemByID = self.systemByID
1150
1256
  self.tradingStationCount = 0
1257
+
1151
1258
  # Fleet Carriers are station type 24.
1152
1259
  # Odyssey settlements are station type 25.
1153
1260
  # Assume type 0 (Unknown) are also Fleet Carriers.
1154
- # Storing as a list allows easy expansion if needed.
1155
- types = {'fleet-carrier':[24, 0,],'odyssey':[25,],}
1156
- with closing(self.query(stmt)) as cur:
1261
+ types = {'fleet-carrier': [24, 0], 'odyssey': [25]}
1262
+
1263
+ with self.Session() as session:
1264
+ # Query all stations
1265
+ rows = session.query(
1266
+ SA_Station.station_id,
1267
+ SA_Station.system_id,
1268
+ SA_Station.name,
1269
+ SA_Station.ls_from_star,
1270
+ SA_Station.market,
1271
+ SA_Station.blackmarket,
1272
+ SA_Station.shipyard,
1273
+ SA_Station.max_pad_size,
1274
+ SA_Station.outfitting,
1275
+ SA_Station.rearm,
1276
+ SA_Station.refuel,
1277
+ SA_Station.repair,
1278
+ SA_Station.planetary,
1279
+ SA_Station.type_id,
1280
+ )
1157
1281
  for (
1158
1282
  ID, systemID, name,
1159
1283
  lsFromStar, market, blackMarket, shipyard,
1160
1284
  maxPadSize, outfitting, rearm, refuel, repair, planetary, type_id
1161
- ) in cur:
1162
- isFleet = 'Y' if int(type_id) in types['fleet-carrier'] else 'N'
1285
+ ) in rows:
1286
+ isFleet = 'Y' if int(type_id) in types['fleet-carrier'] else 'N'
1163
1287
  isOdyssey = 'Y' if int(type_id) in types['odyssey'] else 'N'
1164
1288
  station = Station(
1165
1289
  ID, systemByID[systemID], name,
1166
1290
  lsFromStar, market, blackMarket, shipyard,
1167
- maxPadSize, outfitting, rearm, refuel, repair, planetary, isFleet, isOdyssey,
1291
+ maxPadSize, outfitting, rearm, refuel, repair,
1292
+ planetary, isFleet, isOdyssey,
1168
1293
  0, None,
1169
1294
  )
1170
1295
  stationByID[ID] = station
1171
-
1172
- tradingCount = 0
1173
- stmt = """
1174
- SELECT station_id,
1175
- COUNT(*) AS item_count,
1176
- AVG(JULIANDAY('now') - JULIANDAY(modified))
1177
- FROM StationItem
1178
- GROUP BY 1
1179
- HAVING item_count > 0
1180
- """
1181
- with closing(self.query(stmt)) as cur:
1182
- for ID, itemCount, dataAge in cur:
1296
+
1297
+ # Trading station info
1298
+ tradingCount = 0
1299
+ rows = (
1300
+ session.query(
1301
+ SA_StationItem.station_id,
1302
+ func.count().label("item_count"),
1303
+ # Dialect-safe average age in **days**
1304
+ func.avg(age_in_days(session, SA_StationItem.modified)).label("data_age_days"),
1305
+ )
1306
+ .group_by(SA_StationItem.station_id)
1307
+ .having(func.count() > 0)
1308
+ )
1309
+
1310
+ for ID, itemCount, dataAge in rows:
1183
1311
  station = stationByID[ID]
1184
1312
  station.itemCount = itemCount
1185
1313
  station.dataAge = dataAge
1186
1314
  tradingCount += 1
1187
-
1315
+
1188
1316
  self.stationByID = stationByID
1189
1317
  self.tradingStationCount = tradingCount
1190
1318
  self.tdenv.DEBUG1("Loaded {:n} Stations", len(stationByID))
1191
1319
  self.stellarGrid = None
1320
+
1321
+
1192
1322
 
1193
1323
  def addLocalStation(
1194
1324
  self,
@@ -1210,18 +1340,18 @@ class TradeDB:
1210
1340
  commit=True,
1211
1341
  ):
1212
1342
  """
1213
- Add a station to the local cache and memory copy.
1343
+ Add a station to the local cache and memory copy using SQLAlchemy.
1214
1344
  """
1215
-
1216
- market = market.upper()
1345
+ # Normalise/validate inputs
1346
+ market = market.upper()
1217
1347
  blackMarket = blackMarket.upper()
1218
- shipyard = shipyard.upper()
1219
- maxPadSize = maxPadSize.upper()
1220
- outfitting = outfitting.upper()
1221
- rearm = rearm.upper()
1222
- refuel = refuel.upper()
1223
- repair = repair.upper()
1224
- planetary = planetary.upper()
1348
+ shipyard = shipyard.upper()
1349
+ maxPadSize = maxPadSize.upper()
1350
+ outfitting = outfitting.upper()
1351
+ rearm = rearm.upper()
1352
+ refuel = refuel.upper()
1353
+ repair = repair.upper()
1354
+ planetary = planetary.upper()
1225
1355
  assert market in "?YN"
1226
1356
  assert blackMarket in "?YN"
1227
1357
  assert shipyard in "?YN"
@@ -1230,37 +1360,42 @@ class TradeDB:
1230
1360
  assert rearm in "?YN"
1231
1361
  assert refuel in "?YN"
1232
1362
  assert repair in "?YN"
1233
- assert planetary in '?YN'
1234
- assert fleet in '?YN'
1235
- assert odyssey in '?YN'
1236
-
1363
+ assert planetary in "?YN"
1364
+ assert fleet in "?YN"
1365
+ assert odyssey in "?YN"
1366
+
1367
+ # Type mapping
1237
1368
  type_id = 0
1238
1369
  if fleet == 'Y':
1239
1370
  type_id = 24
1240
1371
  if odyssey == 'Y':
1241
1372
  type_id = 25
1242
-
1243
- db = self.getDB()
1244
- cur = db.cursor()
1245
- cur.execute("""
1246
- INSERT INTO Station (
1247
- name, system_id,
1248
- ls_from_star, market, blackmarket, shipyard, max_pad_size,
1249
- outfitting, rearm, refuel, repair, planetary, type_id,
1250
- modified
1251
- ) VALUES (
1252
- ?, ?,
1253
- ?, ?, ?, ?, ?,
1254
- ?, ?, ?, ?, ?, ?,
1255
- DATETIME(?)
1373
+
1374
+ with self.Session() as session:
1375
+ orm_station = SA_Station(
1376
+ name=name,
1377
+ system_id=system.ID,
1378
+ ls_from_star=lsFromStar,
1379
+ market=market,
1380
+ blackmarket=blackMarket,
1381
+ shipyard=shipyard,
1382
+ max_pad_size=maxPadSize,
1383
+ outfitting=outfitting,
1384
+ rearm=rearm,
1385
+ refuel=refuel,
1386
+ repair=repair,
1387
+ planetary=planetary,
1388
+ type_id=type_id,
1389
+ modified=None if modified == 'now' else modified,
1256
1390
  )
1257
- """, [
1258
- name, system.ID,
1259
- lsFromStar, market, blackMarket, shipyard, maxPadSize,
1260
- outfitting, rearm, refuel, repair, planetary, type_id,
1261
- modified,
1262
- ])
1263
- ID = cur.lastrowid
1391
+ session.add(orm_station)
1392
+ if commit:
1393
+ session.commit()
1394
+ else:
1395
+ session.flush()
1396
+ ID = orm_station.station_id
1397
+
1398
+ # Legacy wrapper object
1264
1399
  station = Station(
1265
1400
  ID, system, name,
1266
1401
  lsFromStar=lsFromStar,
@@ -1273,13 +1408,13 @@ class TradeDB:
1273
1408
  refuel=refuel,
1274
1409
  repair=repair,
1275
1410
  planetary=planetary,
1276
- fleet='?',
1277
- odyssey='?',
1278
- itemCount=0, dataAge=0,
1411
+ fleet=fleet,
1412
+ odyssey=odyssey,
1413
+ itemCount=0,
1414
+ dataAge=0,
1279
1415
  )
1280
1416
  self.stationByID[ID] = station
1281
- if commit:
1282
- db.commit()
1417
+
1283
1418
  self.tdenv.NOTE(
1284
1419
  "{} (#{}) added to {}: "
1285
1420
  "ls={}, mkt={}, bm={}, yard={}, pad={}, "
@@ -1313,36 +1448,35 @@ class TradeDB:
1313
1448
  commit=True,
1314
1449
  ):
1315
1450
  """
1316
- Alter the properties of a station in-memory and in the DB.
1451
+ Alter the properties of a station in-memory and in the DB using SQLAlchemy.
1317
1452
  """
1318
1453
  changes = []
1319
-
1454
+
1320
1455
  def _changed(label, old, new):
1321
- changes.append(
1322
- f"{label}('{old}'=>'{new}')"
1323
- )
1324
-
1456
+ changes.append(f"{label}('{old}'=>'{new}')")
1457
+
1458
+ # Mutate wrapper + record changes
1325
1459
  if name is not None:
1326
1460
  if force or name.upper() != station.dbname.upper():
1327
1461
  _changed("name", station.dbname, name)
1328
1462
  station.dbname = name
1329
-
1463
+
1330
1464
  if lsFromStar is not None:
1331
1465
  assert lsFromStar >= 0
1332
1466
  if lsFromStar != station.lsFromStar:
1333
1467
  if lsFromStar > 0 or force:
1334
1468
  _changed("ls", station.lsFromStar, lsFromStar)
1335
1469
  station.lsFromStar = lsFromStar
1336
-
1337
- def _check_setting(label, name, newValue, allowed):
1470
+
1471
+ def _check_setting(label, attr_name, newValue, allowed):
1338
1472
  if newValue is not None:
1339
1473
  newValue = newValue.upper()
1340
1474
  assert newValue in allowed
1341
- oldValue = getattr(station, name, '?')
1475
+ oldValue = getattr(station, attr_name, '?')
1342
1476
  if newValue != oldValue and (force or newValue != '?'):
1343
1477
  _changed(label, oldValue, newValue)
1344
- setattr(station, name, newValue)
1345
-
1478
+ setattr(station, attr_name, newValue)
1479
+
1346
1480
  _check_setting("pad", "maxPadSize", maxPadSize, TradeDB.padSizes)
1347
1481
  _check_setting("mkt", "market", market, TradeDB.marketStates)
1348
1482
  _check_setting("blk", "blackMarket", blackMarket, TradeDB.marketStates)
@@ -1354,85 +1488,78 @@ class TradeDB:
1354
1488
  _check_setting("plt", "planetary", planetary, TradeDB.planetStates)
1355
1489
  _check_setting("flc", "fleet", fleet, TradeDB.fleetStates)
1356
1490
  _check_setting("ody", "odyssey", odyssey, TradeDB.odysseyStates)
1357
-
1491
+
1358
1492
  if not changes:
1359
1493
  return False
1360
-
1361
- db = self.getDB()
1362
- db.execute("""
1363
- UPDATE Station
1364
- SET name=?,
1365
- ls_from_star=?,
1366
- market=?,
1367
- blackmarket=?,
1368
- shipyard=?,
1369
- max_pad_size=?,
1370
- outfitting=?,
1371
- rearm=?,
1372
- refuel=?,
1373
- repair=?,
1374
- planetary=?,
1375
- modified=DATETIME(?)
1376
- WHERE station_id = ?
1377
- """, [
1378
- station.dbname,
1379
- station.lsFromStar,
1380
- station.market,
1381
- station.blackMarket,
1382
- station.shipyard,
1383
- station.maxPadSize,
1384
- station.outfitting,
1385
- station.rearm,
1386
- station.refuel,
1387
- station.repair,
1388
- station.planetary,
1389
- modified,
1390
- station.ID
1391
- ])
1392
- if commit:
1393
- db.commit()
1394
-
1494
+
1495
+ with self.Session() as session:
1496
+ orm_station = session.get(SA_Station, station.ID)
1497
+ if not orm_station:
1498
+ raise TradeException(f"Station ID not found: {station.ID}")
1499
+
1500
+ orm_station.name = station.dbname
1501
+ orm_station.system_id = station.system.ID
1502
+ orm_station.ls_from_star = station.lsFromStar
1503
+ orm_station.market = station.market
1504
+ orm_station.blackmarket = station.blackMarket
1505
+ orm_station.shipyard = station.shipyard
1506
+ orm_station.max_pad_size = station.maxPadSize
1507
+ orm_station.outfitting = station.outfitting
1508
+ orm_station.rearm = station.rearm
1509
+ orm_station.refuel = station.refuel
1510
+ orm_station.repair = station.repair
1511
+ orm_station.planetary = station.planetary
1512
+ orm_station.type_id = (
1513
+ 24 if station.fleet == 'Y' else
1514
+ 25 if station.odyssey == 'Y' else 0
1515
+ )
1516
+ orm_station.modified = None if modified == 'now' else modified
1517
+
1518
+ if commit:
1519
+ session.commit()
1520
+ else:
1521
+ session.flush()
1522
+
1395
1523
  self.tdenv.NOTE(
1396
1524
  "{} (#{}) updated in {}: {}",
1397
1525
  station.name(), station.ID,
1398
1526
  self.dbPath if self.tdenv.detail > 1 else "local db",
1399
1527
  ", ".join(changes)
1400
1528
  )
1401
-
1529
+
1402
1530
  return True
1403
1531
 
1404
1532
  def removeLocalStation(self, station, commit=True):
1405
1533
  """
1406
- Removes a station from the local database and memory image.
1407
-
1408
- Becareful of any references to the station you may still have
1409
- after this.
1534
+ Remove a station from the local database and memory image using SQLAlchemy.
1535
+ Be careful of any references to the station you may still have after this.
1410
1536
  """
1411
-
1412
- # Remove reference from my system
1537
+ # Remove reference from parent system (wrapper-level)
1413
1538
  system = station.system
1414
- system.stations.remove(station)
1415
-
1416
- # Remove the ID lookup
1417
- del self.stationByID[station.ID]
1418
-
1419
- # Delete database entry
1420
- db = self.getDB()
1421
- db.execute("""
1422
- DELETE FROM Station
1423
- WHERE system_id = ? AND station_id = ?
1424
- """, [system.ID, station.ID]
1425
- )
1426
- if commit:
1427
- db.commit()
1428
-
1539
+ if station in system.stations:
1540
+ system.stations.remove(station)
1541
+
1542
+ # Remove from ID lookup cache
1543
+ if station.ID in self.stationByID:
1544
+ del self.stationByID[station.ID]
1545
+
1546
+ # Delete from DB
1547
+ with self.Session() as session:
1548
+ orm_station = session.get(SA_Station, station.ID)
1549
+ if orm_station:
1550
+ session.delete(orm_station)
1551
+ if commit:
1552
+ session.commit()
1553
+ else:
1554
+ session.flush()
1555
+
1429
1556
  self.tdenv.NOTE(
1430
1557
  "{} (#{}) deleted from {}",
1431
1558
  station.name(), station.ID,
1432
1559
  self.dbPath if self.tdenv.detail > 1 else "local db",
1433
1560
  )
1434
-
1435
- station.dbname = "DELETED "+station.dbname
1561
+
1562
+ station.dbname = "DELETED " + station.dbname
1436
1563
  del station
1437
1564
 
1438
1565
  def lookupPlace(self, name):
@@ -1771,19 +1898,22 @@ class TradeDB:
1771
1898
 
1772
1899
  def _loadShips(self):
1773
1900
  """
1774
- Populate the Ship list.
1901
+ Populate the Ship list using SQLAlchemy.
1775
1902
  CAUTION: Will orphan previously loaded objects.
1776
1903
  """
1777
- stmt = """
1778
- SELECT ship_id, name, cost
1779
- FROM Ship
1780
- """
1781
- self.shipByID = {
1782
- row[0]: Ship(*row, stations=[])
1783
- for row in self.query(stmt)
1784
- }
1785
-
1904
+ with self.Session() as session:
1905
+ rows = session.query(
1906
+ SA_Ship.ship_id,
1907
+ SA_Ship.name,
1908
+ SA_Ship.cost,
1909
+ )
1910
+ self.shipByID = {
1911
+ row.ship_id: Ship(row.ship_id, row.name, row.cost, stations=[])
1912
+ for row in rows
1913
+ }
1914
+
1786
1915
  self.tdenv.DEBUG1("Loaded {} Ships", len(self.shipByID))
1916
+
1787
1917
 
1788
1918
  def lookupShip(self, name):
1789
1919
  """
@@ -1806,17 +1936,17 @@ class TradeDB:
1806
1936
 
1807
1937
  def _loadCategories(self):
1808
1938
  """
1809
- Populate the list of item categories.
1939
+ Populate the list of item categories using SQLAlchemy.
1810
1940
  CAUTION: Will orphan previously loaded objects.
1811
1941
  """
1812
- stmt = """
1813
- SELECT category_id, name
1814
- FROM Category
1815
- """
1816
- with closing(self.query(stmt)) as cur:
1942
+ with self.Session() as session:
1943
+ rows = session.query(
1944
+ SA_Category.category_id,
1945
+ SA_Category.name,
1946
+ )
1817
1947
  self.categoryByID = {
1818
- ID: Category(ID, name, [])
1819
- for (ID, name) in cur
1948
+ row.category_id: Category(row.category_id, row.name, [])
1949
+ for row in rows
1820
1950
  }
1821
1951
 
1822
1952
  self.tdenv.DEBUG1("Loaded {} Categories", len(self.categoryByID))
@@ -1837,16 +1967,19 @@ class TradeDB:
1837
1967
 
1838
1968
  def _loadItems(self):
1839
1969
  """
1840
- Populate the Item list.
1970
+ Populate the Item list using SQLAlchemy.
1841
1971
  CAUTION: Will orphan previously loaded objects.
1842
1972
  """
1843
- stmt = """
1844
- SELECT item_id, name, category_id, avg_price, fdev_id
1845
- FROM Item
1846
- """
1847
1973
  itemByID, itemByName, itemByFDevID = {}, {}, {}
1848
- with closing(self.query(stmt)) as cur:
1849
- for ID, name, categoryID, avgPrice, fdevID in cur:
1974
+ with self.Session() as session:
1975
+ rows = session.query(
1976
+ SA_Item.item_id,
1977
+ SA_Item.name,
1978
+ SA_Item.category_id,
1979
+ SA_Item.avg_price,
1980
+ SA_Item.fdev_id,
1981
+ )
1982
+ for ID, name, categoryID, avgPrice, fdevID in rows:
1850
1983
  category = self.categoryByID[categoryID]
1851
1984
  item = Item(
1852
1985
  ID, name, category,
@@ -1857,17 +1990,13 @@ class TradeDB:
1857
1990
  itemByName[name] = item
1858
1991
  if fdevID:
1859
1992
  itemByFDevID[fdevID] = item
1860
-
1861
1993
  category.items.append(item)
1862
-
1994
+
1863
1995
  self.itemByID = itemByID
1864
1996
  self.itemByName = itemByName
1865
1997
  self.itemByFDevID = itemByFDevID
1866
-
1867
- self.tdenv.DEBUG1(
1868
- "Loaded {:n} Items",
1869
- len(self.itemByID)
1870
- )
1998
+
1999
+ self.tdenv.DEBUG1("Loaded {:n} Items", len(self.itemByID))
1871
2000
 
1872
2001
  def lookupItem(self, name):
1873
2002
  """
@@ -1881,87 +2010,107 @@ class TradeDB:
1881
2010
 
1882
2011
  def getAverageSelling(self):
1883
2012
  """
1884
- Query the database for average selling prices of all items.
2013
+ Query the database for average selling prices of all items using SQLAlchemy.
1885
2014
  """
1886
2015
  if not self.avgSelling:
1887
2016
  self.avgSelling = {itemID: 0 for itemID in self.itemByID}
1888
- self.avgSelling.update({
1889
- ID: int(cr)
1890
- for ID, cr in self.getDB().execute("""
1891
- SELECT i.item_id, IFNULL(AVG(supply_price), 0)
1892
- FROM Item AS i
1893
- LEFT OUTER JOIN StationItem AS si ON (
1894
- i.item_id = si.item_id AND si.supply_price > 0
1895
- )
1896
- WHERE supply_price > 0
1897
- GROUP BY 1
1898
- """)
1899
- })
2017
+
2018
+ with self.Session() as session:
2019
+ rows = (
2020
+ session.query(
2021
+ SA_Item.item_id,
2022
+ func.ifnull(func.avg(SA_StationItem.supply_price), 0),
2023
+ )
2024
+ .outerjoin(
2025
+ SA_StationItem,
2026
+ (SA_Item.item_id == SA_StationItem.item_id) &
2027
+ (SA_StationItem.supply_price > 0),
2028
+ )
2029
+ .filter(SA_StationItem.supply_price > 0)
2030
+ .group_by(SA_Item.item_id)
2031
+ )
2032
+ for ID, cr in rows:
2033
+ self.avgSelling[ID] = int(cr)
2034
+
1900
2035
  return self.avgSelling
1901
-
2036
+
1902
2037
  def getAverageBuying(self):
1903
2038
  """
1904
- Query the database for average buying prices of all items.
2039
+ Query the database for average buying prices of all items using SQLAlchemy.
1905
2040
  """
1906
2041
  if not self.avgBuying:
1907
2042
  self.avgBuying = {itemID: 0 for itemID in self.itemByID}
1908
- self.avgBuying.update({
1909
- ID: int(cr)
1910
- for ID, cr in self.getDB().execute("""
1911
- SELECT i.item_id, IFNULL(AVG(demand_price), 0)
1912
- FROM Item AS i
1913
- LEFT OUTER JOIN StationItem AS si ON (
1914
- i.item_id = si.item_id AND si.demand_price > 0
1915
- )
1916
- WHERE demand_price > 0
1917
- GROUP BY 1
1918
- """)
1919
- })
2043
+
2044
+ with self.Session() as session:
2045
+ rows = (
2046
+ session.query(
2047
+ SA_Item.item_id,
2048
+ func.ifnull(func.avg(SA_StationItem.demand_price), 0),
2049
+ )
2050
+ .outerjoin(
2051
+ SA_StationItem,
2052
+ (SA_Item.item_id == SA_StationItem.item_id) &
2053
+ (SA_StationItem.demand_price > 0),
2054
+ )
2055
+ .filter(SA_StationItem.demand_price > 0)
2056
+ .group_by(SA_Item.item_id)
2057
+ )
2058
+ for ID, cr in rows:
2059
+ self.avgBuying[ID] = int(cr)
2060
+
1920
2061
  return self.avgBuying
2062
+
1921
2063
 
1922
2064
  ############################################################
1923
2065
  # Rare Items
1924
2066
 
1925
2067
  def _loadRareItems(self):
1926
2068
  """
1927
- Populate the RareItem list.
2069
+ Populate the RareItem list using SQLAlchemy.
1928
2070
  """
1929
- stmt = """
1930
- SELECT rare_id, station_id, category_id, name,
1931
- cost, max_allocation, illegal, suppressed
1932
- FROM RareItem
1933
- """
1934
-
1935
-
1936
2071
  rareItemByID, rareItemByName = {}, {}
1937
2072
  stationByID = self.stationByID
1938
- with closing(self.query(stmt)) as cur:
2073
+
2074
+ with self.Session() as session:
2075
+ rows = session.query(
2076
+ SA_RareItem.rare_id,
2077
+ SA_RareItem.station_id,
2078
+ SA_RareItem.category_id,
2079
+ SA_RareItem.name,
2080
+ SA_RareItem.cost,
2081
+ SA_RareItem.max_allocation,
2082
+ SA_RareItem.illegal,
2083
+ SA_RareItem.suppressed,
2084
+ )
1939
2085
  for (
1940
2086
  ID, stnID, catID, name,
1941
2087
  cost, maxAlloc, illegal, suppressed
1942
- ) in cur:
1943
- station = stationByID[stnID]
2088
+ ) in rows:
2089
+ station = stationByID[stnID]
1944
2090
  category = self.categoryByID[catID]
1945
2091
  rare = RareItem(
1946
- ID, station, name, cost, maxAlloc, illegal, suppressed,
2092
+ ID, station, name,
2093
+ cost, maxAlloc, illegal, suppressed,
1947
2094
  category, f"{category.dbname}/{name}"
1948
2095
  )
1949
- rareItemByID[ID] = rareItemByName[name] = rare
1950
- self.rareItemByID = rareItemByID
2096
+ rareItemByID[ID] = rare
2097
+ rareItemByName[name] = rare
2098
+
2099
+ self.rareItemByID = rareItemByID
1951
2100
  self.rareItemByName = rareItemByName
1952
-
1953
- self.tdenv.DEBUG1(
1954
- "Loaded {:n} RareItems",
1955
- len(rareItemByID)
1956
- )
2101
+
2102
+ self.tdenv.DEBUG1("Loaded {:n} RareItems", len(rareItemByID))
2103
+
1957
2104
 
1958
2105
  ############################################################
1959
2106
  # Price data.
1960
2107
 
1961
2108
  def close(self):
1962
- if self.conn:
1963
- self.conn.close()
1964
- self.conn = None
2109
+ if self.engine:
2110
+ self.engine.dispose()
2111
+ # Keep engine + Session references so reloadCache/buildCache can reuse them
2112
+
2113
+
1965
2114
 
1966
2115
  def load(self, maxSystemLinkLy=None):
1967
2116
  """