draftomen 0.3.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 (59) hide show
  1. draftomen/__init__.py +17 -0
  2. draftomen/assets/draftomen.icns +0 -0
  3. draftomen/assets/draftomen.ico +0 -0
  4. draftomen/assets/draftomen_logo.png +0 -0
  5. draftomen/audit.py +606 -0
  6. draftomen/backtest.py +449 -0
  7. draftomen/benchmark.py +834 -0
  8. draftomen/carddb.py +1869 -0
  9. draftomen/cardimages.py +313 -0
  10. draftomen/cli.py +891 -0
  11. draftomen/config.py +133 -0
  12. draftomen/deckbuilder.py +2723 -0
  13. draftomen/events.py +772 -0
  14. draftomen/logfollow.py +451 -0
  15. draftomen/mock_session.py +745 -0
  16. draftomen/paths.py +120 -0
  17. draftomen/pickengine.py +1117 -0
  18. draftomen/pool.py +1259 -0
  19. draftomen/preferences.py +289 -0
  20. draftomen/qml/AboutDialog.qml +156 -0
  21. draftomen/qml/AppBar.qml +120 -0
  22. draftomen/qml/BacktestView.qml +316 -0
  23. draftomen/qml/BuildView.qml +1151 -0
  24. draftomen/qml/CardPreview.qml +434 -0
  25. draftomen/qml/DimensionalButton.qml +42 -0
  26. draftomen/qml/DimensionalComboBox.qml +182 -0
  27. draftomen/qml/DimensionalSurface.qml +118 -0
  28. draftomen/qml/DimensionalTabButton.qml +41 -0
  29. draftomen/qml/LiveDraftView.qml +468 -0
  30. draftomen/qml/Main.qml +175 -0
  31. draftomen/qml/NavigationRail.qml +226 -0
  32. draftomen/qml/PoolSummaryPanel.qml +322 -0
  33. draftomen/qml/PrivacyDialog.qml +96 -0
  34. draftomen/qml/RecentPickThumbnail.qml +83 -0
  35. draftomen/qml/RecentPicksGallery.qml +333 -0
  36. draftomen/qml/RecommendationRow.qml +404 -0
  37. draftomen/qml/SettingsSwitch.qml +141 -0
  38. draftomen/qml/SettingsView.qml +531 -0
  39. draftomen/qml/StateBanner.qml +189 -0
  40. draftomen/qml/StatusStrip.qml +84 -0
  41. draftomen/qml/Theme.qml +71 -0
  42. draftomen/qml/qmldir +23 -0
  43. draftomen/qt_adapter.py +884 -0
  44. draftomen/qt_gui.py +338 -0
  45. draftomen/qt_mock.py +63 -0
  46. draftomen/ranking.py +126 -0
  47. draftomen/replay.py +480 -0
  48. draftomen/session.py +3668 -0
  49. draftomen/setinfo.py +26 -0
  50. draftomen/seventeen.py +2766 -0
  51. draftomen/splash.py +617 -0
  52. draftomen/tui.py +4007 -0
  53. draftomen/watch.py +336 -0
  54. draftomen-0.3.0.dist-info/METADATA +156 -0
  55. draftomen-0.3.0.dist-info/RECORD +59 -0
  56. draftomen-0.3.0.dist-info/WHEEL +5 -0
  57. draftomen-0.3.0.dist-info/entry_points.txt +6 -0
  58. draftomen-0.3.0.dist-info/licenses/LICENSE +21 -0
  59. draftomen-0.3.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,289 @@
