chesscalc 2.0.0__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 (125) hide show
  1. chesscalc/__init__.py +71 -0
  2. chesscalc/apsw/__init__.py +5 -0
  3. chesscalc/apsw/database.py +46 -0
  4. chesscalc/apsw/database_du.py +35 -0
  5. chesscalc/basecore/__init__.py +5 -0
  6. chesscalc/basecore/database.py +66 -0
  7. chesscalc/berkeleydb/__init__.py +5 -0
  8. chesscalc/berkeleydb/database.py +73 -0
  9. chesscalc/berkeleydb/database_du.py +63 -0
  10. chesscalc/calculate.py +79 -0
  11. chesscalc/core/__init__.py +5 -0
  12. chesscalc/core/apply_identities.py +219 -0
  13. chesscalc/core/calculate.py +838 -0
  14. chesscalc/core/configuration.py +24 -0
  15. chesscalc/core/constants.py +98 -0
  16. chesscalc/core/delete_games.py +725 -0
  17. chesscalc/core/eventrecord.py +305 -0
  18. chesscalc/core/export.py +288 -0
  19. chesscalc/core/filespec.py +391 -0
  20. chesscalc/core/gamerecord.py +741 -0
  21. chesscalc/core/identify_event.py +124 -0
  22. chesscalc/core/identify_item.py +406 -0
  23. chesscalc/core/identify_mode.py +108 -0
  24. chesscalc/core/identify_person.py +769 -0
  25. chesscalc/core/identify_playertype.py +107 -0
  26. chesscalc/core/identify_termination.py +108 -0
  27. chesscalc/core/identify_timecontrol.py +101 -0
  28. chesscalc/core/identity.py +285 -0
  29. chesscalc/core/mirror_identities.py +220 -0
  30. chesscalc/core/moderecord.py +265 -0
  31. chesscalc/core/name_lookup.py +236 -0
  32. chesscalc/core/person.py +81 -0
  33. chesscalc/core/pgnheaders.py +138 -0
  34. chesscalc/core/playerrecord.py +444 -0
  35. chesscalc/core/playertyperecord.py +270 -0
  36. chesscalc/core/population.py +179 -0
  37. chesscalc/core/selectorrecord.py +107 -0
  38. chesscalc/core/tab_from_selection.py +176 -0
  39. chesscalc/core/terminationrecord.py +274 -0
  40. chesscalc/core/timecontrolrecord.py +269 -0
  41. chesscalc/core/update_rule.py +155 -0
  42. chesscalc/core/utilities.py +37 -0
  43. chesscalc/db/__init__.py +5 -0
  44. chesscalc/db/database.py +73 -0
  45. chesscalc/db/database_du.py +63 -0
  46. chesscalc/db_tkinter/__init__.py +5 -0
  47. chesscalc/db_tkinter/database.py +61 -0
  48. chesscalc/db_tkinter/database_du.py +45 -0
  49. chesscalc/dpt/__init__.py +5 -0
  50. chesscalc/dpt/database.py +81 -0
  51. chesscalc/dpt/database_du.py +31 -0
  52. chesscalc/dpt/dptnofistat.py +65 -0
  53. chesscalc/gui/__init__.py +5 -0
  54. chesscalc/gui/calculator.py +2728 -0
  55. chesscalc/gui/events.py +386 -0
  56. chesscalc/gui/eventsgrid.py +32 -0
  57. chesscalc/gui/eventspec.py +79 -0
  58. chesscalc/gui/eventsrow.py +77 -0
  59. chesscalc/gui/games.py +119 -0
  60. chesscalc/gui/gamesgrid.py +32 -0
  61. chesscalc/gui/gamesrow.py +126 -0
  62. chesscalc/gui/gridlocator.py +65 -0
  63. chesscalc/gui/help_.py +30 -0
  64. chesscalc/gui/modes.py +269 -0
  65. chesscalc/gui/modesgrid.py +32 -0
  66. chesscalc/gui/modesrow.py +68 -0
  67. chesscalc/gui/performancedu.py +998 -0
  68. chesscalc/gui/persons.py +403 -0
  69. chesscalc/gui/personsgrid.py +32 -0
  70. chesscalc/gui/personsrow.py +94 -0
  71. chesscalc/gui/players.py +330 -0
  72. chesscalc/gui/playersgrid.py +32 -0
  73. chesscalc/gui/playersrow.py +88 -0
  74. chesscalc/gui/playertypes.py +269 -0
  75. chesscalc/gui/playertypesgrid.py +32 -0
  76. chesscalc/gui/playertypesrow.py +67 -0
  77. chesscalc/gui/report.py +56 -0
  78. chesscalc/gui/reportapply.py +457 -0
  79. chesscalc/gui/reportmirror.py +252 -0
  80. chesscalc/gui/reportremovepgn.py +23 -0
  81. chesscalc/gui/rule.py +1489 -0
  82. chesscalc/gui/ruleedit.py +55 -0
  83. chesscalc/gui/ruleinsert.py +38 -0
  84. chesscalc/gui/ruleshow.py +75 -0
  85. chesscalc/gui/selectors.py +70 -0
  86. chesscalc/gui/selectorsgrid.py +32 -0
  87. chesscalc/gui/selectorsrow.py +85 -0
  88. chesscalc/gui/terminations.py +271 -0
  89. chesscalc/gui/terminationsgrid.py +32 -0
  90. chesscalc/gui/terminationsrow.py +68 -0
  91. chesscalc/gui/timecontrols.py +265 -0
  92. chesscalc/gui/timecontrolsgrid.py +32 -0
  93. chesscalc/gui/timecontrolsrow.py +68 -0
  94. chesscalc/help_/__init__.py +30 -0
  95. chesscalc/help_/aboutcalculator.txt +16 -0
  96. chesscalc/help_/calculator.txt +110 -0
  97. chesscalc/legacy/__init__.py +5 -0
  98. chesscalc/legacy/core/__init__.py +5 -0
  99. chesscalc/legacy/core/performances.py +1000 -0
  100. chesscalc/legacy/gui/__init__.py +5 -0
  101. chesscalc/legacy/gui/performance.py +279 -0
  102. chesscalc/legacy/gui/population.py +533 -0
  103. chesscalc/legacy/gui/prediction.py +419 -0
  104. chesscalc/lmdb/__init__.py +5 -0
  105. chesscalc/lmdb/database.py +56 -0
  106. chesscalc/lmdb/database_du.py +64 -0
  107. chesscalc/shared/alldu.py +815 -0
  108. chesscalc/shared/dbdu.py +44 -0
  109. chesscalc/shared/litedu.py +83 -0
  110. chesscalc/shared/rundu.py +93 -0
  111. chesscalc/shared/task.py +41 -0
  112. chesscalc/sqlite/__init__.py +5 -0
  113. chesscalc/sqlite/database.py +50 -0
  114. chesscalc/sqlite/database_du.py +35 -0
  115. chesscalc/unqlite/__init__.py +5 -0
  116. chesscalc/unqlite/database.py +46 -0
  117. chesscalc/unqlite/database_du.py +35 -0
  118. chesscalc/vedis/__init__.py +5 -0
  119. chesscalc/vedis/database.py +46 -0
  120. chesscalc/vedis/database_du.py +35 -0
  121. chesscalc-2.0.0.dist-info/LICENCE +15 -0
  122. chesscalc-2.0.0.dist-info/METADATA +109 -0
  123. chesscalc-2.0.0.dist-info/RECORD +125 -0
  124. chesscalc-2.0.0.dist-info/WHEEL +5 -0
  125. chesscalc-2.0.0.dist-info/top_level.txt +1 -0
