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.
- py.typed +1 -0
- trade.py +49 -0
- tradedangerous/__init__.py +43 -0
- tradedangerous/cache.py +1381 -0
- tradedangerous/cli.py +136 -0
- tradedangerous/commands/TEMPLATE.py +74 -0
- tradedangerous/commands/__init__.py +244 -0
- tradedangerous/commands/buildcache_cmd.py +102 -0
- tradedangerous/commands/buy_cmd.py +427 -0
- tradedangerous/commands/commandenv.py +372 -0
- tradedangerous/commands/exceptions.py +94 -0
- tradedangerous/commands/export_cmd.py +150 -0
- tradedangerous/commands/import_cmd.py +222 -0
- tradedangerous/commands/local_cmd.py +243 -0
- tradedangerous/commands/market_cmd.py +207 -0
- tradedangerous/commands/nav_cmd.py +252 -0
- tradedangerous/commands/olddata_cmd.py +270 -0
- tradedangerous/commands/parsing.py +221 -0
- tradedangerous/commands/rares_cmd.py +298 -0
- tradedangerous/commands/run_cmd.py +1521 -0
- tradedangerous/commands/sell_cmd.py +262 -0
- tradedangerous/commands/shipvendor_cmd.py +60 -0
- tradedangerous/commands/station_cmd.py +68 -0
- tradedangerous/commands/trade_cmd.py +181 -0
- tradedangerous/commands/update_cmd.py +67 -0
- tradedangerous/corrections.py +55 -0
- tradedangerous/csvexport.py +234 -0
- tradedangerous/db/__init__.py +27 -0
- tradedangerous/db/adapter.py +192 -0
- tradedangerous/db/config.py +107 -0
- tradedangerous/db/engine.py +259 -0
- tradedangerous/db/lifecycle.py +332 -0
- tradedangerous/db/locks.py +208 -0
- tradedangerous/db/orm_models.py +500 -0
- tradedangerous/db/paths.py +113 -0
- tradedangerous/db/utils.py +661 -0
- tradedangerous/edscupdate.py +565 -0
- tradedangerous/edsmupdate.py +474 -0
- tradedangerous/formatting.py +210 -0
- tradedangerous/fs.py +156 -0
- tradedangerous/gui.py +1146 -0
- tradedangerous/mapping.py +133 -0
- tradedangerous/mfd/__init__.py +103 -0
- tradedangerous/mfd/saitek/__init__.py +3 -0
- tradedangerous/mfd/saitek/directoutput.py +678 -0
- tradedangerous/mfd/saitek/x52pro.py +195 -0
- tradedangerous/misc/checkpricebounds.py +287 -0
- tradedangerous/misc/clipboard.py +49 -0
- tradedangerous/misc/coord64.py +83 -0
- tradedangerous/misc/csvdialect.py +57 -0
- tradedangerous/misc/derp-sentinel.py +35 -0
- tradedangerous/misc/diff-system-csvs.py +159 -0
- tradedangerous/misc/eddb.py +81 -0
- tradedangerous/misc/eddn.py +349 -0
- tradedangerous/misc/edsc.py +437 -0
- tradedangerous/misc/edsm.py +121 -0
- tradedangerous/misc/importeddbstats.py +54 -0
- tradedangerous/misc/prices-json-exp.py +179 -0
- tradedangerous/misc/progress.py +194 -0
- tradedangerous/plugins/__init__.py +249 -0
- tradedangerous/plugins/edcd_plug.py +371 -0
- tradedangerous/plugins/eddblink_plug.py +861 -0
- tradedangerous/plugins/edmc_batch_plug.py +133 -0
- tradedangerous/plugins/spansh_plug.py +2647 -0
- tradedangerous/prices.py +211 -0
- tradedangerous/submit-distances.py +422 -0
- tradedangerous/templates/Added.csv +37 -0
- tradedangerous/templates/Category.csv +17 -0
- tradedangerous/templates/RareItem.csv +143 -0
- tradedangerous/templates/TradeDangerous.sql +338 -0
- tradedangerous/tools.py +40 -0
- tradedangerous/tradecalc.py +1302 -0
- tradedangerous/tradedb.py +2320 -0
- tradedangerous/tradeenv.py +313 -0
- tradedangerous/tradeenv.pyi +109 -0
- tradedangerous/tradeexcept.py +131 -0
- tradedangerous/tradeorm.py +183 -0
- tradedangerous/transfers.py +192 -0
- tradedangerous/utils.py +243 -0
- tradedangerous/version.py +16 -0
- tradedangerous-12.7.6.dist-info/METADATA +106 -0
- tradedangerous-12.7.6.dist-info/RECORD +87 -0
- tradedangerous-12.7.6.dist-info/WHEEL +5 -0
- tradedangerous-12.7.6.dist-info/entry_points.txt +3 -0
- tradedangerous-12.7.6.dist-info/licenses/LICENSE +373 -0
- tradedangerous-12.7.6.dist-info/top_level.txt +2 -0
- 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
|
+
|