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,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
+ Deadlock-safety requirement:
132
+ - Do NOT release the advisory lock before the station's writes are COMMITTED.
133
+ - Previously we only committed when this helper created the transaction.
134
+ If the Session already had an active transaction (SQLAlchemy autobegin),
135
+ the lock could be released while row locks were still pending commit.
136
+
137
+ Behaviour:
138
+ - On MySQL/MariaDB: tries GET_LOCK() with bounded retries + exponential backoff.
139
+ - If acquired (got=True): COMMIT on normal exit BEFORE releasing the advisory lock,
140
+ regardless of whether this helper started the transaction.
141
+ - If NOT acquired (got=False) and this helper started the transaction: ROLLBACK to
142
+ avoid leaving an idle open transaction pinned to a connection.
143
+ - If an exception escapes the caller's block: ROLLBACK (best-effort) then re-raise.
144
+ - On unsupported dialects (e.g. SQLite): yields True and does nothing.
145
+
146
+ WARNING:
147
+ - Do not wrap this context manager inside an external transaction manager
148
+ (e.g. `with session.begin():`) because it may COMMIT inside that scope.
149
+ """
150
+ # Fast-path NO-OP for SQLite/unsupported dialects
151
+ if not _is_lock_supported(session):
152
+ yield True
153
+ return
154
+
155
+ # Prefer READ COMMITTED to reduce lock contention (best-effort).
156
+ _ensure_read_committed(session)
157
+
158
+ started_txn = False
159
+ txn_ctx = None
160
+ if not session.in_transaction():
161
+ # Pin lock + DML to the same connection by opening a txn.
162
+ txn_ctx = session.begin()
163
+ started_txn = True
164
+
165
+ got = False
166
+ try:
167
+ attempt = 0
168
+ while attempt < max_retries:
169
+ if acquire_station_lock(session, station_id, timeout_seconds):
170
+ got = True
171
+ break
172
+ time.sleep(backoff_start_seconds * (2 ** attempt))
173
+ attempt += 1
174
+
175
+ # Hand control to caller
176
+ yield got
177
+
178
+ if got:
179
+ # Commit while the advisory lock is still held.
180
+ if session.in_transaction():
181
+ session.commit()
182
+ else:
183
+ # If we opened a txn just to attempt locking, close it out cleanly.
184
+ if started_txn and session.in_transaction():
185
+ session.rollback()
186
+
187
+ except Exception:
188
+ # Ensure we don't leak row locks / open txn on error.
189
+ if session.in_transaction():
190
+ try:
191
+ session.rollback()
192
+ except Exception:
193
+ pass
194
+ raise
195
+
196
+ finally:
197
+ # Release advisory lock after commit/rollback decisions above.
198
+ if got:
199
+ try:
200
+ release_station_lock(session, station_id)
201
+ except Exception:
202
+ pass
203
+
204
+ if started_txn and txn_ctx is not None:
205
+ try:
206
+ txn_ctx.close()
207
+ except Exception:
208
+ pass