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,474 @@
1
+ #!/usr/bin/env python3.6
2
+ # Deprecated
3
+
4
+ """
5
+ based on edscupdate.py without the submit_distance()
6
+
7
+ This tool looks for changes in the EDSM service since the most
8
+ recent "modified" date in the System table or the date supplied
9
+ on the command line.
10
+
11
+ It then tries to do some validation but also requires user
12
+ confirmation.
13
+
14
+ For each star that appears to be new, it copies the name into
15
+ the clipboard so you can paste it into the "SEARCH" box in the
16
+ game to verify that the name is correct.
17
+
18
+ Additionally it shows you the distance from "current system"
19
+ to the star as a way to verify the co-ordinates.
20
+
21
+ This helps to catch cases where people have typo'd system names,
22
+ but given the right coordinates; it also helps catch cases where
23
+ people have used the star name from in-system which sometimes
24
+ differs from the star name in the galaxy map.
25
+
26
+ For each star you can type "y" to accept the star, "n" to skip it
27
+ or "q" to stop recording.
28
+ """
29
+
30
+ import argparse
31
+ import misc.clipboard
32
+ import misc.edsm
33
+ import os
34
+ import random
35
+ import re
36
+ import sys
37
+ import tradedb
38
+ import tradeenv
39
+
40
+
41
+ # Systems we know are bad.
42
+ ignore = []
43
+
44
+ class UsageError(Exception):
45
+ """ Raised when command line usage is invalid. """
46
+ pass
47
+
48
+ def parse_arguments():
49
+ parser = argparse.ArgumentParser(
50
+ description='Review and validate incoming EDSM star data.',
51
+ epilog='Confirmed systems are written to tmp/new.systems.csv.',
52
+ )
53
+ parser.add_argument(
54
+ 'refSystem',
55
+ help='*Exact* name of the system you are *currently* in, '
56
+ 'used as a reference system for distance validations.',
57
+ type=str,
58
+ metavar='"REFERENCE SYSTEM"',
59
+ default=None,
60
+ nargs='?',
61
+ )
62
+ parser.add_argument(
63
+ '--cmdr',
64
+ required=False,
65
+ help='Specify your commander name.',
66
+ type=str,
67
+ default=os.environ.get('CMDR', None),
68
+ )
69
+ grp = parser.add_mutually_exclusive_group()
70
+ if grp: # for indentation
71
+ grp.add_argument(
72
+ '--random',
73
+ action='store_true',
74
+ required=False,
75
+ help='Show systems in random order, maximum of --max-systems.',
76
+ )
77
+ grp.add_argument(
78
+ '--distance',
79
+ action='store_true',
80
+ required=False,
81
+ help='Select upto 10 systems by proximity.',
82
+ )
83
+ parser.add_argument(
84
+ '--max-systems',
85
+ dest='maxSystems',
86
+ help='Maximum systems to query with --random/--distance.',
87
+ required=False,
88
+ type=int,
89
+ default=10
90
+ )
91
+ parser.add_argument(
92
+ '--max-ly',
93
+ dest='maxLy',
94
+ help='Maximum distance to reference systems (for --distance).',
95
+ required=False,
96
+ type=int,
97
+ default=0
98
+ )
99
+ parser.add_argument(
100
+ '--add-to-local-db', '-A',
101
+ action='store_true',
102
+ required=False,
103
+ help='Add accepted systems to the local database.',
104
+ dest='add',
105
+ )
106
+ parser.add_argument(
107
+ '--date',
108
+ required=False,
109
+ help='Use specified date (YYYY-MM-DD HH:MM:SS format) for '
110
+ 'start of update search. '
111
+ 'Default is to use the last System modified date.',
112
+ type=str,
113
+ default=None,
114
+ )
115
+ parser.add_argument(
116
+ '--no-splash', '-NS',
117
+ required=False,
118
+ action='store_false',
119
+ help="Don't display the 'splash' text on startup.",
120
+ dest='splash',
121
+ default=True,
122
+ )
123
+ parser.add_argument(
124
+ '--summary',
125
+ required=False,
126
+ help='Check for and report on new systems but do no work.',
127
+ action='store_true',
128
+ )
129
+ parser.add_argument(
130
+ '--detail', '-v',
131
+ help='Increase level of detail in output.',
132
+ default=0,
133
+ required=False,
134
+ action='count',
135
+ )
136
+ parser.add_argument(
137
+ '--debug', '-w',
138
+ help='Enable/raise level of diagnostic output.',
139
+ default=0,
140
+ required=False,
141
+ action='count',
142
+ )
143
+ parser.add_argument(
144
+ '--ref',
145
+ help='Reference system (for --distance).',
146
+ default=None,
147
+ dest='refSys',
148
+ type=str,
149
+ )
150
+ parser.add_argument(
151
+ '--log-edsm',
152
+ required=False,
153
+ help='Log the EDSM request and response in tmp/edsm.log.',
154
+ default=False,
155
+ dest='logEDSM',
156
+ action='store_true',
157
+ )
158
+ parser.add_argument(
159
+ '--yes',
160
+ required=False,
161
+ help='Answer "y" to autoconfirm all EDSM systems.',
162
+ default=False,
163
+ dest='autoOK',
164
+ action='store_true',
165
+ )
166
+
167
+ argv = parser.parse_args(sys.argv[1:])
168
+ if not argv.summary:
169
+ if not argv.refSystem:
170
+ raise UsageError(
171
+ "Must specify a reference system name (when not using "
172
+ "--summary). Be sure to put the name in double quotes, "
173
+ "e.g. \"SOL\" or \"I BOOTIS\".\n"
174
+ )
175
+ if not argv.distance and argv.refSys:
176
+ raise UsageError("--ref requires --distance")
177
+ if not argv.distance and argv.maxLy:
178
+ raise UsageError("--max-ly requires --distance")
179
+
180
+ return argv
181
+
182
+ def is_change(tdb, sysinfo):
183
+ """ Check if a system's EDSM data is different than TDs """
184
+ name = sysinfo['name'] = sysinfo['name'].upper()
185
+ if name in ignore:
186
+ return False
187
+ try:
188
+ x = sysinfo['coords']['x']
189
+ y = sysinfo['coords']['y']
190
+ z = sysinfo['coords']['z']
191
+ place = tdb.systemByName[name]
192
+ if place.posX == x and place.posY == y and place.posZ == z:
193
+ return False
194
+ except KeyError:
195
+ place = None
196
+ sysinfo['place'] = place
197
+ return True
198
+
199
+ def has_position_changed(place, name, x, y, z):
200
+ if not place:
201
+ return False
202
+
203
+ print("! @{} [{},{},{}] changed to @{} [{},{},{}]".format(
204
+ name, x, y, z,
205
+ place.dbname, place.posX, place.posY, place.posZ
206
+ ))
207
+
208
+ return True
209
+
210
+ def check_database(tdb, name, x, y, z):
211
+ # is it in the database?
212
+ cur = tdb.query("""
213
+ SELECT name, pos_x, pos_y, pos_z
214
+ FROM System
215
+ WHERE pos_x BETWEEN ? and ?
216
+ AND pos_y BETWEEN ? and ?
217
+ AND pos_z BETWEEN ? and ?
218
+ """, [
219
+ x - 0.5, x + 0.5,
220
+ y - 0.5, y + 0.5,
221
+ z - 0.5, z + 0.5,
222
+ ])
223
+ for mname, mx, my, mz in cur:
224
+ print(
225
+ "! @{} [{},{},{}] matches coords for "
226
+ "@{} [{},{},{}]".format(
227
+ name, x, y, z,
228
+ mname, mx, my, mz
229
+ ), file=sys.stderr)
230
+
231
+ def get_distance(tdb, startSys, x, y, z):
232
+ distance = tdb.calculateDistance(
233
+ startSys.posX, startSys.posY, startSys.posZ,
234
+ x, y, z
235
+ )
236
+ return float("{:.2f}".format(distance))
237
+
238
+ def get_extras():
239
+ extras = set()
240
+ try:
241
+ with open("data/extra-stars.txt", "r", encoding="utf-8") as fh:
242
+ for line in fh:
243
+ name = line.partition('#')[0].strip().upper()
244
+ if name:
245
+ extras.add(name)
246
+ except FileNotFoundError:
247
+ pass
248
+ return extras
249
+
250
+ def add_to_extras(argv, name):
251
+ with open("data/extra-stars.txt", "a", encoding="utf-8") as fh:
252
+ print(name.upper(), file=fh)
253
+ print("Added {} to data/extra-stars.txt".format(name))
254
+
255
+ def main():
256
+ argv = parse_arguments()
257
+ tdenv = tradeenv.TradeEnv(properties=argv)
258
+ tdenv.quiet = 1
259
+ tdb = tradedb.TradeDB(tdenv)
260
+
261
+ if not argv.summary:
262
+ try:
263
+ argv.startSys = tdb.lookupSystem(argv.refSystem)
264
+ except (LookupError, tradedb.AmbiguityError):
265
+ raise UsageError(
266
+ "Unrecognized system '{}'. Reference System must be an "
267
+ "*exact* name for a system that TD already knows.\n"
268
+ "Did you forget to put double-quotes around the reference "
269
+ "system name?"
270
+ .format(argv.refSystem)
271
+ )
272
+
273
+ if not argv.date:
274
+ argv.date = tdb.query("SELECT MAX(modified) FROM System").fetchone()[0]
275
+ dateRe = re.compile(r'^20\d\d-(0[1-9]|1[012])-(0[1-9]|[12]\d|3[01]) ([01]\d|2[0123]):[0-5]\d:[0-5]\d$')
276
+ if not dateRe.match(argv.date):
277
+ raise UsageError(
278
+ "Invalid date: '{}', expecting YYYY-MM-DD HH:MM:SS format."
279
+ .format(argv.date)
280
+ )
281
+ print("start date: {}".format(argv.date))
282
+
283
+ edsq = misc.edsm.StarQueryMulti(
284
+ log=argv.logEDSM,
285
+ startDateTime=argv.date,
286
+ submitted=1,
287
+ showId=1,
288
+ )
289
+ data = edsq.fetch()
290
+
291
+ systems = data
292
+
293
+ print("{} results".format(len(systems)))
294
+ # Filter out systems we already know that match the EDSM data.
295
+ systems = [
296
+ sysinfo for sysinfo in systems if is_change(tdb, sysinfo)
297
+ ]
298
+ print("{} deltas".format(len(systems)))
299
+
300
+ if argv.summary or len(systems) <= 0:
301
+ return
302
+
303
+ systems = [
304
+ sysinfo for sysinfo in systems if 'coords' in sysinfo
305
+ ]
306
+
307
+ if argv.random:
308
+ num = min(len(systems), argv.maxSystems)
309
+ systems = random.sample(systems, num)
310
+
311
+ if argv.refSys:
312
+ refSys = tdb.lookupPlace(argv.refSys)
313
+ else:
314
+ refSys = None
315
+ startSys = argv.startSys
316
+ for sysinfo in systems:
317
+ x = sysinfo['coords']['x']
318
+ y = sysinfo['coords']['y']
319
+ z = sysinfo['coords']['z']
320
+ sysinfo['distance'] = get_distance(tdb, startSys, x, y, z)
321
+ if refSys:
322
+ sysinfo['refdist'] = get_distance(tdb, refSys, x, y, z)
323
+ else:
324
+ sysinfo['refdist'] = None
325
+
326
+ if argv.distance:
327
+ if argv.maxLy > 0:
328
+ if refSys:
329
+ systems = [
330
+ sysinfo for sysinfo in systems if sysinfo['refdist'] <= argv.maxLy
331
+ ]
332
+ else:
333
+ systems = [
334
+ sysinfo for sysinfo in systems if sysinfo['distance'] <= argv.maxLy
335
+ ]
336
+ else:
337
+ if refSys:
338
+ systems.sort(key=lambda sysinfo: sysinfo['refdist'])
339
+ else:
340
+ systems.sort(key=lambda sysinfo: sysinfo['distance'])
341
+ systems = systems[:argv.maxSystems]
342
+
343
+ if argv.splash and not argv.autoOK:
344
+ print(
345
+ "\n"
346
+ "===============================================================\n"
347
+ "\n"
348
+ " The tool will now take you through the stars returned by EDSM\n"
349
+ " that are new or different from your local System.csv.\n"
350
+ "\n"
351
+ " You will be prompted with the name and predicted distance from\n"
352
+ " your current system so you can check for mistakes.\n"
353
+ "\n"
354
+ " The name will be copied into your clipboard so you can alt-tab\n"
355
+ " into the game and paste the name into the Galaxy Map's SEARCH\n"
356
+ " box (under NAVIGATION). Let the map zoom to the system.\n"
357
+ "\n"
358
+ " Check the name and distance, then use the appropriate action.\n"
359
+ "\n"
360
+ " (Use the -NS option to skip this text in future)\n"
361
+ "\n"
362
+ "===============================================================\n"
363
+ "\n"
364
+ )
365
+
366
+ input("Hit enter to continue: ")
367
+
368
+ if not argv.autoOK:
369
+ print("""At the prompt enter:
370
+ q
371
+ to indicate you've suffered enough,
372
+ y
373
+ to accept the name/value,
374
+ n (or anything else)
375
+ to skip the name/value (no confirmation),
376
+ =name (e.g. =SOL)
377
+ to accept the distance but correct spelling,
378
+ """)
379
+ print()
380
+
381
+ extras = get_extras()
382
+
383
+ clip = misc.clipboard.SystemNameClip()
384
+ total = len(systems)
385
+ current = 0
386
+ with open("tmp/new.systems.csv", "w", encoding="utf-8") as output:
387
+ if argv.autoOK:
388
+ commit=False
389
+ else:
390
+ commit=True
391
+ for sysinfo in systems:
392
+ current += 1
393
+ name = sysinfo['name']
394
+ created = sysinfo['date']
395
+ x = sysinfo['coords']['x']
396
+ y = sysinfo['coords']['y']
397
+ z = sysinfo['coords']['z']
398
+
399
+ if not argv.autoOK:
400
+ print(
401
+ "\n"
402
+ "-----------------------------------------------\n"
403
+ "{syidlab:.<12}: {syid}\n"
404
+ "{crealab:.<12}: {crts}\n"
405
+ .format(
406
+ syidlab="ID",
407
+ crealab="Created",
408
+ syid=sysinfo['id'],
409
+ crts=created,
410
+ )
411
+ )
412
+ if refSys:
413
+ print("{reflab:.<12}: {refdist}ly\n".format(
414
+ reflab="Ref Dist",
415
+ refdist=sysinfo['refdist'],
416
+ ))
417
+
418
+ check_database(tdb, name, x, y, z)
419
+
420
+ change = has_position_changed(sysinfo['place'], name, x, y, z)
421
+ if change:
422
+ oldDist = startSys.distanceTo(sysinfo['place'])
423
+ print("Old Distance: {:.2f}ly".format(oldDist))
424
+
425
+ distance = sysinfo['distance']
426
+ clip.copy_text(name)
427
+ prompt = "{}/{}: '{}': {:.2f}ly? ".format(
428
+ current, total,
429
+ name,
430
+ distance,
431
+ )
432
+ if argv.autoOK:
433
+ if change:
434
+ ok = "n"
435
+ else:
436
+ ok = "y"
437
+ else:
438
+ ok = input(prompt)
439
+ if ok.lower() == 'q':
440
+ break
441
+ if ok.startswith('='):
442
+ name = ok[1:].strip().upper()
443
+ if name not in extras:
444
+ add_to_extras(argv, name)
445
+ ok = 'y'
446
+ if ok.lower() != 'y':
447
+ continue
448
+
449
+ if argv.add:
450
+ print("Add {:>6}: {:>12} {} {}".format(current, sysinfo['id'], created, name))
451
+ tdb.addLocalSystem(
452
+ name,
453
+ x, y, z,
454
+ added='EDSM',
455
+ modified=created,
456
+ commit=commit
457
+ )
458
+
459
+ print("'{}',{},{},{},'EDSM','{}'".format(
460
+ name, x, y, z, created,
461
+ ), file=output)
462
+ if argv.add and not commit:
463
+ tdb.getDB().commit()
464
+
465
+
466
+ if __name__ == "__main__":
467
+ try:
468
+ main()
469
+ except KeyboardInterrupt:
470
+ print("^C")
471
+ except UsageError as e:
472
+ print("ERROR: {}\nSee {} --help for usage help.".format(
473
+ str(e), sys.argv[0]
474
+ ))
@@ -0,0 +1,210 @@
1
+ """
2
+ Provides a library of mechanisms for formatting output text to ensure consistency across
3
+ TradeDangerous and plugin tools,
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import itertools
8
+ import typing
9
+
10
+ if typing.TYPE_CHECKING:
11
+ from collections.abc import Callable, Iterable
12
+ from typing import Any
13
+
14
+
15
+ class ColumnFormat:
16
+ """
17
+ Describes formatting of a column to be populated with data.
18
+
19
+ Member Functions:
20
+
21
+ text()
22
+ Applies all formatting (except qualifier) to the name to
23
+ produce a correctly sized title field.
24
+
25
+ format(value)
26
+ Applies all formatting to key(value) to produce a correctly
27
+ sized value field.
28
+
29
+ Attributes:
30
+ name
31
+ Heading for column to display when calling title()
32
+ e.g. name="Station Name"
33
+ align
34
+ Alignment formatter for .format
35
+ e.g. align='<' or align='>' or align=''
36
+ width
37
+ Numeric value for the width of the column
38
+ e.g. width=5
39
+ qualifier
40
+ Final part of the print format,
41
+ e.g. qualifier='.2f' or qualifier='n'
42
+ pre
43
+ Prefix to the column
44
+ post
45
+ Postfix to the column
46
+ key
47
+ Retrieve the printable name of the item
48
+ pred
49
+ Predicate: Return False to leave this column blank
50
+
51
+ e.g.
52
+ cols = [
53
+ ColumnFormat("Name", "<", "5", '', key=lambda item:item['name']),
54
+ ColumnFormat("Dist", ">", "6", ".2f",
55
+ pre='[',
56
+ post=']'
57
+ key=lambda item:item['dist']),
58
+ ]
59
+ rows = [ {'name':'Bob', 'dist':1.5}, {'name':'John', 'dist':23}]
60
+ # print titles
61
+ print(*[col.text() for col in cols])
62
+ for row in rows:
63
+ print(*[col.format(row) for col in cols])
64
+ Produces:
65
+ Name [ Dist]
66
+ Bob [ 1.30]
67
+ John [23.00]
68
+
69
+ """
70
+ name: str # name of the column
71
+ align: str # format's alignment specifier
72
+ width: int # width specifier
73
+ qualifier: str | None # optional format type specifier e.g. '.2f', 's', 'n'
74
+ pre: str | None # prefix to the column
75
+ post: str | None # postfix to the column
76
+ key: Callable # function to retrieve the printable name of the item
77
+ pred: Callable # predicate: return False to leave this column blank
78
+
79
+ def __init__(
80
+ self,
81
+ name,
82
+ align,
83
+ width,
84
+ qualifier=None,
85
+ pre=None,
86
+ post=None,
87
+ key=lambda item: item,
88
+ pred=lambda item: True,
89
+ ) -> None:
90
+ self.name = name
91
+ self.align = align
92
+ self.width = max(int(width), len(name))
93
+ self.qualifier = qualifier or ''
94
+ self.key = key
95
+ self.pre = pre or ''
96
+ self.post = post or ''
97
+ self.pred = pred
98
+
99
+ def __str__(self) -> str:
100
+ return f'{self.pre}{self.name:{self.align}{self.width}}{self.post}'
101
+ text = __str__
102
+
103
+ def format(self, value: str) -> str:
104
+ """ Returns the string formatted with a specific value"""
105
+ if not self.pred(value):
106
+ return f'{self.pre}{"":{self.align}{self.width}}{self.post}'
107
+ return f'{self.pre}{self.key(value):{self.align}{self.width}{self.qualifier}}{self.post}'
108
+
109
+ class RowFormat:
110
+ """
111
+ Describes an ordered collection of ColumnFormats
112
+ for dispay data from rows, such that calling
113
+ rowFmt.format(rowData)
114
+ will return the result of formatting each column
115
+ against rowData.
116
+
117
+ Member Functions
118
+
119
+ append(col, [after])
120
+ Adds a ColumnFormatter to the end of the row
121
+ If 'after' is specified, tries to insert
122
+ the new column immediately after the first
123
+ column who's name matches after.
124
+
125
+ insert(pos, newCol)
126
+ Inserts a ColumnFormatter at position pos in the list
127
+
128
+ text()
129
+ Returns a list of all the column headings
130
+
131
+ format(rowData):
132
+ Returns a list of applying rowData to all
133
+ of the columns
134
+
135
+ """
136
+ columns: list[ColumnFormat]
137
+ prefix: str
138
+ suffix: str
139
+
140
+ def __init__(self, prefix: str | None = None, suffix: str | None = None) -> None:
141
+ self.columns = []
142
+ self.prefix = prefix or ""
143
+ self.suffix = suffix or ""
144
+
145
+ def addColumn(self, *args, **kwargs) -> None:
146
+ self.append(ColumnFormat(*args, **kwargs))
147
+
148
+ def append(self, column: ColumnFormat, after: str | None = None) -> 'RowFormat':
149
+ columns = self.columns
150
+ if after:
151
+ for idx, col in enumerate(columns, 1):
152
+ if col.name == after:
153
+ columns.insert(idx, column)
154
+ return self
155
+ columns.append(column)
156
+ return self
157
+
158
+ def insert(self, pos: int, column: ColumnFormat | None) -> None:
159
+ if column is not None:
160
+ self.columns.insert(pos, column)
161
+
162
+ def __str__(self) -> str:
163
+ return f"{self.prefix} {' '.join(str(col) for col in self.columns)}{self.suffix}"
164
+
165
+ text = __str__ # alias
166
+
167
+ def heading(self) -> tuple[str, str]:
168
+ """ Returns a title and the appropriate underline for that text. """
169
+ headline = f"{self}"
170
+ return headline, '-' * len(headline)
171
+
172
+ def format(self, row_data: Any) -> str:
173
+ return f"{self.prefix} {' '.join(col.format(row_data) for col in self.columns)}{self.suffix}"
174
+
175
+
176
+ def max_len(iterable: Iterable, key: Callable[[Any], str] = lambda item: item) -> int:
177
+ """ Helper that returns the maximum length of strings produced
178
+ by applying key() to the elements of the given iterable. """
179
+ iterable, readahead = itertools.tee(iter(iterable))
180
+ try:
181
+ next(readahead)
182
+ except StopIteration:
183
+ return 0
184
+ return max(len(key(item)) for item in iterable)
185
+
186
+
187
+ if __name__ == '__main__':
188
+ rowFmt = RowFormat(). \
189
+ append(ColumnFormat("Name", '<', '8', key=lambda row: row['name'])). \
190
+ append(ColumnFormat("Dist", '>', '6', '.2f', pre='[', post=']', key=lambda row: row['dist']))
191
+
192
+ rows = [
193
+ { 'name': 'Bob', 'dist': 6.2, 'age': 30 },
194
+ { 'name': 'Dave', 'dist': 42, 'age': 18 },
195
+ ]
196
+
197
+ def present():
198
+ rowTitle = rowFmt.text()
199
+ print(rowTitle)
200
+ print('-' * len(rowTitle))
201
+ for row in rows:
202
+ print(rowFmt.format(row))
203
+
204
+ print("Simple usage:")
205
+ present()
206
+
207
+ # print()
208
+ # print("Adding age ColumnFormat:")
209
+ # rowFmt.append(after='Name', col=ColumnFormat("Age", '>', 3, pre='|', post='|', key=lambda row: row['age']))
210
+ # present()