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,313 @@
1
+ # The runtime environment TD tools are expected to run with is encapsulated
2
+ # into a single object, the TradeEnv. See TradeEnv docstring for more.
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ import os
7
+ import sys
8
+ import traceback
9
+ import typing
10
+
11
+ # Import some utilities from the 'rich' library that provide ways to colorize and animate
12
+ # the console output, along with other useful features.
13
+ # If the user has 'EXCEPTIONS' defined to something in the environment, then we can
14
+ # immediately benefit from beautified stacktraces.
15
+ from rich.console import Console
16
+ from rich.traceback import install as install_rich_traces
17
+
18
+
19
+ if typing.TYPE_CHECKING:
20
+ import argparse
21
+ from typing import Any
22
+
23
+
24
+ _ROOT = os.path.abspath(os.path.dirname(__file__))
25
+
26
+
27
+ # Create a single instance of the console for everyone to use, unless they really
28
+ # want to do something unusual.
29
+ CONSOLE = Console()
30
+ STDERR = Console(stderr=True)
31
+
32
+ if os.getenv("EXCEPTIONS"):
33
+ # This makes call stacks show additional context and do syntax highlighting
34
+ # that can turn reading a callstack from hours into seconds.
35
+ install_rich_traces(console=STDERR, show_locals=False, extra_lines=1)
36
+
37
+
38
+ class BaseColorTheme:
39
+ """ A way to theme the console output colors. The default is none. """
40
+ CLOSE: str = "" # code to stop the last color
41
+ dim: str = "" # code to make text dim
42
+ bold: str = "" # code to make text bold
43
+ italic: str = "" # code to make text italic
44
+ # blink: NEVER = "don't you dare"
45
+
46
+ # style, label
47
+ debug, DEBUG = dim, "#"
48
+ note, NOTE = bold, "NOTE"
49
+ info, INFO = "", "INFO"
50
+ warn, WARN = "", "WARNING"
51
+
52
+ seq_first: str = "" # the first item in a sequence
53
+ seq_last: str = "" # the last item in a sequence
54
+
55
+ # Included as examples of how you might use this to manipulate tradecal output.
56
+ itm_units: str = "" # the amount of something
57
+ itm_name: str = "" # name of that unit
58
+ itm_price: str = "" # how much does it cost?
59
+
60
+ def render(self, renderable: Any, style: str) -> str: # pragma: no cover, pylint: disable=unused-argument
61
+ """ Renders the given printable item with the given style; BaseColorTheme simply uses a string transformation. """
62
+ if isinstance(renderable, str):
63
+ return renderable # avoid an allocation
64
+ return str(renderable)
65
+
66
+
67
+ class BasicRichColorTheme(BaseColorTheme):
68
+ """ Provide's 'rich' styling without our own colorization. """
69
+ CLOSE = "[/]"
70
+ bold = "[bold]"
71
+ dim = "[dim]"
72
+ italic = "[italic]"
73
+
74
+ # style, label
75
+ debug, DEBUG = dim, "#"
76
+ note, NOTE = bold, "NOTE"
77
+ info, INFO = "", "INFO"
78
+ warn, WARN = "[orange3]", "WARNING"
79
+
80
+ def render(self, renderable: Any, style: str) -> str: # pragma: no cover
81
+ style_attr = getattr(self, style, "")
82
+ if not style_attr:
83
+ return renderable if isinstance(renderable, str) else str(renderable)
84
+ return f"{style_attr}{renderable}{self.CLOSE}"
85
+
86
+
87
+ class RichColorTheme(BasicRichColorTheme):
88
+ """ Demonstrates how you might augment the rich theme with colors to be used fin e.g tradecal. """
89
+ DEBUG = ":spider_web:"
90
+ NOTE = ":information_source:"
91
+ WARN = ":warning:"
92
+ INFO = ":gear:"
93
+
94
+ # e.g. First station
95
+ seq_first = "[cyan]"
96
+ # e.g. Last station
97
+ seq_last = "[blue]"
98
+
99
+ # Included as examples of how you might use this to manipulate tradecal output.
100
+ itm_units = "[yellow3]"
101
+ itm_name = "[yellow]"
102
+ itm_price = "[bold]"
103
+
104
+
105
+ class BaseConsoleIOMixin:
106
+ """ Base mixin for running output through rich. """
107
+ color: bool
108
+ console: Console
109
+ debug: int
110
+ detail: int
111
+ encoding: str
112
+ quiet: int
113
+ stderr: Console
114
+ theme: BaseColorTheme
115
+
116
+ def uprint(self, *args: Any, stderr: bool = False, style: str | None = None, **kwargs: Any) -> None:
117
+ """
118
+ unicode-safe print via console or stderr, with 'rich' markup handling.
119
+ """
120
+ console = self.stderr if stderr else self.console
121
+ console.print(*args, style=style, **kwargs)
122
+
123
+
124
+ class NonUtf8ConsoleIOMixin(BaseConsoleIOMixin):
125
+ """ Mixing for running output through rich with UTF8-translation smoothing. """
126
+ def uprint(self, *args: Any, stderr: bool = False, style: str | None = None, **kwargs: Any) -> None:
127
+ """ unicode-handling print: when the stdout stream is not utf-8 supporting,
128
+ we do a little extra io work to ensure users don't get confusing unicode
129
+ errors. When the output stream *is* utf-8.
130
+
131
+ :param stderr: report to stderr instead of stdout
132
+ :param style: specify a 'rich' console style to use when the stream supports it
133
+ """
134
+ console = self.stderr if stderr else self.console
135
+ try:
136
+ # Attempt to print; the 'file' argument isn't supported by rich, so we'll
137
+ # need to fall-back on old print when someone specifies it.
138
+ console.print(*args, style=style, **kwargs)
139
+
140
+ except UnicodeEncodeError as e:
141
+ # Characters in the output couldn't be translated to unicode.
142
+ if not self.quiet:
143
+ self.stderr.print(
144
+ f"{self.theme.WARN}{self.theme.bold}CAUTION: Your terminal/console couldn't handle some "
145
+ "text I tried to print."
146
+ )
147
+ if 'EXCEPTIONS' in os.environ:
148
+ traceback.print_exc()
149
+ else:
150
+ self.stderr.print(e)
151
+
152
+ # Try to translate each ary into a viable string using utf error-replacement.
153
+ components = [
154
+ str(arg)
155
+ .encode(TradeEnv.encoding, errors="replace")
156
+ .decode(TradeEnv.encoding)
157
+ for arg in args
158
+ ]
159
+ console.print(*components, style=style, **kwargs)
160
+
161
+
162
+ ENV_DEFAULTS: dict[str, Any] = {
163
+ 'debug': 0,
164
+ 'detail': 0,
165
+ 'quiet': 0,
166
+ 'color': False,
167
+ 'theme': BaseColorTheme(),
168
+ 'persist': bool(os.environ.get('TD_PERSIST', '1')), # Use the 'persistence' mechanimsm
169
+ 'dataDir': os.environ.get('TD_DATA') or os.path.join(os.getcwd(), 'data'),
170
+ 'csvDir': os.environ.get('TD_CSV') or os.environ.get('TD_DATA') or os.path.join(os.getcwd(), 'data'),
171
+ 'tmpDir': os.environ.get('TD_TMP') or os.path.join(os.getcwd(), 'tmp'),
172
+ 'templateDir': os.path.join(_ROOT, 'templates'),
173
+ 'cwDir': os.getcwd(),
174
+ 'console': CONSOLE,
175
+ 'stderr': STDERR,
176
+ 'maxSystemLinkLy': 64.0,
177
+ }
178
+
179
+
180
+ # If the console doesn't support UTF8, use the more-complicated implementation.
181
+ if str(sys.stdout.encoding).upper() != 'UTF-8':
182
+ Utf8SafeConsoleIOMixin = NonUtf8ConsoleIOMixin
183
+ else:
184
+ Utf8SafeConsoleIOMixin = BaseConsoleIOMixin
185
+
186
+
187
+ class TradeEnv(Utf8SafeConsoleIOMixin):
188
+ """
189
+ TradeDangerous provides a container for runtime configuration (cli flags, etc) and io operations to
190
+ enable normalization of things without having to pass huge sets of arguments. This includes things
191
+ like logging and reporting functionality.
192
+
193
+ To print debug lines, use DEBUG<N>, e.g. DEBUG0, which takes a format() string and parameters, e.g.
194
+ DEBUG1("hello, {world}{}", "!", world="world")
195
+
196
+ is similar to:
197
+ arg0, arg1 = "!", "world"
198
+ if tdenv.debug > 1:
199
+ tdenv.console.print("# hello, {arg1}{}".format(arg0=arg0, arg1=arg1))
200
+
201
+ Use "NOTE" to print remarks which can be disabled with -q.
202
+ """
203
+ csvDir: str
204
+ cwDir: str
205
+ dataDir: str
206
+ maxSystemLinkLy: float
207
+ persist: bool
208
+ templateDir: str
209
+ theme: BaseColorTheme
210
+ tmpDir: str
211
+
212
+ encoding = sys.stdout.encoding
213
+
214
+ def __init__(self, properties: dict[str, typing.Any] | argparse.Namespace | None = None, **kwargs: Any) -> None:
215
+ # Inject the defaults into ourselves in a dict-like way
216
+ self.__dict__.update(ENV_DEFAULTS)
217
+
218
+ # If properties is a namespace, extract the dictionary; otherwise use it as-is
219
+ if properties and hasattr(properties, '__dict__'): # which arparse.Namespace has
220
+ properties = properties.__dict__
221
+ # Merge into our dictionary
222
+ self.__dict__.update(properties or {})
223
+
224
+ # Merge the kwargs dictionary
225
+ self.__dict__.update(kwargs or {})
226
+
227
+ # When debugging has been enabled on startup, enable slightly more
228
+ # verbose rich backtraces.
229
+ if self.__dict__['debug']:
230
+ install_rich_traces(console=STDERR, show_locals=True, extra_lines=2)
231
+
232
+ self.theme = RichColorTheme() if self.__dict__['color'] else BasicRichColorTheme()
233
+
234
+ @staticmethod
235
+ def __disabled_uprint(*args: Any, **kwargs: Any) -> None:
236
+ pass
237
+
238
+ def __getattr__(self, key: str) -> Any:
239
+ """ Return the default for attributes we don't have """
240
+ # The first time the DEBUG attribute is referenced, register a method for it.
241
+ disabled: bool = False
242
+ theme_prefix: str | None = None
243
+ theme_label: str | None = None
244
+ match key:
245
+ case "WARN" if self.quiet > 1:
246
+ disabled = True
247
+ case "WARN":
248
+ theme_prefix, theme_label = self.theme.warn, self.theme.WARN
249
+ case "NOTE" | "INFO" if self.quiet:
250
+ disabled = True
251
+ case "NOTE":
252
+ theme_prefix, theme_label = self.theme.note, self.theme.NOTE
253
+ case "INFO":
254
+ theme_prefix, theme_label = self.theme.info, self.theme.INFO
255
+ case _ if key.startswith("DEBUG") and int(key[5:]) >= self.debug:
256
+ disabled = True
257
+ case _ if key.startswith("DEBUG"):
258
+ theme_prefix, theme_label = self.theme.debug, self.theme.DEBUG + key[5:]
259
+ case _:
260
+ pass
261
+
262
+ # If there's no function but there's a theme, create a function
263
+ if disabled:
264
+ setattr(self, key, self.__disabled_uprint)
265
+ return self.__disabled_uprint
266
+
267
+ if theme_prefix is not None:
268
+ def __log_helper(outText: str, *args: Any, stderr: bool = False, **kwargs: Any):
269
+ try:
270
+ msg = str(outText) if not (args or kwargs) else str(outText).format(*args, **kwargs)
271
+ except Exception: # noqa # pylint: disable=broad-except
272
+ # Fallback: dump raw message + args/kwargs repr
273
+ msg = f"{outText} {args!r} {kwargs!r}"
274
+
275
+ self.uprint(f"{theme_prefix}{theme_label}: {msg}", stderr=stderr)
276
+
277
+ setattr(self, key, __log_helper)
278
+ return __log_helper
279
+
280
+ return None
281
+
282
+ def remove_file(self, *args: str | Path) -> bool:
283
+ """ Unlinks a file, as long as it exists, and logs the action at level 1. """
284
+ path = Path(*args)
285
+ if not path.exists():
286
+ return False
287
+ path.unlink()
288
+ self.DEBUG1(":cross_mark: deleted {}", path)
289
+ return True
290
+
291
+ def rename_file(self, *, old: str | Path, new: str | Path) -> bool:
292
+ """
293
+ If 'new' exists, deletes it, and then attempts to rename old -> new. If new is not specified,
294
+ then '.old' is appended to the end of the old filename while retaining the original suffix.
295
+
296
+ :param old: The current path/name of the file.
297
+ :param new: The path/name to rename the file to and remove before attempting.
298
+ :returns: True if the file existed and was renamed.
299
+ """
300
+ # Promote new to a guaranteed Path and remove it if it's present.
301
+ new = Path(new)
302
+ self.remove_file(new)
303
+
304
+ # Promote new to a guaranteed Path and confirm it exists.
305
+ old = Path(old)
306
+ if not old.exists():
307
+ return False
308
+
309
+ # Perform the rename and log it at level 1.
310
+ old.rename(new)
311
+ self.DEBUG1(":recycle: moved {} to {}", old, new)
312
+
313
+ return True
@@ -0,0 +1,109 @@
1
+ # pylint: disable=multiple-statements-on-one-line
2
+ # flake8: noqa: E704
3
+ #
4
+ # Because TradeEnv on-the-fly constructs the various logging methods
5
+ # etc, most IDEs and linters struggle with the types of many of its
6
+ # methods and built-in properties.
7
+ #
8
+ # This is a python header unit that forward declares stuff so that
9
+ # the IDEs are less of a wall of squiggles.
10
+
11
+ from pathlib import Path
12
+ from typing import Any
13
+ from rich.console import Console
14
+ import argparse
15
+
16
+
17
+ class BaseColorTheme:
18
+ CLOSE: str
19
+ dim: str
20
+ bold: str
21
+ italic: str
22
+ debug: str
23
+ DEBUG: str
24
+ note: str
25
+ NOTE: str
26
+ info: str
27
+ INFO: str
28
+ warn: str
29
+ WARN: str
30
+ seq_first: str
31
+ seq_last: str
32
+ itm_units: str
33
+ itm_name: str
34
+ itm_price: str
35
+ def render(self, renderable: Any, style: str) -> str: ...
36
+
37
+
38
+ class BasicRichColorTheme(BaseColorTheme):
39
+ CLOSE: str
40
+ bold: str
41
+ dim: str
42
+ italic: str
43
+ debug: str
44
+ DEBUG: str
45
+ note: str
46
+ NOTE: str
47
+ info: str
48
+ INFO: str
49
+ warn: str
50
+ WARN: str
51
+ def render(self, renderable: Any, style: str) -> str: ...
52
+
53
+
54
+ class RichColorTheme(BasicRichColorTheme):
55
+ DEBUG: str
56
+ NOTE: str
57
+ WARN: str
58
+ INFO: str
59
+ seq_first: str
60
+ seq_last: str
61
+ itm_units: str
62
+ itm_name: str
63
+ itm_price: str
64
+
65
+
66
+ class BaseConsoleIOMixin:
67
+ color: bool
68
+ console: Console
69
+ debug: int
70
+ detail: int
71
+ encoding: str
72
+ quiet: int
73
+ stderr: Console
74
+ theme: BaseColorTheme
75
+
76
+ def uprint(self, *args: Any, stderr: bool = False, style: str | None = None, **kwargs: Any) -> None: ...
77
+
78
+
79
+ class NonUtf8ConsoleIOMixin(BaseConsoleIOMixin):
80
+ def uprint(self, *args: Any, stderr: bool = False, style: str | None = None, **kwargs: Any) -> None: ...
81
+
82
+
83
+ class TradeEnv(BaseConsoleIOMixin):
84
+ csvDir: str
85
+ cwDir: str
86
+ dataDir: str
87
+ maxSystemLinkLy: float
88
+ persist: bool
89
+ templateDir: str
90
+ theme: BaseColorTheme
91
+ tmpDir: str
92
+
93
+ def __init__(self, properties: dict[str, Any] | argparse.Namespace | None = None, **kwargs: Any) -> None: ...
94
+
95
+ # Dynamically-generated log methods with full type hints
96
+ def DEBUG0(self, outText: str, *args: Any, stderr: bool = False, **kwargs: Any) -> None: ...
97
+ def DEBUG1(self, outText: str, *args: Any, stderr: bool = False, **kwargs: Any) -> None: ...
98
+ def DEBUG2(self, outText: str, *args: Any, stderr: bool = False, **kwargs: Any) -> None: ...
99
+ def DEBUG3(self, outText: str, *args: Any, stderr: bool = False, **kwargs: Any) -> None: ...
100
+ def DEBUG4(self, outText: str, *args: Any, stderr: bool = False, **kwargs: Any) -> None: ...
101
+ def INFO(self, outText: str, *args: Any, stderr: bool = False, **kwargs: Any) -> None: ...
102
+ def NOTE(self, outText: str, *args: Any, stderr: bool = False, **kwargs: Any) -> None: ...
103
+ def WARN(self, outText: str, *args: Any, stderr: bool = False, **kwargs: Any) -> None: ...
104
+
105
+ # File operations
106
+ def remove_file(self, *args: str | Path) -> bool: ...
107
+ def rename_file(self, *, old: str | Path, new: str | Path) -> bool: ...
108
+
109
+ def uprint(self, *args: Any, stderr: bool = False, style: str | None = None, **kwargs: Any) -> None: ...
@@ -0,0 +1,131 @@
1
+ """
2
+ tradeexcept defines standard exceptions used within TradeDangerous.
3
+ """
4
+ from __future__ import annotations
5
+ import typing
6
+
7
+ if typing.TYPE_CHECKING:
8
+ try:
9
+ from collections.abc import Callable
10
+ except ImportError:
11
+ from typing import Callable
12
+ from typing import Any
13
+ from pathlib import Path
14
+
15
+
16
+ AMBIGUITY_LIMIT = 6
17
+
18
+
19
+ class SimpleAbort(Exception):
20
+ """
21
+ SimpleAbort is Exception but can be caught and presented without
22
+ any kind of backtrace.
23
+ """
24
+
25
+
26
+ class TradeException(Exception):
27
+ """
28
+ Distinguishes runtime logical errors (such as no data for what you
29
+ queried) from programmatic errors (such as Oliver accessing a hash
30
+ with the wrong type of key).
31
+
32
+ TradeExcepts should be caught by the program and displayed in the
33
+ most user friendly way possible.
34
+ """
35
+ pass
36
+
37
+
38
+ class MissingDB(TradeException):
39
+ """
40
+ Reports that the database is missing in a scenario where it is
41
+ required and not default-created for the user.
42
+
43
+ Ideally, this should describe to the user how to create the
44
+ database, perhaps through a "bootstrap" subcommand.
45
+ """
46
+ def __init__(self, dbpath: str | Path):
47
+ super().__init__(
48
+ f"{dbpath}: Data file(s) are missing, you must initialize the database first. "
49
+ "Consider using `trade import -P eddblink -O bootstrap` or if you are "
50
+ "managing data by hand, use the buildcache subcommand."
51
+ )
52
+
53
+ class AmbiguityError(TradeException):
54
+ """
55
+ Raised when a search key could match multiple entities.
56
+ Attributes:
57
+ lookupType - description of what was being queried,
58
+ searchKey - the key given to the search routine,
59
+ anyMatch - list of items which were found to match, if any
60
+ key - retrieve the display string for a candidate
61
+ """
62
+ def __init__(
63
+ self,
64
+ lookupType: str,
65
+ searchKey: str,
66
+ anyMatch: list[Any],
67
+ key: Callable[[Any], str] = lambda item: item
68
+ ) -> None:
69
+ self.lookupType = lookupType
70
+ self.searchKey = searchKey
71
+ self.anyMatch = anyMatch
72
+ self.key = key
73
+
74
+ def __str__(self) -> str:
75
+ anyMatch, key = self.anyMatch, self.key
76
+
77
+ # ------------------------------------------------------------------
78
+ # Special-case: system name collisions where we passed in
79
+ # (index, System) pairs from TradeDB.lookupSystem.
80
+ # ------------------------------------------------------------------
81
+ if (
82
+ self.lookupType == "System"
83
+ and anyMatch
84
+ and isinstance(anyMatch[0], tuple)
85
+ and len(anyMatch[0]) >= 2
86
+ ):
87
+ lines = [
88
+ f'System name "{self.searchKey}" refers to more than one distinct system.',
89
+ "",
90
+ 'Select the one you intended using "@N":',
91
+ "",
92
+ ]
93
+ for index, system in anyMatch:
94
+ # Be tolerant in case the contents are not exactly (int, System)
95
+ try:
96
+ name = system.dbname
97
+ x, y, z = system.posX, system.posY, system.posZ
98
+ lines.append(
99
+ f" {name}@{index} 45 ({x:.1f}, {y:.1f}, {z:.1f})"
100
+ )
101
+ except Exception:
102
+ # Fallback to the provided key() formatter
103
+ lines.append(f" {key((index, system))}")
104
+ lines.append("")
105
+ lines.append("(Index numbers are ordered by Galactic X coordinate.)")
106
+ return "\n".join(lines)
107
+
108
+ # ------------------------------------------------------------------
109
+ # Generic ambiguity formatting used everywhere else
110
+ # ------------------------------------------------------------------
111
+ if not anyMatch:
112
+ # Not matching anything is not "ambiguous".
113
+ raise RuntimeError('called AmbiguityError with no matches')
114
+
115
+ # Truncate the list of candidates so we don't show more than 10
116
+ candidates = [key(c) for c in anyMatch[:AMBIGUITY_LIMIT]]
117
+ if len(anyMatch) < 3:
118
+ opportunities = " or ".join(candidates)
119
+ else:
120
+ if len(anyMatch) > AMBIGUITY_LIMIT:
121
+ candidates[-1] = "..."
122
+ else:
123
+ candidates[-1] = "or " + candidates[-1] # oxford comma
124
+ opportunities = ", ".join(candidates)
125
+
126
+ return f'{self.lookupType} "{self.searchKey}" could match {opportunities}'
127
+
128
+
129
+ class SystemNotStationError(TradeException):
130
+ """ Raised when a station lookup matched a System but
131
+ could not be automatically reduced to a Station. """