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,179 @@
1
+ #! /usr/bin/env python
2
+
3
+ # Experimental module to generate a JSON version of the .prices file.
4
+
5
+ import sqlite3
6
+ import json
7
+ import time
8
+ import collections
9
+ import os
10
+
11
+
12
+ # Set to True to allow export of systems that don't have any station data
13
+ emptySystems = True
14
+ # Set to True to allow export of stations that don't have prices
15
+ emptyStations = True
16
+
17
+ conn = sqlite3.connect("data/TradeDangerous.db")
18
+
19
+
20
+ def collectItemData(db):
21
+ """
22
+ Builds a flat, array of item names that serves as a table of items.
23
+ Station Price Data refers to this table by position in the array.
24
+ As a result, we also need a mapping for itemID -> tableID
25
+ """
26
+
27
+ items = [] # table of items
28
+ itemIdx = {} # mapping from itemID -> position in items
29
+ for ID, name in db.execute("SELECT i.item_id, i.name FROM Item AS i"):
30
+ itemIdx[ID] = len(items)
31
+ items.append(name)
32
+
33
+ return items, itemIdx
34
+
35
+
36
+ def collectSystems(
37
+ db,
38
+ itemIdx,
39
+ withEmptySystems=False,
40
+ withStations=True,
41
+ withEmptyStations=False,
42
+ withPrices=True
43
+ ):
44
+ """
45
+ Build the System -> Station -> Price data from the supplied DB.
46
+
47
+ withEmptySystems:
48
+ True: include systems with no station data.
49
+
50
+ withStations:
51
+ False: (implies withEmptySystems=True) don't include station data.
52
+
53
+ withEmptyStations:
54
+ True: include stations with no price data.
55
+
56
+ withPrices:
57
+ False: (implies withEmptyStations=True) don't include price data.
58
+ """
59
+
60
+ systems = collections.defaultdict(dict)
61
+ if not withStations:
62
+ withEmptySystems = True
63
+
64
+ for sysID, sys, posX, posY, posZ in db.execute("""
65
+ SELECT sys.system_id, sys.name,
66
+ sys.pos_x, sys.pos_y, sys.pos_z
67
+ FROM System AS sys
68
+ LEFT OUTER JOIN Station
69
+ USING (system_id)
70
+ GROUP BY 1
71
+ """):
72
+ systemData = {
73
+ 'pos': [ posX, posY, posZ ],
74
+ }
75
+ if withStations:
76
+ stations = collectStations(
77
+ db, itemIdx,
78
+ sysID,
79
+ withEmptyStations=withEmptyStations,
80
+ withPrices=withPrices,
81
+ )
82
+ if not stations and not withEmptySystems:
83
+ continue
84
+ if stations:
85
+ systemData['stn'] = stations
86
+ systems[sys] = systemData
87
+
88
+ return systems
89
+
90
+
91
+ def collectStations(
92
+ db,
93
+ itemIdx,
94
+ sysID,
95
+ withEmptyStations=False,
96
+ withPrices=True
97
+ ):
98
+ """
99
+ Populate a station list for a given system, including price data.
100
+ """
101
+ stations = {}
102
+ if not withPrices:
103
+ withEmptyStations = True
104
+ for stnID, name, lsFromStar, lastMod in db.execute("""
105
+ SELECT stn.station_id, stn.name, stn.ls_from_star,
106
+ MAX(si.modified)
107
+ FROM Station AS stn
108
+ LEFT OUTER JOIN StationItem si
109
+ USING (station_id)
110
+ WHERE stn.system_id = ?
111
+ GROUP BY 1
112
+ """, [sysID]):
113
+ if not lastMod and not withEmptyStations:
114
+ continue
115
+ stationData = {
116
+ 'ls': int(lsFromStar),
117
+ }
118
+ if lastMod:
119
+ stationData['ts'] = lastMod
120
+ if withPrices:
121
+ buy, sell = collectPriceData(
122
+ db, itemIdx,
123
+ stnID,
124
+ )
125
+ stationData['buy'] = buy
126
+ stationData['sell'] = sell
127
+
128
+ stations[name] = stationData
129
+
130
+ return stations
131
+
132
+
133
+ def collectPriceData(db, itemIdx, stnID):
134
+ """
135
+ Collect buying and selling data for a given station. Items reference the
136
+ position in the itemData array.
137
+ """
138
+
139
+ buying, selling = [], []
140
+
141
+ for itmID, cr in db.execute("""
142
+ SELECT sb.item_id, sb.price
143
+ FROM StationBuying AS sb
144
+ WHERE station_id = ?
145
+ """, [stnID]):
146
+ buying.append([ itemIdx[itmID], cr ])
147
+
148
+ for itmID, cr, units, level in db.execute("""
149
+ SELECT ss.item_id, ss.price, ss.units, ss.level
150
+ FROM StationSelling AS ss
151
+ WHERE station_id = ?
152
+ """, [stnID]):
153
+ selling.append([ itemIdx[itmID], cr, units, level ])
154
+
155
+ return buying, selling
156
+
157
+
158
+ itemData, itemIdx = collectItemData(conn)
159
+ sysData = collectSystems(
160
+ conn, itemIdx,
161
+ withEmptySystems=emptySystems, withEmptyStations=emptyStations
162
+ )
163
+ jsonData = {
164
+ 'src': 'td/json-exp',
165
+ 'time': int(time.time()),
166
+ 'items': itemData,
167
+ 'emptySys': emptySystems,
168
+ 'emptyStn': emptyStations,
169
+ 'sys': sysData,
170
+ }
171
+
172
+ try:
173
+ cmdrName = os.environ['CMDR']
174
+ jsonData['cmdr'] = cmdrName
175
+ except KeyError:
176
+ pass
177
+
178
+ print(json.dumps(jsonData, separators=(',',':')))
179
+
@@ -0,0 +1,194 @@
1
+ from __future__ import annotations
2
+ from contextlib import contextmanager
3
+ import typing
4
+
5
+ from rich.progress import (
6
+ Progress as RichProgress,
7
+ TaskID,
8
+ ProgressColumn,
9
+ BarColumn, DownloadColumn, MofNCompleteColumn, SpinnerColumn,
10
+ TaskProgressColumn, TextColumn, TimeElapsedColumn, TimeRemainingColumn,
11
+ TransferSpeedColumn
12
+ )
13
+
14
+
15
+ if typing.TYPE_CHECKING:
16
+ from collections.abc import Iterable
17
+ from typing import Optional, Type # noqa
18
+
19
+
20
+ class BarStyle:
21
+ """ Base class for Progress bar style types. """
22
+ def __init__(self, width: int = 10, prefix: Optional[str] = None, *, add_columns: Optional[Iterable[ProgressColumn]]):
23
+ self.columns = [SpinnerColumn(), TextColumn("[progress.description]{task.description}"), BarColumn(bar_width=width)]
24
+ if add_columns:
25
+ self.columns.extend(add_columns)
26
+
27
+
28
+ class CountingBar(BarStyle):
29
+ """ Creates a progress bar that is counting M/N items to completion. """
30
+ def __init__(self, width: int = 10, prefix: Optional[str] = None):
31
+ my_columns = [MofNCompleteColumn(), TimeElapsedColumn()]
32
+ super().__init__(width, prefix, add_columns=my_columns)
33
+
34
+
35
+ class DefaultBar(BarStyle):
36
+ """ Creates a simple default progress bar with a percentage and time elapsed. """
37
+ def __init__(self, width: int = 10, prefix: Optional[str] = None):
38
+ my_columns = [TaskProgressColumn(), TimeElapsedColumn()]
39
+ super().__init__(width, prefix, add_columns=my_columns)
40
+
41
+
42
+ class LongRunningCountBar(BarStyle):
43
+ """ Creates a progress bar that is counting M/N items to completion with a time-remaining counter """
44
+ def __init__(self, width: int = 10, prefix: Optional[str] = None):
45
+ my_columns = [MofNCompleteColumn(), TimeElapsedColumn(), TimeRemainingColumn()]
46
+ super().__init__(width, prefix, add_columns=my_columns)
47
+
48
+
49
+ class ElapsedBar(BarStyle):
50
+ """ Creates a progress bar that is just showing something will take time. """
51
+ def __init__(self, width: int = 10, prefix: Optional[str] = None):
52
+ my_columns = [TimeElapsedColumn()]
53
+ super().__init__(width, prefix, add_columns=my_columns)
54
+
55
+
56
+ class TransferBar(BarStyle):
57
+ """ Creates a progress bar representing a data transfer, which shows the amount of
58
+ data transferred, speed, and estimated time remaining. """
59
+ def __init__(self, width: int = 16, prefix: Optional[str] = None):
60
+ my_columns = [DownloadColumn(), TransferSpeedColumn(), TimeRemainingColumn()]
61
+ super().__init__(width, prefix, add_columns=my_columns)
62
+
63
+
64
+ class Progress:
65
+ """
66
+ Facade around the rich Progress bar system to help transition away from
67
+ TD's original basic progress bar implementation.
68
+ """
69
+ def __init__(self,
70
+ max_value: Optional[float] = None,
71
+ width: Optional[int] = None,
72
+ start: float = 0,
73
+ prefix: Optional[str] = None,
74
+ label: Optional[str] = None,
75
+ *,
76
+ style: Optional[Type[BarStyle]] = None, # pylint:disable=deprecated-typing-alias
77
+ show: bool = True,
78
+ ) -> None:
79
+ """
80
+ :param max_value: Last value we can reach (100%).
81
+ :param width: How wide to make the bar itself.
82
+ :param start: Override initial value to non-zero.
83
+ :param prefix: Text to print between the spinner and the bar.
84
+ :param style: Bar-style factory to use for styling.
85
+ :param show: If False, disables the bar entirely.
86
+ """
87
+ self.show = bool(show)
88
+ if not show:
89
+ return
90
+
91
+ if style is None:
92
+ style = DefaultBar
93
+
94
+ self.max_value = 0 if max_value is None else max(max_value, start)
95
+ self.value = start
96
+ self.prefix = prefix or ""
97
+ self.label = label or "Working..."
98
+ self.width = width or 25
99
+ # The 'Progress' itself is a view for displaying the progress of tasks. So we construct it
100
+ # and then create a task for our job.
101
+ style_instance = style(width=self.width, prefix=self.prefix)
102
+ self.progress = RichProgress(
103
+ # What fields to display.
104
+ *style_instance.columns,
105
+ # Hide it once it's finished, update it for us, 4x a second
106
+ transient=True, auto_refresh=True, refresh_per_second=5
107
+ )
108
+
109
+ # Now we add an actual task to track progress on.
110
+ self.task = self.progress.add_task(self.label, total=max_value, start=True)
111
+ if self.value:
112
+ self.progress.update(self.task, advance=self.value)
113
+
114
+ # And show the task tracker.
115
+ self.progress.start()
116
+
117
+ def __enter__(self):
118
+ """ Context manager.
119
+
120
+ Example use:
121
+
122
+ import time
123
+ import tradedangerous.progress
124
+
125
+ # Progress(max_value=100, width=32, style=progress.CountingBar)
126
+ with progress.Progress(100, 32, style=progress.CountingBar) as prog:
127
+ for i in range(100):
128
+ prog.increment(1)
129
+ time.sleep(3)
130
+ """
131
+ return self
132
+
133
+ def __exit__(self, *args, **kwargs):
134
+ self.clear()
135
+
136
+ def increment(self, value: Optional[float] = None, description: Optional[str] = None, *, progress: Optional[float] = None) -> None:
137
+ """
138
+ Increase the progress of the bar by a given amount.
139
+
140
+ :param value: How much to increase the progress by.
141
+ :param description: If set, replaces the task description.
142
+ :param progress: Instead of increasing by value, set the absolute progress to this.
143
+ """
144
+ if not self.show:
145
+ return
146
+ if description:
147
+ self.prefix = description
148
+ self.progress.update(self.task, description=description, refresh=True)
149
+
150
+ bump = False
151
+ if not value and progress is not None and self.value != progress:
152
+ self.value = progress
153
+ bump = True
154
+ elif value:
155
+ self.value += value # Update our internal count
156
+ bump = True
157
+
158
+ if self.value >= self.max_value: # Did we go past the end? Increase the end.
159
+ self.max_value += value * 2
160
+ self.progress.update(self.task, description=self.prefix or self.label, total=self.max_value)
161
+ bump = True
162
+
163
+ if bump and self.max_value > 0:
164
+ self.progress.update(self.task, description=self.prefix or self.label, completed=self.value)
165
+
166
+ def clear(self) -> None:
167
+ """ Remove the current progress bar, if any. """
168
+ # These two shouldn't happen separately, but incase someone tinkers, test each
169
+ # separately and shut them down.
170
+ if not self.show:
171
+ return
172
+
173
+ if self.task:
174
+ self.progress.remove_task(self.task)
175
+ self.task = None
176
+
177
+ if self.progress:
178
+ self.progress.stop()
179
+ self.progress = None
180
+
181
+ @contextmanager
182
+ def sub_task(self, description: str, max_value: Optional[int] = None, width: int = 25):
183
+ if not self.show:
184
+ yield
185
+ return
186
+ task = self.progress.add_task(description, total=max_value, start=True, width=width)
187
+ try:
188
+ yield task
189
+ finally:
190
+ self.progress.remove_task(task)
191
+
192
+ def update_task(self, task: TaskID, advance: float | int, description: Optional[str] = None):
193
+ if self.show:
194
+ self.progress.update(task, advance=advance, description=description)
@@ -0,0 +1,249 @@
1
+ from __future__ import annotations
2
+ from textwrap import TextWrapper
3
+ import importlib
4
+ import typing
5
+
6
+
7
+ if typing.TYPE_CHECKING:
8
+ from tradedangerous import TradeDB, TradeEnv
9
+
10
+
11
+ __all__ = [
12
+ 'PluginException',
13
+ 'PluginBase',
14
+ 'ImportPluginBase',
15
+ 'load',
16
+ ]
17
+
18
+
19
+ class PluginException(Exception):
20
+ """
21
+ Base class for exceptions thrown by plugins.
22
+ """
23
+
24
+
25
+ class PluginBase:
26
+ """
27
+ Base class for plugin implementation.
28
+
29
+ To implement a plugin, create a file in the plugins directory
30
+ called "mypluginname_plug.py". This file should implement
31
+ plugin classes derived from the appropriate plugins base,
32
+ e.g. for an import pluggin:
33
+
34
+ import plugins
35
+
36
+ class ImportPlugin(plugins.ImportPluginBase):
37
+ # your implementation here
38
+ """
39
+
40
+ def __init__(self, tdb: TradeDB, tdenv: TradeEnv) -> None:
41
+ """
42
+ Parameters:
43
+ tdb
44
+ Instance of TradeDB
45
+ tdenv
46
+ Instance of TradeEnv
47
+ """
48
+ self.tdb = tdb
49
+ self.tdenv = tdenv
50
+ self.options: dict[str, typing.Any] = {}
51
+
52
+ pluginOptions: dict[str, typing.Any] = getattr(self, "pluginOptions", {})
53
+
54
+ for opt in tdenv.pluginOptions or {}:
55
+ equals = opt.find('=')
56
+ if equals < 0:
57
+ key, value = opt, True
58
+ else:
59
+ key, value = opt[:equals], opt[equals+1:]
60
+ keyName = key.lower()
61
+ if keyName not in pluginOptions:
62
+ if keyName == "help":
63
+ raise SystemExit(self.usage())
64
+
65
+ if not pluginOptions:
66
+ raise PluginException(
67
+ "This plugin does not support any options."
68
+ )
69
+
70
+ raise PluginException(
71
+ "Unknown plugin option: '{}'.\n"
72
+ "\n"
73
+ "Valid options for this plugin:\n"
74
+ " {}.\n"
75
+ "\n"
76
+ "Use '--opt=help' for details.\n"
77
+ .format(
78
+ opt,
79
+ ', '.join(
80
+ opt for opt in sorted(pluginOptions.keys())
81
+ )))
82
+ self.options[key.lower()] = value
83
+
84
+
85
+ def usage(self):
86
+ tw = TextWrapper(
87
+ width=78,
88
+ drop_whitespace=True,
89
+ expand_tabs=True,
90
+ fix_sentence_endings=True,
91
+ break_long_words=True,
92
+ break_on_hyphens=True,
93
+ )
94
+
95
+ assert self.__doc__, "Plugin class requires a docstring to define usage()"
96
+ text = tw.fill(self.__doc__.strip()) + "\n\n"
97
+
98
+ options = getattr(self, "pluginOptions", None)
99
+ if not options:
100
+ return text + "This plugin does not support any options.\n"
101
+
102
+ tw.subsequent_indent=' ' * 24
103
+ text += "Options supported by this plugin:\n"
104
+ for opt in sorted(options.keys()):
105
+ text += f"--opt={opt:<12} "
106
+ text += tw.fill(options[opt].strip()) + "\n"
107
+ text += "\n"
108
+ text += "You can also chain options together, e.g.:\n"
109
+ text += f" --opt={','.join(list(options.keys())[:3])}\n"
110
+
111
+ return text
112
+
113
+
114
+ def getOption(self, key: str) -> typing.Any:
115
+ """ Case-sensitive plugin-option lookup. """
116
+ return self.options.get(key.lower(), None)
117
+
118
+
119
+ def run(self) -> bool:
120
+ """
121
+ Plugin must implement: Execute the plugin's logic.
122
+
123
+ Called by the parent module before it does any validation
124
+ of arguments and before it does any processing work.
125
+
126
+ Return True to allow the calling module to continue working,
127
+ e.g. if your plugin simply changes the tdenv.
128
+
129
+ Return False if you have completed work and the calling
130
+ module can finish.
131
+ """
132
+ raise NotImplementedError
133
+
134
+
135
+ def finish(self) -> bool:
136
+ """
137
+ Plugin may need to implement: Called after all preparation
138
+ work has been done by the sub-command invoking the plugin.
139
+
140
+ Return False if you have completed all neccessary work.
141
+ Returning True will allow the sub-command to finish its
142
+ normal workflow after you return.
143
+ """
144
+ raise NotImplementedError
145
+
146
+
147
+ class ImportPluginBase(PluginBase):
148
+ """
149
+ Base class for import plugins.
150
+
151
+ Called by "import" as soon as argument parsing has been done.
152
+
153
+ An import plugin implements "run()" and "finish()".
154
+
155
+ On entry to the "import" function, before the database has
156
+ been loaded or the cache file generated, the plugin's
157
+ "run()" member is invoked.
158
+
159
+ This function can do as much or as little work as you need.
160
+
161
+ When you are done, return True to allow "import" to continue,
162
+ e.g. for example a plugin that simply sets the cmdenv.url to
163
+ a specific download would return True.
164
+
165
+ On the other hand, if you complete all the import work
166
+ relevant to your plugin, return None or False and the command
167
+ will early-out.
168
+
169
+ "finish()" will then be called before the import command
170
+ tries to import the data. This gives you an opportunity to
171
+ modify, parse or even import the data yourself.
172
+
173
+ Returning False from finish() ends processing, the import
174
+ command will rebuild the main .prices file and exit. This
175
+ allows you to write a plugin that does its own processing
176
+ of a custom .prices format, for example.
177
+
178
+ Returning True will allow the import command to proceed
179
+ with import as normal. This allows, for example, a plugin
180
+ that pre-processes the .prices file before import is called.
181
+ """
182
+
183
+ defaultImportFile = "import.prices"
184
+
185
+ def __init__(self, tdb: TradeDB, tdenv: TradeEnv) -> None:
186
+ """
187
+ Parameters:
188
+ tdb
189
+ Instance of TradeDB
190
+ tdenv
191
+ Instance of TradeEnv
192
+ """
193
+ super().__init__(tdb, tdenv)
194
+
195
+
196
+ def run(self) -> bool:
197
+ """
198
+ Plugin Must Implement:
199
+
200
+ Called immediately on entry to the import_cmd.run() function.
201
+ This means that you have no database access via the tdb object.
202
+
203
+ If you need to access data from the .db file, you should put
204
+ that code in the "finish()" object.
205
+
206
+ Returns:
207
+ True if you want the "import" command to continue (e.g.
208
+ to reach the call to "finish()",
209
+ False or None to early out after your return.
210
+ """
211
+ raise NotImplementedError
212
+
213
+
214
+ def finish(self) -> bool:
215
+ """
216
+ Plugin Must Implement:
217
+
218
+ Called after import has rebuilt the cache, loaded the DB data
219
+ into it's TradeDB instance, done any downloads and checked for
220
+ the presence of cmdenv.filename, but before it has tried to
221
+ import the .prices data.
222
+
223
+ Returns:
224
+ True if you want the "import" command to continue and
225
+ try to import the .prices data,
226
+ False or None to early out after your return.
227
+ """
228
+ raise NotImplementedError
229
+
230
+
231
+ def load(pluginName: str, typeName: str):
232
+ """
233
+ Attempt to load a plugin and find the specified plugin
234
+ class within it.
235
+ """
236
+
237
+ # Check if a file matching this name exists.
238
+ moduleName = f"{__name__}.{pluginName.lower()}_plug"
239
+ try:
240
+ importedModule = importlib.import_module(moduleName)
241
+ except ImportError as e:
242
+ raise PluginException(f"Unable to load plugin '{pluginName}': {e}") from e
243
+
244
+ pluginClass = getattr(importedModule, typeName, None)
245
+ if not pluginClass:
246
+ raise PluginException(f"{pluginName} plugin does not provide a {typeName}.")
247
+
248
+ return pluginClass
249
+