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,195 @@
1
+ """
2
+ x52pro.py - DirectOutputDevice for SaitekX52Pro
3
+
4
+ Version: 0.2
5
+ Author: Frazzle
6
+ Version: 0.3
7
+ Author: Oliver "kfsone" Smith <oliver@kfs.org> 2014
8
+
9
+ Description: Python class implementing specific functions for Saitek X52 Pro
10
+
11
+
12
+ TODO:
13
+ * Error handling and exceptions
14
+ """
15
+
16
+ from tradedangerous.mfd.saitek.directoutput import DirectOutputDevice
17
+
18
+ import sys
19
+ import time
20
+
21
+
22
+ class X52Pro(DirectOutputDevice):
23
+ class Page:
24
+ _lines = [ str(), str(), str() ]
25
+ _leds = dict()
26
+
27
+ def __init__(self, device, page_id, name, active):
28
+ self.device = device
29
+ self.page_id = page_id
30
+ self.name = name
31
+ self.device.AddPage(self.page_id, name, 1 if active else 0)
32
+ self.active = active
33
+
34
+ def __del__(self, *args, **kwargs):
35
+ try:
36
+ self.device.RemovePage(self.page_id)
37
+ except AttributeError:
38
+ pass
39
+
40
+ def __getitem__(self, key):
41
+ return self._lines[key]
42
+
43
+ def __setitem__(self, key, value):
44
+ self._lines[key] = value
45
+ if self.active:
46
+ self.device.SetString(self.page_id, key, value)
47
+
48
+ def activate(self):
49
+ if self.active is True:
50
+ return
51
+ self.device.AddPage(self.page_id, self.name, 1)
52
+
53
+ def refresh(self):
54
+ # Resend strings to the display
55
+ for lineNo, string in enumerate(self._lines):
56
+ self.device.SetString(self.page_id, lineNo, string)
57
+ for led, value in self._leds:
58
+ self.device.SetLed(self.page_id, led, 1 if value else 0)
59
+
60
+ def set_led(self, led, value):
61
+ self._leds[led] = value
62
+ if self.active:
63
+ self.device.SetLed(self.page_id, led, 1 if value else 0)
64
+
65
+ def set_led_colour(self, value, led_red, led_green):
66
+ if value == "red":
67
+ self.set_led(led_red, 1)
68
+ self.set_led(led_green, 0)
69
+ elif value == "green":
70
+ self.set_led(led_red, 0)
71
+ self.set_led(led_green, 1)
72
+ elif value == "orange":
73
+ self.set_led(led_red, 1)
74
+ self.set_led(led_green, 1)
75
+ elif value == "off":
76
+ self.set_led(led_red, 0)
77
+ self.set_led(led_green, 0)
78
+
79
+ def fire(self, value):
80
+ self.set_led(0, value)
81
+
82
+ def fire_a(self, value):
83
+ self.set_led_colour(value, 1, 2)
84
+
85
+ def fire_b(self, value):
86
+ self.set_led_colour(value, 3, 4)
87
+
88
+ def fire_d(self, value):
89
+ self.set_led_colour(value, 5, 6)
90
+
91
+ def fire_e(self, value):
92
+ self.set_led_colour(value, 7, 8)
93
+
94
+ def toggle_1_2(self, value):
95
+ self.set_led_colour(value, 9, 10)
96
+
97
+ def toggle_3_4(self, value):
98
+ self.set_led_colour(value, 11, 12)
99
+
100
+ def toggle_5_6(self, value):
101
+ self.set_led_colour(value, 13, 14)
102
+
103
+ def pov_2(self, value):
104
+ self.set_led_colour(value, 15, 16)
105
+
106
+ def clutch(self, value):
107
+ self.set_led_colour(value, 17, 18)
108
+
109
+ def throttle_axis(self, value):
110
+ self.set_led(19, value)
111
+
112
+ pages = {}
113
+ _page_counter = 0
114
+
115
+ def add_page(self, name, active=True):
116
+ page = self.pages[name] = self.Page(self, self._page_counter, name, active=active)
117
+ self._page_counter += 1
118
+ return page
119
+
120
+ def remove_page(self, name):
121
+ del self.pages[name]
122
+
123
+
124
+ def OnPage(self, page_id, activated):
125
+ super().OnPage(page_id, activated)
126
+ for page in self.pages.values():
127
+ if page.page_id == page_id:
128
+ print("Found the page", page_id, activated)
129
+ if activated:
130
+ page.refresh()
131
+ else:
132
+ page.active = False
133
+ return
134
+
135
+
136
+ def OnSoftButton(self, *args, **kwargs):
137
+ super().OnSoftButton(*args, **kwargs)
138
+ print("*** ON SOFT BUTTON")
139
+
140
+
141
+ def finish(self):
142
+ for page in self.pages:
143
+ del page
144
+ super().finish()
145
+
146
+
147
+ if __name__ == '__main__':
148
+ x52 = X52Pro(debug_level=1)
149
+ print("X52 Connected")
150
+
151
+ x52.add_page("Page1")
152
+ print("Page1 page added")
153
+
154
+ x52.pages["Page1"][0] = "Test String"
155
+ try:
156
+ x52.pages["Page1"][5] = "FAIL"
157
+ raise ValueError("Your stick thinks it has 5 lines of MFD, I think it lies.")
158
+ except IndexError:
159
+ print("Your MFD has the correct number of lines of text")
160
+
161
+ x52.pages["Page1"][1] = "-- Page 1 [0]"
162
+
163
+ fireToggle = False
164
+ x52.pages["Page1"].fire(fireToggle)
165
+ x52.pages["Page1"].fire_a("amber")
166
+ x52.pages["Page1"].toggle_3_4("red")
167
+ x52.pages["Page1"].throttle_axis(~fireToggle)
168
+
169
+ print("Adding second page")
170
+ x52.add_page("Page2", active=False)
171
+ x52.pages["Page2"][0] = "Second Page Is Right Here"
172
+ x52.pages["Page2"][1] = "-- Page 2 [1]"
173
+
174
+ print("Looping")
175
+
176
+ loopNo = 0
177
+ while True:
178
+ try:
179
+ loopNo += 1
180
+ x52.pages["Page1"][2] = "Loop #" + str(loopNo)
181
+ time.sleep(0.25)
182
+ x52.pages["Page1"].fire_a("red")
183
+ time.sleep(0.25)
184
+ x52.pages["Page1"].fire_a("orange")
185
+ time.sleep(0.25)
186
+ x52.pages["Page1"].fire_a("green")
187
+ time.sleep(0.25)
188
+ x52.pages["Page1"].fire_a("off")
189
+ fireToggle = ~fireToggle
190
+ x52.pages["Page1"].fire(fireToggle)
191
+ x52.pages["Page1"].throttle_axis(~fireToggle)
192
+ except Exception as e:
193
+ print(e)
194
+ x52.finish()
195
+ sys.exit()
@@ -0,0 +1,287 @@
1
+ #! /usr/bin/env python
2
+
3
+ from collections import defaultdict
4
+ import argparse
5
+ import colorama
6
+ import tradedb
7
+ import tradeenv
8
+ import sys
9
+
10
+ colorama.init()
11
+
12
+ def check_price_bounds(
13
+ tdb,
14
+ table,
15
+ margin=25,
16
+ deletePrices="tmp/deletions.prices",
17
+ deleteSql="tmp/deletions.sql",
18
+ doDeletions=False,
19
+ percentile=5,
20
+ errorFilter=None,
21
+ ):
22
+ assert isinstance(percentile, (int,float))
23
+ assert percentile >= 0.01 and percentile < 50
24
+
25
+ lowP = percentile / 100
26
+ highP = 1 - lowP
27
+
28
+ mask = "{:>7}:{:<28}|{:>8}|{:>8}|{:>8}|{:>8}|{:>8}|{:>8}|{:>8}|{:>8}|{:>8} {}"
29
+ header = "{}:\n".format(table)
30
+ header += mask.format(
31
+ "#prices", "name",
32
+ "p0", "low", "p{}".format(percentile),
33
+ "p50", "avg", "midavg",
34
+ "p{}".format(100-percentile), "high", "p100",
35
+ ""
36
+ )
37
+ header += "\n" + '-' * len(header)
38
+ print(header)
39
+
40
+ deletions = []
41
+ stations = defaultdict(list)
42
+
43
+ def remediation(item, compare, value):
44
+ deletion = (
45
+ "DELETE FROM {} "
46
+ "WHERE item_id = {} "
47
+ "AND price > 0 AND price {} {}".format(
48
+ table, item.ID, compare, value
49
+ ))
50
+ deletions.append((deletion, "{}".format(item.dbname)))
51
+ count = 0
52
+ for (stnID,) in tdb.query("""
53
+ SELECT station_id FROM {}
54
+ WHERE item_id = {} AND price > 0 AND price {} {}
55
+ """.format(
56
+ table, item.ID, compare, value,
57
+ )):
58
+ stations[stnID].append((item.ID, compare, value))
59
+ count += 1
60
+ return count
61
+
62
+ # Distance between p50 and p5 or p95 is 45.
63
+ # We use multiplier to expand projections so that our thresholds
64
+ # behave as though they are pX to p50+M instead of pX to p50.
65
+ multiplier = (50 + margin) / (50 - lowP)
66
+
67
+ for item in tdb.itemByID.values():
68
+ if item.dbname.upper() in tdb.tdenv.ignore:
69
+ continue
70
+ cur = tdb.query("""
71
+ SELECT price
72
+ FROM {}
73
+ WHERE item_id = ?
74
+ AND price > 0
75
+ """.format(table), [item.ID])
76
+ prices = [ row[0] for row in cur ]
77
+ if not prices:
78
+ if tdb.tdenv.detail:
79
+ tdb.tdenv.NOTE("{}: Zero entries", item.dbname)
80
+ continue
81
+ prices.sort()
82
+ numPrices = len(prices)
83
+ if numPrices < 20:
84
+ tdb.tdenv.NOTE("{}: only {} rows", item.dbname, numPrices)
85
+ continue
86
+
87
+ lowPos = int(numPrices * lowP)
88
+ midPos = int(numPrices * 0.5)
89
+ highPos = int(numPrices * highP)
90
+
91
+ low = prices[lowPos]
92
+ mid = prices[midPos]
93
+ high = prices[highPos]
94
+
95
+ avg = int(sum(prices) / numPrices)
96
+ midlist = prices[lowPos:highPos]
97
+ midavg = int(sum(midlist) / len(midlist))
98
+
99
+ # project the line from p50->p5 to predict
100
+ # what we would expect p0 to be.
101
+ # prices under 11 are invalid anyway
102
+ bestMid = max(mid, avg)
103
+ leastMid = min(mid, avg)
104
+ lowCutoff = max(int(leastMid - (bestMid - low) * multiplier), 0)
105
+ highCutoff = int(bestMid + (high - leastMid) * multiplier)
106
+
107
+ if prices[0] < 11:
108
+ alert = colorama.Fore.RED
109
+ comp, cutoff, error = '<', 11, 'DUMB'
110
+ elif prices[0] < lowCutoff:
111
+ alert = colorama.Fore.YELLOW
112
+ comp, cutoff, error = '<', lowCutoff, 'LOW'
113
+ elif prices[-1] > 100 and prices[-1] > highCutoff:
114
+ alert = colorama.Fore.CYAN
115
+ comp, cutoff, error = '>', highCutoff, 'HIGH'
116
+ else:
117
+ continue
118
+
119
+ if errorFilter and error != errorFilter:
120
+ continue
121
+
122
+ count = remediation(item, comp, cutoff)
123
+ print(
124
+ alert,
125
+ mask.format(
126
+ numPrices,
127
+ item.dbname,
128
+ prices[0],
129
+ lowCutoff,
130
+ low,
131
+ mid,
132
+ avg,
133
+ midavg,
134
+ high,
135
+ highCutoff,
136
+ prices[-1],
137
+ '{:<4s} {:>4n}'.format(error, count),
138
+ ),
139
+ colorama.Fore.RESET,
140
+ sep="",
141
+ )
142
+
143
+ if stations:
144
+ print()
145
+ print("Generating", deletePrices)
146
+ now = tdb.query("SELECT CURRENT_TIMESTAMP").fetchone()[0]
147
+ with open(deletePrices, "w", encoding="utf-8") as fh:
148
+ print("# Deletions based on {} prices".format(
149
+ table,
150
+ ), file=fh)
151
+ for stnID, items in stations.items():
152
+ station = tdb.stationByID[stnID]
153
+ print(file=fh)
154
+ print("@ {}".format(station.name()), file=fh)
155
+ for item in items:
156
+ itemID = item[0]
157
+ print(" {:<30} {:>7} {:>7} {:>9} {:>9} {} # was {} {}"
158
+ .format(
159
+ tdb.itemByID[itemID].name(),
160
+ 0, 0,
161
+ '-', '-',
162
+ now,
163
+ item[1], item[2],
164
+ ), file=fh
165
+ )
166
+ if doDeletions:
167
+ db = tdb.getDB()
168
+ print("Generating", deleteSql)
169
+ with open(deleteSql, "w", encoding="utf-8") as fh:
170
+ for deletion in deletions:
171
+ print(deletion[0], '; --', deletion[1], file=fh)
172
+ if doDeletions:
173
+ db.execute(deletion[0])
174
+ if doDeletions:
175
+ db.commit()
176
+
177
+ def main():
178
+ parser = argparse.ArgumentParser(
179
+ description='Check for prices that are outside reasonable bounds.'
180
+ )
181
+ parser.add_argument(
182
+ '--percentile',
183
+ help='Set cutoff percentile',
184
+ type=float,
185
+ default=2,
186
+ )
187
+ parser.add_argument(
188
+ '--selling',
189
+ help='Check the StationSelling table instead of StationBuying',
190
+ action='store_true',
191
+ )
192
+ parser.add_argument(
193
+ '--delete',
194
+ help='Remove bad elements from the local .db immediately',
195
+ action='store_true',
196
+ default=False,
197
+ )
198
+ parser.add_argument(
199
+ '--db',
200
+ help='Specify location of the SQLite database.',
201
+ default=None,
202
+ dest='dbFilename',
203
+ type=str,
204
+ )
205
+ parser.add_argument(
206
+ '--debug', '-w',
207
+ help='Enable/raise level of diagnostic output.',
208
+ default=0,
209
+ required=False,
210
+ action='count',
211
+ )
212
+ parser.add_argument(
213
+ '--detail', '-v',
214
+ help='Increase level of detail in output.',
215
+ default=0,
216
+ required=False,
217
+ action='count',
218
+ )
219
+ parser.add_argument(
220
+ '--quiet', '-q',
221
+ help='Reduce level of detail in output.',
222
+ default=0,
223
+ required=False,
224
+ action='count',
225
+ )
226
+ parser.add_argument(
227
+ '--margin',
228
+ help='Adjust the error margin.',
229
+ type=int,
230
+ default=25,
231
+ )
232
+ parser.add_argument(
233
+ '--ignore',
234
+ help='Ignore items.',
235
+ action='append',
236
+ default=[],
237
+ )
238
+
239
+ filters = parser.add_mutually_exclusive_group()
240
+ filters.add_argument(
241
+ '--dumb',
242
+ help='Limit to "DUMB" items (<11cr).',
243
+ dest='filters',
244
+ action='store_const',
245
+ const='DUMB',
246
+ )
247
+ filters.add_argument(
248
+ '--high',
249
+ help='Limit to "HIGH" items.',
250
+ dest='filters',
251
+ action='store_const',
252
+ const='HIGH',
253
+ )
254
+ filters.add_argument(
255
+ '--low',
256
+ help='Limit to "LOW" items.',
257
+ dest='filters',
258
+ action='store_const',
259
+ const='LOW',
260
+ )
261
+
262
+ argv = parser.parse_args(sys.argv[1:])
263
+ argv.ignore = [
264
+ ignore.upper() for ignore in argv.ignore
265
+ ]
266
+
267
+ table = "StationSelling" if argv.selling else "StationBuying"
268
+ tdenv = tradeenv.TradeEnv(properties=argv)
269
+ tdb = tradedb.TradeDB(tdenv)
270
+
271
+ tdenv.NOTE(
272
+ "Checking {}, margin={}", table, argv.margin,
273
+ )
274
+
275
+ errorFilter = getattr(argv, "filters", None)
276
+ check_price_bounds(
277
+ tdb,
278
+ table,
279
+ margin=argv.margin,
280
+ doDeletions=argv.delete,
281
+ percentile=argv.percentile,
282
+ errorFilter=errorFilter,
283
+ )
284
+
285
+
286
+ if __name__ == "__main__":
287
+ main()
@@ -0,0 +1,49 @@
1
+ """
2
+ Wrapper for trade-dangerous clipboard functionality.
3
+ """
4
+
5
+ import os
6
+
7
+ if 'NOTK' not in os.environ:
8
+ try:
9
+ from tkinter import Tk
10
+
11
+ class SystemNameClip:
12
+ """
13
+ A cross-platform wrapper for copying system names into
14
+ the clipboard such that they can be pasted into the E:D
15
+ galaxy search - which means forcing them into lower case
16
+ because E:D's search doesn't work with upper case
17
+ characters.
18
+ """
19
+
20
+ def __init__(self):
21
+ self.tkroot = Tk()
22
+ self.tkroot.withdraw()
23
+
24
+ def strip(self, text, trailing):
25
+ if text.endswith(trailing):
26
+ text = text[:-len(trailing)].strip()
27
+ return text
28
+
29
+ def copy_text(self, text):
30
+ """ Copies the specified text into the clipboard. """
31
+ text = text.lower().strip()
32
+ text = self.strip(text, "(fix)")
33
+ text = self.strip(text, "(fixed)")
34
+ self.tkroot.clipboard_clear()
35
+ self.tkroot.clipboard_append(text.lower())
36
+
37
+ except ImportError:
38
+ print(
39
+ "WARNING: This feature expects you to have 'tkinter' package "
40
+ "installed to work. It is either not installed or broken.\n"
41
+ "Set the environment variable 'NOTK' to disable this warning."
42
+ )
43
+
44
+ class SystemNameClip:
45
+ """
46
+ Dummy implementation when tkinter is not available.
47
+ """
48
+ def copy_text(self, text):
49
+ pass
@@ -0,0 +1,83 @@
1
+ # Coordinate ID generator:
2
+ # Takes a set of 3d coordinates (x, y, z), truncates to 2dp and then
3
+ # generates a base64-like(*) representation that can be used as an id
4
+ # for the object.
5
+ #
6
+ # The actual encoding is not base64, since base64 contains characters
7
+ # that present a problem when used, e.g, in URIs. I replaced the '+'
8
+ # and '/' characters with '_' and '.' respectively.
9
+ #
10
+ # Example usage:
11
+ # coord_to_id64(51.5625, 32.1875, -27.125)
12
+ # Generates:
13
+ # "1gA:Oi:-Go"
14
+ #
15
+ # A reversing function is also provided.
16
+ #
17
+ # Original Author: Oliver "kfsone" Smith <oliver@kfs.org>
18
+ # Released under the "use it with attribution" license.
19
+
20
+ import string
21
+
22
+
23
+ alphabet = string.digits + string.ascii_lowercase + string.ascii_uppercase + '_.'
24
+ precision = 100.
25
+
26
+
27
+ def coord_to_d64(coord):
28
+ i = int(abs(coord * precision))
29
+
30
+ digits = ""
31
+ while True:
32
+ digits = alphabet[i & 63] + digits
33
+ i >>= 6 # divides by 64, or one digit
34
+ if i == 0:
35
+ break
36
+
37
+ sign = '-' if coord < 0 else ''
38
+ return sign + digits
39
+
40
+
41
+ def d64_to_coord(d64):
42
+ divisor, digits = precision, d64
43
+ if d64.startswith('-'):
44
+ divisor = -divisor
45
+ digits = digits[1:]
46
+
47
+ number = 0
48
+ for digit in digits:
49
+ value = alphabet.find(digit)
50
+ if value < 0:
51
+ raise ValueError("Invalid d64 value: {}".format(value))
52
+ number = (number * 64) + value
53
+
54
+ return number / divisor
55
+
56
+
57
+ def pos_to_id64(x, y, z):
58
+ return coord_to_d64(x) + ':' + coord_to_d64(y) + ':' + coord_to_d64(z)
59
+
60
+
61
+ def id64_to_pos(id64):
62
+ (x64, y64, z64) = id64.split(':')
63
+ return (d64_to_coord(x64), d64_to_coord(y64), d64_to_coord(z64))
64
+
65
+
66
+ if __name__ == "__main__":
67
+ test1 = ( 51.5625,32.1875,-27.125 )
68
+ test2 = ( -154.65625,40.34375,-82.78125 )
69
+
70
+ id64 = pos_to_id64(test1[0], test1[1], test1[2])
71
+ print("id64 of {} = {}".format(test1, id64))
72
+ pos = id64_to_pos(id64)
73
+ print("pos of {} = {}".format(id64, pos))
74
+
75
+ id64 = pos_to_id64(test1[0], test1[1], test1[2])
76
+ print("id64 of {} = {}".format(test1, id64))
77
+ pos = id64_to_pos(id64)
78
+ print("pos of {} = {}".format(id64, pos))
79
+
80
+ id64 = pos_to_id64(test2[0], test2[1], test2[2])
81
+ print("id64 of {} = {}".format(test2, id64))
82
+ pos = id64_to_pos(id64)
83
+ print("pos of {} = {}".format(id64, pos))
@@ -0,0 +1,57 @@
1
+ import csv
2
+
3
+
4
+ class CSVDialect:
5
+ """
6
+ Defines the TDCSVDialect class for fine-tuning CSV parsing.
7
+
8
+ Python's CSV parser uses an Excel dialect for CSV parsing which
9
+ seems to introduce some needless overhead parsing our simpler,
10
+ standard format.
11
+
12
+ Providing the correct defaults trims away a little inefficiency,
13
+ but it may also protect us against any opinionated future
14
+ changes to the python defaults.
15
+
16
+ In particular, the current approach causes the CSV parser to
17
+ produce quoted strings with their quotes intact:
18
+
19
+ csv.reader("'hello'")
20
+ -> "'hello'"
21
+ csv.reader("'hello'", dialect=CSVDialect)
22
+ -> "hello"
23
+
24
+ Use:
25
+
26
+ import csv
27
+ from tradedangerous.misc.csvdialect import CSVDialect
28
+
29
+ old_style = csv.reader(open("data/System.csv", encoding="utf-8"))
30
+ new_style = csv.reader(open("data/System.csv", encoding="utf-8"),
31
+ dialect=CSVDialect)
32
+
33
+ print("headers:")
34
+ print("- old:", next(old_style))
35
+ print("- new:", next(new_style)) # no difference
36
+
37
+ for i, (old, new) in enumerate(zip(old_style, new_style)):
38
+ print(f"{i}: ids={old[0]},{new[0]}; names={old[1]},{new[1]}")
39
+ if i >= 5:
40
+ break
41
+ """
42
+ # comma separator, not tab - it's what the 'c' stands for, damnit Excel.
43
+ delimiter = ","
44
+ # single-quote for non-numeric values
45
+ quotechar = "'"
46
+ # The only escape we support is '' within a quoted string for a literal single-quote
47
+ # ... they term that ... *drum roll* double quote.
48
+ escapechar = None
49
+ doublequote = True # ... I'm not making this up
50
+ # Unix-style line endings, no old-mac LFCR and no old-dos CRLF please.
51
+ lineterminator = "\n"
52
+ # No whitespace chasing
53
+ skipinitialspace = False
54
+ # We actually expect all non-numerics to be quoted, such as the empty string,
55
+ # but we're happy to tolerate less. Also, if we use the slightly more aggressive
56
+ # quoting, it requires floats to be quoted ... which seems insane.
57
+ quoting = csv.QUOTE_MINIMAL
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env python
2
+
3
+ import tradedb
4
+ tdb = tradedb.TradeDB()
5
+
6
+ names = set()
7
+
8
+ for sys in tdb.systemByID.values():
9
+ for stn in sys.stations:
10
+ names.add(stn.name().upper())
11
+
12
+ mutators = {
13
+ 'D': [ 'O', '0', ],
14
+ 'W': [ 'VV', ],
15
+ 'R': [ 'IT' ],
16
+ 'L': [ 'II' ],
17
+ '&': [ '6' ],
18
+ }
19
+
20
+ def mutate(text, pos):
21
+ for i in range(pos, len(text)):
22
+ char = text[i]
23
+ if char not in mutators:
24
+ continue
25
+ bef, aft = text[:i], text[i+1:]
26
+ for mutant in mutators[char]:
27
+ t2 = bef + mutant + aft
28
+ yield t2
29
+ yield from mutate(str(t2), i+len(mutant))
30
+
31
+
32
+ for name in names:
33
+ for mutant in mutate(name, 0):
34
+ if mutant in names:
35
+ print("{} <-> {}".format(name, mutant))