chesscalc/__init__.py ADDED
@@ -0,0 +1,71 @@
1
+ # __init__.py
2
+ # Copyright 2008 Roger Marsh
3
+ # Licence: See LICENCE (BSD licence)
4
+
5
+ """Calculate player's performances on a set of games.
6
+
7
+ Performances are calculated by iteration where each player starts with 0 as
8
+ their performance. The scale used is +50 for a win, -50 for a loss, and 0 for
9
+ a draw, relative to the opponents performance. English Chess Federation (ECF)
10
+ grades have the same scale.
11
+
12
+ The performances are not grades because the results of calculations in previous
13
+ runs are not used in this calculation.
14
+
15
+ """
16
+ import os
17
+ import datetime
18
+ import traceback
19
+
20
+ import solentware_base.core.constants as sb_c_constants
21
+
22
+ APPLICATION_NAME = "ChessPerfCalc"
23
+ ERROR_LOG = "ErrorLog"
24
+ REPORT_DIRECTORY = "_reports"
25
+
26
+ # Map database module names to application module
27
+ APPLICATION_DATABASE_MODULE = {
28
+ sb_c_constants.BERKELEYDB_MODULE: __name__ + ".berkeleydb.database",
29
+ sb_c_constants.BSDDB3_MODULE: __name__ + ".db.database",
30
+ sb_c_constants.SQLITE3_MODULE: __name__ + ".sqlite.database",
31
+ sb_c_constants.APSW_MODULE: __name__ + ".apsw.database",
32
+ sb_c_constants.DPT_MODULE: __name__ + ".dpt.database",
33
+ sb_c_constants.UNQLITE_MODULE: __name__ + ".unqlite.database",
34
+ sb_c_constants.VEDIS_MODULE: __name__ + ".vedis.database",
35
+ sb_c_constants.DB_TCL_MODULE: __name__ + ".db_tkinter.database",
36
+ sb_c_constants.LMDB_MODULE: __name__ + ".lmdb.database",
37
+ }
38
+
39
+ del sb_c_constants
40
+
41
+
42
+ def write_error_to_log(directory):
43
+ """Write the exception to the error log with a time stamp.
44
+
45
+ Consider using the ExceptionHandler.report_exception(...) method if
46
+ the write_error_to_log() call is in an instance of a subclass of the
47
+ solentware_bind.gui.exceptionhandler.ExceptionHandler class.
48
+
49
+ """
50
+ with open(
51
+ os.path.join(directory, ERROR_LOG),
52
+ "a",
53
+ encoding="utf-8",
54
+ ) as file:
55
+ file.write(
56
+ "".join(
57
+ (
58
+ "\n\n\n",
59
+ " ".join(
60
+ (
61
+ APPLICATION_NAME,
62
+ "exception report at",
63
+ datetime.datetime.now().isoformat(),
64
+ )
65
+ ),
66
+ "\n\n",
67
+ traceback.format_exc(),
68
+ "\n\n",
69
+ )
70
+ )
71
+ )
@@ -0,0 +1,5 @@
1
+ # __init__.py
2
+ # Copyright 2023 Roger Marsh
3
+ # Licence: See LICENCE (BSD licence)
4
+
5
+ """Identify apsw as a sub-package in chesscalc."""
@@ -0,0 +1,46 @@
1
+ # database.py
2
+ # Copyright 2023 Roger Marsh
3
+ # Licence: See LICENCE (BSD licence)
4
+
5
+ """Chess Performance Calculation database using Sqlite3 via apsw."""
6
+
7
+ from solentware_base import apsw_database
8
+
9
+ from ..core.filespec import FileSpec
10
+ from ..basecore import database
11
+
12
+
13
+ class Database(database.Database, apsw_database.Database):
14
+ """Methods and data structures to create, open, and close database."""
15
+
16
+ _deferred_update_process = "chesscalc.apsw.database_du"
17
+
18
+ def __init__(
19
+ self,
20
+ sqlite3file,
21
+ use_specification_items=None,
22
+ dpt_records=None,
23
+ **kargs,
24
+ ):
25
+ """Define database specification then delegate."""
26
+ names = FileSpec(
27
+ use_specification_items=use_specification_items,
28
+ dpt_records=dpt_records,
29
+ )
30
+
31
+ super().__init__(
32
+ names,
33
+ sqlite3file,
34
+ use_specification_items=use_specification_items,
35
+ **kargs,
36
+ )
37
+
38
+ def delete_database(self):
39
+ """Close and delete the open chess results database."""
40
+ return super().delete_database(
41
+ (
42
+ self.database_file,
43
+ "-".join((self.database_file, "lock")),
44
+ "-".join((self.database_file, "journal")),
45
+ )
46
+ )
@@ -0,0 +1,35 @@
1
+ # database_du.py
2
+ # Copyright 2023 Roger Marsh
3
+ # Licence: See LICENCE (BSD licence)
4
+
5
+ """Chess performance database deferred update using Sqlite 3 via apsw."""
6
+
7
+ from solentware_base import apswdu_database
8
+ from solentware_base import apsw_database
9
+
10
+ from ..shared import litedu
11
+ from ..shared import alldu
12
+
13
+
14
+ class ApswDatabaseduError(Exception):
15
+ """Exception class for apsw.database_du module."""
16
+
17
+
18
+ class Database(alldu.Alldu, litedu.Litedu, apswdu_database.Database):
19
+ """Provide custom deferred update for chess performance database."""
20
+
21
+ def __init__(self, sqlite3file, **kargs):
22
+ """Delegate with ApswDatabaseduError as exception class."""
23
+ super().__init__(sqlite3file, ApswDatabaseduError, **kargs)
24
+
25
+
26
+ class DatabaseSU(alldu.Alldu, litedu.Litedu, apsw_database.Database):
27
+ """Provide custom deferred update for chess performance database."""
28
+
29
+ def __init__(self, sqlite3file, **kargs):
30
+ """Delegate with ApswDatabaseduError as exception class."""
31
+ super().__init__(sqlite3file, ApswDatabaseduError, **kargs)
32
+
33
+ # Class structure implies this is not an override at present.
34
+ def deferred_update_housekeeping(self):
35
+ """Override to do nothing."""
@@ -0,0 +1,5 @@
1
+ # __init__.py
2
+ # Copyright 2023 Roger Marsh
3
+ # Licence: See LICENCE (BSD licence)
4
+
5
+ """Identify basecore as a sub-package in chesscalc."""
@@ -0,0 +1,66 @@
1
+ # database.py
2
+ # Copyright 2023 Roger Marsh
3
+ # Licence: See LICENCE (BSD licence)
4
+
5
+ """Database methods common to all database engine interfaces."""
6
+
7
+ import os
8
+ import shutil
9
+
10
+ from .. import APPLICATION_NAME, ERROR_LOG, REPORT_DIRECTORY
11
+
12
+
13
+ class Database:
14
+ """Provide methods common to all database engine interfaces."""
15
+
16
+ # Non *_du database actions are done in separate threads by default,
17
+ # not the main thread. Override with 'can_use_thread = False' in
18
+ # subclasses if necessary.
19
+ # For SQLite3 the apsw interface is happy with the default, but the
20
+ # sqlite3 interface gives a sqlite3.programmingerror exception.
21
+ # For Berkeley DB the bsddb3 and berkeleydb interfaces are happy with
22
+ # the default, but the tcl via tkinter interface gives a RuntimeError
23
+ # exception.
24
+ can_use_thread = True
25
+
26
+ def use_deferred_update_process(self):
27
+ """Return path to deferred update module."""
28
+ return self._deferred_update_process
29
+
30
+ def open_database(self, files=None):
31
+ """Return '' to fit behaviour of dpt version of this method."""
32
+ super().open_database(files=files)
33
+ return ""
34
+
35
+ def delete_database(self, names):
36
+ """Delete database and return message about items not deleted."""
37
+ listnames = set(n for n in os.listdir(self.home_directory))
38
+ homenames = set(n for n in names if os.path.basename(n) in listnames)
39
+ for name in (ERROR_LOG, REPORT_DIRECTORY):
40
+ if name in listnames:
41
+ homenames.add(name)
42
+ if len(listnames - set(os.path.basename(h) for h in homenames)):
43
+ message = "".join(
44
+ (
45
+ "There is at least one file or folder in\n\n",
46
+ self.home_directory,
47
+ "\n\nwhich may not be part of the database. These items ",
48
+ "have not been deleted by ",
49
+ APPLICATION_NAME,
50
+ ".",
51
+ )
52
+ )
53
+ else:
54
+ message = None
55
+ self.close_database()
56
+ for hnm in homenames:
57
+ hnm = os.path.join(self.home_directory, hnm)
58
+ if os.path.isdir(hnm):
59
+ shutil.rmtree(hnm, ignore_errors=True)
60
+ else:
61
+ os.remove(hnm)
62
+ try:
63
+ os.rmdir(self.home_directory)
64
+ except (FileNotFoundError, OSError):
65
+ pass
66
+ return message
@@ -0,0 +1,5 @@
1
+ # __init__.py
2
+ # Copyright 2023 Roger Marsh
3
+ # Licence: See LICENCE (BSD licence)
4
+
5
+ """Identify berkeleydb as a sub-package in chesscalc."""
@@ -0,0 +1,73 @@
1
+ # database.py
2
+ # Copyright 2023 Roger Marsh
3
+ # Licence: See LICENCE (BSD licence)
4
+
5
+ """Chess Performance Calculation database using Berkeley DB via berkeleydb."""
6
+
7
+ from berkeleydb.db import (
8
+ # DB_BTREE,
9
+ # DB_HASH,
10
+ # DB_RECNO,
11
+ # DB_DUPSORT,
12
+ DB_CREATE,
13
+ DB_RECOVER,
14
+ DB_INIT_MPOOL,
15
+ DB_INIT_LOCK,
16
+ DB_INIT_LOG,
17
+ DB_INIT_TXN,
18
+ DB_PRIVATE,
19
+ )
20
+
21
+ from solentware_base import berkeleydb_database
22
+
23
+ from ..core.filespec import FileSpec
24
+ from ..basecore import database
25
+
26
+
27
+ class Database(database.Database, berkeleydb_database.Database):
28
+ """Methods and data structures to create, open, and close database."""
29
+
30
+ _deferred_update_process = "chesscalc.berkeleydb.database_du"
31
+
32
+ def __init__(
33
+ self,
34
+ DBfile,
35
+ use_specification_items=None,
36
+ dpt_records=None,
37
+ **kargs,
38
+ ):
39
+ """Define database specification and environment then delegate."""
40
+ dbnames = FileSpec(
41
+ use_specification_items=use_specification_items,
42
+ dpt_records=dpt_records,
43
+ )
44
+
45
+ environment = {
46
+ "flags": (
47
+ DB_CREATE
48
+ | DB_RECOVER
49
+ | DB_INIT_MPOOL
50
+ | DB_INIT_LOCK
51
+ | DB_INIT_LOG
52
+ | DB_INIT_TXN
53
+ | DB_PRIVATE
54
+ ),
55
+ }
56
+
57
+ super().__init__(
58
+ dbnames,
59
+ DBfile,
60
+ environment,
61
+ use_specification_items=use_specification_items,
62
+ **kargs,
63
+ )
64
+
65
+ def delete_database(self):
66
+ """Close and delete the open chess results database."""
67
+ return super().delete_database(
68
+ (
69
+ self.database_file,
70
+ "-".join((self.database_file, "lock")),
71
+ self.dbenv.get_lg_dir().decode(),
72
+ )
73
+ )
@@ -0,0 +1,63 @@
1
+ # database_du.py
2
+ # Copyright 2023 Roger Marsh
3
+ # Licence: See LICENCE (BSD licence)
4
+
5
+ """Chess performance deferred update using Berkeley DB via berkeleydb."""
6
+
7
+ import berkeleydb.db
8
+
9
+ from solentware_base import berkeleydbdu_database
10
+ from solentware_base import berkeleydb_database
11
+
12
+ from ..shared import dbdu
13
+ from ..shared import alldu
14
+
15
+
16
+ class BerkeleydbDatabaseduError(Exception):
17
+ """Exception class for berkeleydb.database_du module."""
18
+
19
+
20
+ class Database(alldu.Alldu, dbdu.Dbdu, berkeleydbdu_database.Database):
21
+ """Provide custom deferred update for chess performance database."""
22
+
23
+ def __init__(self, DBfile, **kargs):
24
+ """Delegate with BerkeleydbDatabaseduError as exception class."""
25
+ super().__init__(
26
+ DBfile,
27
+ BerkeleydbDatabaseduError,
28
+ (
29
+ berkeleydb.db.DB_CREATE
30
+ | berkeleydb.db.DB_RECOVER
31
+ | berkeleydb.db.DB_INIT_MPOOL
32
+ | berkeleydb.db.DB_INIT_LOCK
33
+ | berkeleydb.db.DB_INIT_LOG
34
+ | berkeleydb.db.DB_INIT_TXN
35
+ | berkeleydb.db.DB_PRIVATE
36
+ ),
37
+ **kargs
38
+ )
39
+
40
+
41
+ class DatabaseSU(alldu.Alldu, dbdu.Dbdu, berkeleydb_database.Database):
42
+ """Provide custom deferred update for chess performance database."""
43
+
44
+ def __init__(self, DBfile, **kargs):
45
+ """Delegate with BerkeleydbDatabaseduError as exception class."""
46
+ super().__init__(
47
+ DBfile,
48
+ BerkeleydbDatabaseduError,
49
+ (
50
+ berkeleydb.db.DB_CREATE
51
+ | berkeleydb.db.DB_RECOVER
52
+ | berkeleydb.db.DB_INIT_MPOOL
53
+ | berkeleydb.db.DB_INIT_LOCK
54
+ | berkeleydb.db.DB_INIT_LOG
55
+ | berkeleydb.db.DB_INIT_TXN
56
+ | berkeleydb.db.DB_PRIVATE
57
+ ),
58
+ **kargs
59
+ )
60
+
61
+ # Class structure implies this is not an override at present.
62
+ def deferred_update_housekeeping(self):
63
+ """Override to do nothing."""
chesscalc/calculate.py ADDED
@@ -0,0 +1,79 @@
1
+ # calculate.py
2
+ # Copyright 2012 Roger Marsh
3
+ # Licence: See LICENCE (BSD licence)
4
+
5
+ """Calculate performances from a file of chess game results."""
6
+
7
+ if __name__ == "__main__":
8
+ import multiprocessing
9
+
10
+ from . import APPLICATION_NAME
11
+
12
+ multiprocessing.set_start_method("spawn")
13
+ del multiprocessing
14
+
15
+ try:
16
+ from solentware_misc.gui.startstop import (
17
+ start_application_exception,
18
+ stop_application,
19
+ application_exception,
20
+ )
21
+ except Exception as error:
22
+ import tkinter.messagebox
23
+
24
+ try:
25
+ tkinter.messagebox.showerror(
26
+ title="Start Exception",
27
+ message=".\n\nThe reported exception is:\n\n".join(
28
+ (
29
+ " ".join(
30
+ (
31
+ "Unable to import",
32
+ "solentware_misc.gui.startstop module",
33
+ )
34
+ ),
35
+ str(error),
36
+ )
37
+ ),
38
+ )
39
+ except tkinter.TclError:
40
+ pass
41
+ raise SystemExit(
42
+ "Unable to import start application utilities"
43
+ ) from error
44
+ try:
45
+ from .gui.calculator import Calculator
46
+ except Exception as error:
47
+ start_application_exception(
48
+ error, appname=APPLICATION_NAME, action="import"
49
+ )
50
+ raise SystemExit(
51
+ " import ".join(("Unable to", APPLICATION_NAME))
52
+ ) from error
53
+ try:
54
+ app = Calculator()
55
+ except Exception as error:
56
+ start_application_exception(
57
+ error, appname=APPLICATION_NAME, action="initialise"
58
+ )
59
+ raise SystemExit(
60
+ " initialise ".join(("Unable to", APPLICATION_NAME))
61
+ ) from error
62
+ try:
63
+ app.widget.mainloop()
64
+ except SystemExit:
65
+ stop_application(app, app.widget)
66
+ raise
67
+ except Exception as error:
68
+ application_exception(
69
+ error,
70
+ app,
71
+ app.widget,
72
+ title="Chess Performace Calculator",
73
+ appname=APPLICATION_NAME,
74
+ )
75
+ raise SystemExit(
76
+ " reporting exception in ".join(
77
+ ("Exception while", APPLICATION_NAME)
78
+ )
79
+ ) from error
@@ -0,0 +1,5 @@
1
+ # __init__.py
2
+ # Copyright 2009 Roger Marsh
3
+ # Licence: See LICENCE (BSD licence)
4
+
5
+ """Identify core as a sub-package in chesscalc."""
@@ -0,0 +1,219 @@
1
+ # apply_identities.py
2
+ # Copyright 2024 Roger Marsh
3
+ # Licence: See LICENCE (BSD licence)
4
+
5
+ """This module provides functions to verify and apply player identities.
6
+
7
+ Identities exported from a performance calculation database are verified
8
+ for consistency and applied to a database if consistent.
9
+
10
+ """
11
+
12
+ from . import playerrecord
13
+ from . import filespec
14
+
15
+
16
+ class _ApplyIdentitiesReport(list):
17
+ """Report the outcome of verifying and applying player identities."""
18
+
19
+ def __init__(self):
20
+ """Define the initial report which also means no problems found."""
21
+ self.messages_exist = False
22
+
23
+ def append(self, identity):
24
+ """Delegate and note if identity has a message."""
25
+ super().append(identity)
26
+ if identity.message is not None:
27
+ self.messages_exist = True
28
+
29
+
30
+ class _NameStatus:
31
+ """Classify names by representation on database.
32
+
33
+ A name may: not exist, be new, be identified as primary, or an alias
34
+ of a primary.
35
+ """
36
+
37
+ def __init__(self, names):
38
+ """Initialise buckets for names."""
39
+ self.names = names
40
+ self.not_on_database = set()
41
+ self.not_identified = {}
42
+ self.identified_primary = {}
43
+ self.identified_alias = {}
44
+ self.chosen_name = None
45
+ self.message = None
46
+
47
+ def classify_names(self, database):
48
+ """Put each name in the appropriate bucket."""
49
+ not_on_database = self.not_on_database
50
+ not_identified = self.not_identified
51
+ identified_primary = self.identified_primary
52
+ identified_alias = self.identified_alias
53
+ value = playerrecord.PersonValue()
54
+ person_record = playerrecord.PlayerDBrecord(
55
+ valueclass=playerrecord.PersonDBvalue
56
+ )
57
+ encode_record_selector = database.encode_record_selector
58
+ for name in self.names:
59
+ value.load_alias_index_key(repr(name))
60
+ personlist = database.recordlist_key(
61
+ filespec.PLAYER_FILE_DEF,
62
+ filespec.PLAYER_ALIAS_FIELD_DEF,
63
+ key=encode_record_selector(value.alias_index_key()),
64
+ )
65
+ count = personlist.count_records()
66
+ if count > 1:
67
+ self.message = "Player by name record is not unique."
68
+ return
69
+ record = personlist.create_recordsetbase_cursor(
70
+ internalcursor=True
71
+ ).first()
72
+ if record:
73
+ not_identified[name] = record
74
+ continue
75
+ personlist = database.recordlist_key(
76
+ filespec.PLAYER_FILE_DEF,
77
+ filespec.PERSON_ALIAS_FIELD_DEF,
78
+ key=encode_record_selector(value.alias_index_key()),
79
+ )
80
+ count = personlist.count_records()
81
+ if count > 1:
82
+ self.message = "Person by name record is not unique."
83
+ return
84
+ record = personlist.create_recordsetbase_cursor(
85
+ internalcursor=True
86
+ ).first()
87
+ if record:
88
+ person_record.load_record(record)
89
+ alias = person_record.value.alias
90
+ if alias == person_record.value.identity:
91
+ if alias not in identified_primary:
92
+ identified_primary[alias] = {name: record}
93
+ else:
94
+ identified_primary[alias][name] = record
95
+ continue
96
+ if alias not in identified_alias:
97
+ identified_alias[alias] = {name: record}
98
+ else:
99
+ identified_alias[alias][name] = record
100
+ continue
101
+ not_on_database.add(name)
102
+
103
+ def identify_names(self, database):
104
+ """Make each not identified name an alias of an identified name.
105
+
106
+ The name in identified_primary or derived from identified_alias
107
+ will be the identified name if available.
108
+
109
+ Otherwise one of the names in not_identified is picked and noted
110
+ in chosen_name.
111
+
112
+ """
113
+ value = playerrecord.PersonValue()
114
+ packer = playerrecord.PlayerDBrecord(
115
+ valueclass=playerrecord.PersonDBvalue
116
+ )
117
+ player_record = playerrecord.PlayerDBrecord()
118
+ person_record = playerrecord.PlayerDBrecord(
119
+ valueclass=playerrecord.PersonDBvalue
120
+ )
121
+ encode_record_selector = database.encode_record_selector
122
+ names = self.not_identified.copy()
123
+ if len(self.identified_primary):
124
+ chosen_alias, name = list(self.identified_primary.items())[0]
125
+ record = list(name.values())[0]
126
+ elif len(self.identified_alias):
127
+ chosen_alias, name = list(self.identified_alias.items())[0]
128
+ record = list(name.values())[0]
129
+ else:
130
+ chosen_name, record = names.popitem()
131
+ player_record.load_record(record)
132
+ chosen_alias = player_record.value.alias
133
+ self.chosen_name = chosen_name
134
+ person_record.load_record(record)
135
+
136
+ # None is safe because self.srkey == new_record.srkey.
137
+ # filespec.PLAYER_ALIAS_FIELD_DEF is correct value otherwise
138
+ # because of how argument is used in edit_record().
139
+ player_record.edit_record(
140
+ database, filespec.PLAYER_FILE_DEF, None, person_record
141
+ )
142
+
143
+ packer.load_record(record)
144
+ person = packer.value.alias_index_key()
145
+ persongames = database.recordlist_key(
146
+ filespec.GAME_FILE_DEF,
147
+ filespec.GAME_PLAYER_FIELD_DEF,
148
+ key=encode_record_selector(person),
149
+ )
150
+
151
+ while len(names):
152
+ name, record = names.popitem()
153
+ # value should be PlayerDBvalue but while PersonDBvalue gives
154
+ # the same answer there is no need to change it.
155
+ value.load_alias_index_key(repr(name))
156
+ player_record.load_record(record)
157
+ person_record.load_record(record)
158
+ person_record.value.alias = chosen_alias
159
+
160
+ # None is safe because self.srkey == new_record.srkey.
161
+ # filespec.PLAYER_ALIAS_FIELD_DEF is correct value otherwise
162
+ # because of how argument is used in edit_record().
163
+ player_record.edit_record(
164
+ database, filespec.PLAYER_FILE_DEF, None, person_record
165
+ )
166
+
167
+ persongames |= database.recordlist_key(
168
+ filespec.GAME_FILE_DEF,
169
+ filespec.GAME_PLAYER_FIELD_DEF,
170
+ key=encode_record_selector(value.alias_index_key()),
171
+ )
172
+ database.file_records_under(
173
+ filespec.GAME_FILE_DEF,
174
+ filespec.GAME_PERSON_FIELD_DEF,
175
+ persongames,
176
+ encode_record_selector(person),
177
+ )
178
+
179
+
180
+ def verify_and_apply_identities(database, identities):
181
+ """Return None if identities applied or a message for display if not."""
182
+ if not database:
183
+ return None
184
+ if not identities:
185
+ return None
186
+ commit_flag = True
187
+ report = _ApplyIdentitiesReport()
188
+ database.start_transaction()
189
+ try:
190
+ for identity in identities:
191
+ name = _NameStatus(identity)
192
+ report.append(name)
193
+ name.classify_names(database)
194
+ if name.message is not None:
195
+ commit_flag = False
196
+ continue
197
+ if (
198
+ len(set(name.identified_primary).union(name.identified_alias))
199
+ > 1
200
+ ):
201
+ name.message = "More than one database person is referenced."
202
+ report.messages_exist = True
203
+ commit_flag = False
204
+ continue
205
+ if not commit_flag:
206
+ return report
207
+ for identity in report:
208
+ if not identity.not_identified:
209
+ continue
210
+ identity.identify_names(database)
211
+ # For testing report what has been found.
212
+ # if commit_flag:
213
+ # commit_flag = False
214
+ finally:
215
+ if commit_flag:
216
+ database.commit()
217
+ else:
218
+ database.backout()
219
+ return report