1
+ """Persisted user settings for Draftomen interfaces.
2
+ Keep optional behavior and layout choices stable across app restarts.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ import tempfile
9
+ from dataclasses import asdict, dataclass
10
+ from os import PathLike
11
+ from pathlib import Path
12
+ from typing import Literal, TypeAlias
13
+
14
+ from draftomen.paths import app_data_dir
15
+
16
+ PathInput: TypeAlias = str | PathLike[str]
17
+ CardImagePreviewMode: TypeAlias = Literal["auto", "show", "hide"]
18
+
19
+ TUI_PREFERENCES_FILE_NAME = "tui-preferences.json"
20
+ TUI_PREFERENCES_SCHEMA_VERSION = 1
21
+ CARD_IMAGE_PREVIEW_MODES = frozenset({"auto", "show", "hide"})
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class TuiVisibilityPreferences:
26
+ """User-controlled Textual behavior and optional interface elements.
27
+ Missing settings use safe defaults so existing files remain compatible.
28
+ """
29
+
30
+ secondary_columns: bool = True
31
+ build_details: bool = False
32
+ splash_enabled: bool = True
33
+ pool_metadata: bool = True
34
+ pool_color_distribution: bool = True
35
+ pool_mana_curve: bool = True
36
+ account_identifier: bool = True
37
+ draft_identifier: bool = True
38
+ mana_pips_and_sources: bool = True
39
+ attribution: bool = True
40
+ focused_card_details: bool = True
41
+ card_image_preview: CardImagePreviewMode = "auto"
42
+
43
+
44
+ def tui_preferences_path(*, app_dir: PathInput | None = None) -> Path:
45
+ """Return the path used for persisted TUI visibility preferences.
46
+ The file lives directly in Draftomen's per-user application directory.
47
+ """
48
+
49
+ root = Path(app_data_dir() if app_dir is None else app_dir).expanduser()
50
+ return root / TUI_PREFERENCES_FILE_NAME
51
+
52
+
53
+ def load_tui_preferences(
54
+ *,
55
+ app_dir: PathInput | None = None,
56
+ ) -> tuple[TuiVisibilityPreferences, str | None]:
57
+ """Load persisted visibility preferences, falling back safely to defaults.
58
+ A warning describes malformed or unsupported configuration without blocking startup.
59
+ """
60
+
61
+ try:
62
+ path = tui_preferences_path(app_dir=app_dir)
63
+ raw_text = path.read_text(encoding="utf-8")
64
+ except FileNotFoundError:
65
+ return TuiVisibilityPreferences(), None
66
+ except (OSError, UnicodeDecodeError) as error:
67
+ return TuiVisibilityPreferences(), f"Could not load TUI preferences: {error}"
68
+
69
+ try:
70
+ raw_data = json.loads(raw_text)
71
+ except json.JSONDecodeError as error:
72
+ return TuiVisibilityPreferences(), f"Could not load TUI preferences: {error}"
73
+
74
+ if not isinstance(raw_data, dict):
75
+ return (
76
+ TuiVisibilityPreferences(),
77
+ "Could not load TUI preferences: expected an object.",
78
+ )
79
+
80
+ version = raw_data.get("version")
81
+ if version != TUI_PREFERENCES_SCHEMA_VERSION:
82
+ return (
83
+ TuiVisibilityPreferences(),
84
+ "Could not load TUI preferences: unsupported settings version.",
85
+ )
86
+
87
+ visibility = raw_data.get("visibility")
88
+ if not isinstance(visibility, dict):
89
+ return (
90
+ TuiVisibilityPreferences(),
91
+ "Could not load TUI preferences: expected a visibility object.",
92
+ )
93
+
94
+ defaults = TuiVisibilityPreferences()
95
+ values = asdict(defaults)
96
+ invalid_fields: list[str] = []
97
+ for field_name, default_value in values.items():
98
+ value = visibility.get(field_name, default_value)
99
+ if field_name == "card_image_preview":
100
+ if not isinstance(value, str) or value not in CARD_IMAGE_PREVIEW_MODES:
101
+ invalid_fields.append(field_name)
102
+ continue
103
+ elif type(value) is not bool:
104
+ invalid_fields.append(field_name)
105
+ continue
106
+
107
+ values[field_name] = value
108
+
109
+ preferences = TuiVisibilityPreferences(**values)
110
+ if not invalid_fields:
111
+ return preferences, None
112
+
113
+ fields = ", ".join(invalid_fields)
114
+ return preferences, f"TUI preferences used defaults for invalid fields: {fields}."
115
+
116
+
117
+ def save_tui_preferences(
118
+ *,
119
+ preferences: TuiVisibilityPreferences,
120
+ app_dir: PathInput | None = None,
121
+ ) -> str | None:
122
+ """Atomically save visibility preferences and return a non-fatal error message.
123
+ The active TUI can continue using preferences when persistence fails.
124
+ """
125
+
126
+ path = tui_preferences_path(app_dir=app_dir)
127
+ temporary_path: Path | None = None
128
+ try:
129
+ path.parent.mkdir(parents=True, exist_ok=True)
130
+ with tempfile.NamedTemporaryFile(
131
+ mode="w",
132
+ encoding="utf-8",
133
+ dir=path.parent,
134
+ delete=False,
135
+ ) as temporary_file:
136
+ json.dump(
137
+ {
138
+ "version": TUI_PREFERENCES_SCHEMA_VERSION,
139
+ "visibility": asdict(preferences),
140
+ },
141
+ temporary_file,
142
+ indent=2,
143
+ sort_keys=True,
144
+ )
145
+ temporary_file.write("\n")
146
+ temporary_path = Path(temporary_file.name)
147
+
148
+ temporary_path.replace(path)
149
+ except OSError as error:
150
+ return f"Could not save TUI preferences: {error}"
151
+ finally:
152
+ if temporary_path is not None:
153
+ try:
154
+ temporary_path.unlink()
155
+ except FileNotFoundError:
156
+ pass
157
+ except OSError:
158
+ pass
159
+
160
+ return None
161
+
162
+
163
+ GUI_PREFERENCES_FILE_NAME = "gui-preferences.json"
164
+ GUI_PREFERENCES_SCHEMA_VERSION = 1
165
+
166
+
167
+ @dataclass(frozen=True)
168
+ class GuiDisplayPreferences:
169
+ """User-controlled desktop display choices independent from the live session.
170
+ Functional draft choices remain owned by explicit live-session commands.
171
+ """
172
+
173
+ compact_density: bool = False
174
+ secondary_stats: bool = True
175
+ card_preview: bool = True
176
+ detailed_build_context: bool = True
177
+ system_text_scaling: bool = True
178
+ show_backtest: bool = False
179
+
180
+
181
+ def gui_preferences_path(*, app_dir: PathInput | None = None) -> Path:
182
+ """Return the path used for persisted desktop display preferences.
183
+ The file lives directly in Draftomen's per-user application directory.
184
+ """
185
+
186
+ root = Path(app_data_dir() if app_dir is None else app_dir).expanduser()
187
+ return root / GUI_PREFERENCES_FILE_NAME
188
+
189
+
190
+ def load_gui_preferences(
191
+ *,
192
+ app_dir: PathInput | None = None,
193
+ ) -> tuple[GuiDisplayPreferences, str | None]:
194
+ """Load desktop display preferences, falling back safely to defaults.
195
+ A warning describes malformed or unsupported configuration without blocking startup.
196
+ """
197
+
198
+ try:
199
+ path = gui_preferences_path(app_dir=app_dir)
200
+ raw_text = path.read_text(encoding="utf-8")
201
+ except FileNotFoundError:
202
+ return GuiDisplayPreferences(), None
203
+ except (OSError, UnicodeDecodeError) as error:
204
+ return GuiDisplayPreferences(), f"Could not load GUI preferences: {error}"
205
+
206
+ try:
207
+ raw_data = json.loads(raw_text)
208
+ except json.JSONDecodeError as error:
209
+ return GuiDisplayPreferences(), f"Could not load GUI preferences: {error}"
210
+
211
+ if not isinstance(raw_data, dict):
212
+ return GuiDisplayPreferences(), "Could not load GUI preferences: expected an object."
213
+ if raw_data.get("version") != GUI_PREFERENCES_SCHEMA_VERSION:
214
+ return (
215
+ GuiDisplayPreferences(),
216
+ "Could not load GUI preferences: unsupported settings version.",
217
+ )
218
+
219
+ display = raw_data.get("display")
220
+ if not isinstance(display, dict):
221
+ return (
222
+ GuiDisplayPreferences(),
223
+ "Could not load GUI preferences: expected a display object.",
224
+ )
225
+
226
+ defaults = GuiDisplayPreferences()
227
+ values = asdict(defaults)
228
+ invalid_fields: list[str] = []
229
+ for field_name, default_value in values.items():
230
+ value = display.get(field_name, default_value)
231
+ if type(value) is not bool:
232
+ invalid_fields.append(field_name)
233
+ continue
234
+ values[field_name] = value
235
+
236
+ preferences = GuiDisplayPreferences(**values)
237
+ if not invalid_fields:
238
+ return preferences, None
239
+ return (
240
+ preferences,
241
+ "GUI preferences used defaults for invalid fields: "
242
+ + ", ".join(invalid_fields)
243
+ + ".",
244
+ )
245
+
246
+
247
+ def save_gui_preferences(
248
+ *,
249
+ preferences: GuiDisplayPreferences,
250
+ app_dir: PathInput | None = None,
251
+ ) -> str | None:
252
+ """Atomically save desktop display preferences and return a non-fatal error.
253
+ The active GUI can continue using a changed preference when persistence fails.
254
+ """
255
+
256
+ path = gui_preferences_path(app_dir=app_dir)
257
+ temporary_path: Path | None = None
258
+ try:
259
+ path.parent.mkdir(parents=True, exist_ok=True)
260
+ with tempfile.NamedTemporaryFile(
261
+ mode="w",
262
+ encoding="utf-8",
263
+ dir=path.parent,
264
+ delete=False,
265
+ ) as temporary_file:
266
+ json.dump(
267
+ {
268
+ "version": GUI_PREFERENCES_SCHEMA_VERSION,
269
+ "display": asdict(preferences),
270
+ },
271
+ temporary_file,
272
+ indent=2,
273
+ sort_keys=True,
274
+ )
275
+ temporary_file.write("\n")
276
+ temporary_path = Path(temporary_file.name)
277
+ temporary_path.replace(path)
278
+ except OSError as error:
279
+ return f"Could not save GUI preferences: {error}"
280
+ finally:
281
+ if temporary_path is not None:
282
+ try:
283
+ temporary_path.unlink()
284
+ except FileNotFoundError:
285
+ pass
286
+ except OSError:
287
+ pass
288
+
289
+ return None
@@ -0,0 +1,156 @@
1
+ import QtQuick 2.15
2
+ import QtQuick.Controls 2.15
3
+ import QtQuick.Layouts 1.15
4
+
5
+ Dialog {
6
+ id: root
7
+
8
+ required property string applicationVersion
9
+ property var returnFocusItem: null
10
+ readonly property string projectWebsite: "https://www.draftomen.com"
11
+ readonly property string projectRepository: "https://github.com/andreagrandi/draftomen"
12
+
13
+ objectName: "aboutDialog"
14
+ parent: Overlay.overlay
15
+ modal: true
16
+ focus: true
17
+ title: "About Draft Omen"
18
+ width: Math.min(460, Math.max(300, parent ? parent.width - 32 : 460))
19
+ x: parent ? Math.max(16, Math.round((parent.width - width) / 2)) : 16
20
+ y: parent ? Math.max(16, Math.round((parent.height - height) / 2)) : 16
21
+ padding: 16
22
+
23
+ Overlay.modal: Rectangle {
24
+ color: "#99000000"
25
+ }
26
+
27
+ background: Rectangle {
28
+ color: Theme.surface
29
+ border.color: Theme.outline
30
+ border.width: 1
31
+ radius: Theme.radius
32
+ }
33
+
34
+ header: Rectangle {
35
+ implicitHeight: 52
36
+ color: Theme.surfaceHigh
37
+ border.color: Theme.outline
38
+ border.width: 1
39
+
40
+ Label {
41
+ anchors.fill: parent
42
+ anchors.leftMargin: 16
43
+ anchors.rightMargin: 16
44
+ text: root.title
45
+ color: Theme.text
46
+ font.pixelSize: Theme.textPixelSize(18)
47
+ font.bold: true
48
+ verticalAlignment: Text.AlignVCenter
49
+ }
50
+ }
51
+
52
+ onClosed: {
53
+ const opener = root.returnFocusItem
54
+ root.returnFocusItem = null
55
+ if (opener && opener.visible && opener.enabled)
56
+ opener.forceActiveFocus()
57
+ }
58
+
59
+ contentItem: ColumnLayout {
60
+ spacing: 12
61
+ implicitWidth: 360
62
+
63
+ Label {
64
+ objectName: "aboutDialogTitle"
65
+ Layout.fillWidth: true
66
+ text: "Draft Omen"
67
+ color: Theme.text
68
+ font.pixelSize: Theme.textPixelSize(24)
69
+ font.bold: true
70
+ horizontalAlignment: Text.AlignHCenter
71
+ Accessible.name: text
72
+ }
73
+
74
+ Image {
75
+ objectName: "aboutDialogLogo"
76
+ Layout.alignment: Qt.AlignHCenter
77
+ Layout.preferredWidth: 180
78
+ Layout.preferredHeight: 180
79
+ source: "../assets/draftomen_logo.png"
80
+ sourceSize.width: 360
81
+ sourceSize.height: 360
82
+ fillMode: Image.PreserveAspectFit
83
+ smooth: true
84
+ mipmap: true
85
+ Accessible.name: "Draft Omen logo"
86
+ }
87
+
88
+ Label {
89
+ objectName: "aboutDialogVersion"
90
+ Layout.fillWidth: true
91
+ text: "Version " + root.applicationVersion
92
+ color: Theme.text
93
+ horizontalAlignment: Text.AlignHCenter
94
+ Accessible.name: "Draft Omen version " + root.applicationVersion
95
+ }
96
+
97
+ Label {
98
+ objectName: "aboutDialogAuthor"
99
+ Layout.fillWidth: true
100
+ text: "Made with ❤️ by Andrea Grandi"
101
+ color: Theme.text
102
+ horizontalAlignment: Text.AlignHCenter
103
+ Accessible.name: text
104
+ }
105
+
106
+ Label {
107
+ objectName: "aboutDialogLicense"
108
+ Layout.fillWidth: true
109
+ text: "Draft Omen is licensed under the MIT License"
110
+ color: Theme.text
111
+ horizontalAlignment: Text.AlignHCenter
112
+ Accessible.name: text
113
+ }
114
+
115
+ DimensionalButton {
116
+ id: websiteButton
117
+ objectName: "aboutDialogWebsite"
118
+ Layout.alignment: Qt.AlignHCenter
119
+ text: "Project website"
120
+ implicitWidth: 140
121
+ Accessible.name: "Open Draft Omen project website"
122
+ Accessible.description: root.projectWebsite
123
+ onClicked: Qt.openUrlExternally(root.projectWebsite)
124
+ }
125
+
126
+ DimensionalButton {
127
+ objectName: "aboutDialogRepository"
128
+ Layout.alignment: Qt.AlignHCenter
129
+ text: "GitHub repository"
130
+ implicitWidth: 160
131
+ Accessible.name: "Open Draft Omen GitHub repository"
132
+ Accessible.description: root.projectRepository
133
+ onClicked: Qt.openUrlExternally(root.projectRepository)
134
+ }
135
+ }
136
+
137
+ footer: DialogButtonBox {
138
+ implicitHeight: 58
139
+ alignment: Qt.AlignRight
140
+ background: Rectangle {
141
+ color: Theme.surfaceHigh
142
+ border.color: Theme.outline
143
+ border.width: 1
144
+ }
145
+
146
+ DimensionalButton {
147
+ id: closeButton
148
+ objectName: "aboutDialogCloseButton"
149
+ text: "Close"
150
+ accented: false
151
+ implicitWidth: 96
152
+ Accessible.name: "Close About dialog"
153
+ onClicked: root.close()
154
+ }
155
+ }
156
+ }
@@ -0,0 +1,120 @@
1
+ pragma ComponentBehavior: Bound
2
+
3
+ import QtQuick 2.15
4
+ import QtQuick.Controls 2.15
5
+ import QtQuick.Layouts 1.15
6
+
7
+ Rectangle {
8
+ id: root
9
+
10
+ required property var sessionState
11
+ required property var provider
12
+ required property bool narrow
13
+ signal settingsRequested()
14
+
15
+ color: Theme.surfaceLow
16
+ implicitHeight: 68
17
+
18
+ RowLayout {
19
+ anchors.fill: parent
20
+ anchors.leftMargin: 20
21
+ anchors.rightMargin: 20
22
+ spacing: 16
23
+
24
+ Label {
25
+ objectName: "appBarBrandTitle"
26
+ visible: root.narrow
27
+ text: "DRAFTOMEN"
28
+ color: Theme.primary
29
+ font.pixelSize: Theme.textPixelSize(18)
30
+ font.bold: true
31
+ font.letterSpacing: 1.2
32
+ }
33
+
34
+ Rectangle {
35
+ visible: root.narrow
36
+ Layout.preferredWidth: 1
37
+ Layout.preferredHeight: 34
38
+ color: Theme.outline
39
+ }
40
+
41
+ ColumnLayout {
42
+ Layout.fillWidth: true
43
+ Layout.minimumWidth: 0
44
+ spacing: 2
45
+
46
+ Label {
47
+ text: root.sessionState.status ? root.sessionState.status.message : "Starting Draft Omen."
48
+ color: Theme.text
49
+ elide: Text.ElideRight
50
+ Layout.fillWidth: true
51
+ }
52
+
53
+ Label {
54
+ text: root.sessionState.draft
55
+ ? root.sessionState.draft.set_code + " · " + root.sessionState.draft.event_name
56
+ : "No active draft"
57
+ color: Theme.textMuted
58
+ font.pixelSize: Theme.textPixelSize(11)
59
+ Layout.fillWidth: true
60
+ elide: Text.ElideRight
61
+ }
62
+ }
63
+
64
+ DimensionalComboBox {
65
+ id: accountSelector
66
+ objectName: "accountSelector"
67
+ Layout.preferredWidth: 180
68
+ visible: root.sessionState.accounts
69
+ && root.sessionState.accounts.length > 0
70
+ model: root.sessionState.accounts || []
71
+ textRole: "screen_name"
72
+ valueRole: "account_id"
73
+ currentIndex: {
74
+ const activeAccount = root.sessionState.active_account
75
+ if (!activeAccount)
76
+ return -1
77
+ for (let index = 0; index < model.length; index++) {
78
+ if (model[index].account_id === activeAccount.account_id)
79
+ return index
80
+ }
81
+ return -1
82
+ }
83
+ displayText: root.sessionState.active_account
84
+ ? root.sessionState.active_account.screen_name
85
+ || root.sessionState.active_account.account_id
86
+ : "Choose Arena account"
87
+ Accessible.name: "Arena account"
88
+ Accessible.description: "Choose the Arena account whose live draft is shown."
89
+ onActivated: root.provider.chooseAccount(currentValue)
90
+ }
91
+
92
+ DimensionalComboBox {
93
+ id: scenarioSelector
94
+ visible: root.provider && root.provider.mockMode
95
+ Layout.preferredWidth: 126
96
+ model: root.provider ? root.provider.scenarios : []
97
+ currentIndex: root.provider
98
+ ? Math.max(0, root.provider.scenarios.indexOf(root.provider.scenario))
99
+ : -1
100
+ Accessible.name: "Representative state"
101
+ Accessible.description: "Choose deterministic visual-development data."
102
+ onActivated: root.provider.selectScenario(currentText)
103
+ }
104
+ DimensionalButton {
105
+ objectName: "settingsButton"
106
+ text: "Settings"
107
+ Accessible.name: "Open settings"
108
+ Accessible.description: "Open draft guidance, display, and accessibility settings."
109
+ onClicked: root.settingsRequested()
110
+ }
111
+ }
112
+
113
+ Rectangle {
114
+ anchors.bottom: parent.bottom
115
+ width: parent.width
116
+ height: 1
117
+ color: Theme.outline
118
+ }
119
+ }
120
+