django-spire 0.23.9__py3-none-any.whl → 0.23.10__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.
@@ -10,115 +10,106 @@
10
10
 
11
11
  {% block modal_content_content %}
12
12
  <div
13
- data-stream-url="{% block progress_stream_url %}{% endblock %}"
14
- data-redirect-url="{% block progress_redirect_url %}{% endblock %}"
15
13
  data-redirect-delay="{% block progress_redirect_delay %}1000{% endblock %}"
14
+ data-redirect-url="{% block progress_redirect_url %}{% endblock %}"
15
+ data-stream-url="{% block progress_stream_url %}{% endblock %}"
16
16
  x-data="{
17
- tasks: {},
18
- task_order: [],
19
- overall_progress: 0,
20
- message: 'Initializing...',
21
17
  has_error: false,
22
- stream: null,
23
-
24
- init() {
25
- let stream_url = this.$el.dataset.streamUrl;
26
- let redirect_url = this.$el.dataset.redirectUrl;
27
- let redirect_delay = parseInt(this.$el.dataset.redirectDelay);
18
+ message: 'Initializing...',
19
+ overall_percent: 0,
20
+ tasks: {},
28
21
 
29
- this.stream = new ProgressStream(stream_url, {
30
- on_update: (data) => {
31
- this.message = data.message;
32
- this.overall_progress = data.progress;
33
- if (data.task_order) {
34
- this.task_order = data.task_order;
35
- }
36
- if (data.tasks) {
37
- this.tasks = data.tasks;
38
- }
39
- },
40
- on_complete: (data) => {
41
- setTimeout(() => {
42
- close_modal();
43
- }, 500);
22
+ async init() {
23
+ let stream = new ProgressStream(this.$el.dataset.streamUrl, {
24
+ on_complete: () => {
25
+ setTimeout(() => close_modal(), 500);
44
26
  },
45
27
  on_error: (data) => {
46
28
  this.has_error = true;
47
- this.message = data.message;
29
+ this.message = data.message || 'An error occurred';
30
+ },
31
+ on_update: (data) => {
32
+ this.overall_percent = data.overall_percent;
33
+ this.tasks = data.tasks;
48
34
  },
49
- redirect_on_complete: redirect_url,
50
- redirect_delay: redirect_delay
35
+ redirect_delay: parseInt(this.$el.dataset.redirectDelay),
36
+ redirect_url: this.$el.dataset.redirectUrl,
51
37
  });
52
- this.stream.start();
53
- },
54
38
 
55
- format_task_name(name) {
56
- return name.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
39
+ await stream.start();
57
40
  },
58
41
 
59
- get_progress_class(step) {
60
- if (step === 'complete') return 'bg-success';
61
- if (step === 'error') return 'bg-danger';
62
- if (step === 'processing') return 'bg-app-accent';
63
- return 'bg-secondary';
42
+ get_status_icon(status) {
43
+ if (status === 'complete') return 'bi-check-circle-fill text-success';
44
+ if (status === 'error') return 'bi-x-circle-fill text-danger';
45
+ if (status === 'running') return 'bi-arrow-repeat text-primary spin';
46
+ return 'bi-circle text-secondary';
64
47
  }
65
48
  }"
66
49
  >
67
50
  <div class="row g-3">
68
51
  <div class="col-12" x-show="!has_error">
69
- <template x-for="name in task_order" :key="name">
70
- <div class="mb-3" x-show="tasks[name]">
71
- <div class="d-flex justify-content-between align-items-center mb-1">
72
- <span class="fs-7" x-text="format_task_name(name)"></span>
73
- <span class="fs-7" x-text="tasks[name]?.progress + '%'"></span>
52
+ <template x-for="(task, task_id) in tasks" :key="task_id">
53
+ <div class="mb-3">
54
+ <div class="d-flex align-items-center mb-1">
55
+ <i class="bi me-2" :class="get_status_icon(task.status)"></i>
56
+ <span class="fw-medium" x-text="task.name"></span>
57
+ <span class="ms-auto fs-7" x-text="task.percent + '%'"></span>
74
58
  </div>
75
- <div class="progress" style="height: 6px;">
59
+ <div class="progress" style="height: 4px;">
76
60
  <div
77
61
  class="progress-bar"
78
- :class="get_progress_class(tasks[name]?.step)"
62
+ :class="task.status === 'error' ? 'bg-danger' : 'bg-success'"
79
63
  role="progressbar"
80
- :style="'width: ' + (tasks[name]?.progress || 0) + '%'"
64
+ :style="'width: ' + task.percent + '%'"
81
65
  ></div>
82
66
  </div>
67
+ <small class="text-muted" x-text="task.message"></small>
83
68
  </div>
84
69
  </template>
85
70
 
86
71
  <div class="mt-4 pt-3 border-top">
87
72
  <div class="d-flex justify-content-between align-items-center mb-2">
88
- <span class="fs-6 fw-medium">Overall Progress</span>
89
- <span class="fs-6" x-text="overall_progress + '%'"></span>
73
+ <span class="fw-semibold">Overall Progress</span>
74
+ <span x-text="overall_percent + '%'"></span>
90
75
  </div>
91
76
  <div class="progress" style="height: 8px;">
92
77
  <div
93
- class="progress-bar bg-app-accent"
78
+ class="progress-bar bg-primary"
94
79
  role="progressbar"
95
- :style="'width: ' + overall_progress + '%'"
80
+ :style="'width: ' + overall_percent + '%'"
96
81
  ></div>
97
82
  </div>
98
83
  </div>
99
- </div>
100
84
 
101
- <div class="col-12" x-show="!has_error">
102
- <p class="text-app-secondary mb-0 fs-7">
103
- {% block progress_wait_message %}Processing your request. This may take a moment.{% endblock %}
85
+ <p class="text-app-secondary mb-0 fs-7 text-center mt-3">
86
+ {% block progress_wait_message %}This may take a moment. Please do not close this window.{% endblock %}
104
87
  </p>
105
88
  </div>
106
89
 
107
90
  <div class="col-12" x-show="has_error" x-cloak>
108
- <div class="row g-2">
109
- <div class="col-12">
110
- {% include 'django_spire/element/attribute_element.html' with attribute_title='Error' %}
111
- <span class="fs-6 text-app-danger" x-text="message"></span>
112
- </div>
113
- <div class="col-12">
114
- <span @click="close_modal()">
115
- {% block progress_error_button %}
116
- {% include 'django_spire/button/primary_button.html' with button_text='Close' %}
117
- {% endblock %}
118
- </span>
119
- </div>
91
+ <div class="d-flex align-items-center justify-content-center py-4">
92
+ <i class="bi bi-x-circle-fill text-danger me-3 fs-4"></i>
93
+ <span class="fs-5" x-text="message"></span>
94
+ </div>
95
+ <div class="text-center">
96
+ <span @click="close_modal()">
97
+ {% block progress_error_button %}
98
+ {% include 'django_spire/button/primary_button.html' with button_text='Close' %}
99
+ {% endblock %}
100
+ </span>
120
101
  </div>
121
102
  </div>
122
103
  </div>
123
104
  </div>
105
+
106
+ <style>
107
+ .spin {
108
+ animation: spin 1s linear infinite;
109
+ }
110
+ @keyframes spin {
111
+ from { transform: rotate(0deg); }
112
+ to { transform: rotate(360deg); }
113
+ }
114
+ </style>
124
115
  {% endblock %}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-spire
3
- Version: 0.23.9
3
+ Version: 0.23.10
4
4
  Summary: A project for Django Spire
5
5
  Author-email: Brayden Carlson <braydenc@stratusadv.com>, Nathan Johnson <nathanj@stratusadv.com>
6
6
  License: Copyright (c) 2025 Stratus Advanced Technologies and Contributors.
@@ -1,6 +1,6 @@
1
1
  django_spire/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  django_spire/conf.py,sha256=3oUB1mtgHRjvbJsxfQWG5uL1KUP9uGig3zdP2dZphe8,942
3
- django_spire/consts.py,sha256=t8xJZKeg5w7HOvKrg7zQgCDjdqExo49Aay7f11XOD8M,171
3
+ django_spire/consts.py,sha256=d5_qKitqKzGvVZucpkte-GZyZj9s2j8C0KuzkalsyVQ,172
4
4
  django_spire/exceptions.py,sha256=M7buFvm-K4lK09pH5fVcZ-MxsDIzdpEJBF33Xss5bSw,289
5
5
  django_spire/settings.py,sha256=0xImrKpF7VW4wc9jLGO07e2ev6D4-i_TREgpTepLp3I,1042
6
6
  django_spire/urls.py,sha256=wQx6R-nXx69MeOF-WmDxcEUM5WmUHGplbY5uZ_HnDp8,703
@@ -397,17 +397,14 @@ django_spire/contrib/performance/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRk
397
397
  django_spire/contrib/performance/decorators.py,sha256=vCENOqcmXoEvi1Y3Uw55J21zB-ZvpWk7rRo2jqyg8pc,731
398
398
  django_spire/contrib/performance/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
399
399
  django_spire/contrib/performance/tests/test_performance.py,sha256=wVNzrUEG0bTMcvJrqkSWkAE27LSyJMxrRZRd04ZKDQM,3029
400
- django_spire/contrib/progress/__init__.py,sha256=pvd6_7puMDWStjZqcwsWEulpeGupQPv2fladV1IiaXg,631
401
- django_spire/contrib/progress/enums.py,sha256=nwyfKxQno_ghGGTbpK8lRO1cY7dCfbxnnsQ4cSbkXBw,194
402
- django_spire/contrib/progress/mixins.py,sha256=V84LRtniFDLK2E5dKcSZNh-a1zTf9Ee7QXg1rDfoFHo,895
403
- django_spire/contrib/progress/runner.py,sha256=3jSqpdk2MJ7HPNutwF43Hs81O0NlzX4E9gnVaenhXUs,4445
404
- django_spire/contrib/progress/states.py,sha256=V7JU_nzpZN77TQFW0S51Gjil8xn8CKLKkAWDOpqzA8k,1908
405
- django_spire/contrib/progress/task.py,sha256=4pMbzOsjRRNqPKNohh08D39DRFpC3QomTCH5IdqQS0A,780
406
- django_spire/contrib/progress/tracker.py,sha256=xjGlEyKe5aiKW_KC8NKZ8F0BjQdFpLUqHz5Gr_C7jws,6490
400
+ django_spire/contrib/progress/__init__.py,sha256=5a7LTmUyw_cfO-3_QuN5M6zAhrCNs7MjEypL13qhoQA,345
401
+ django_spire/contrib/progress/enums.py,sha256=Rexk804FhmVQou7UOKSOCnjaZrSDZdniV_MWLh-kGxI,218
402
+ django_spire/contrib/progress/session.py,sha256=Oo1QS8f7aTN7K_Y9bwXZHo6otZQkRSp6V_6c0stvYcI,8825
403
+ django_spire/contrib/progress/tasks.py,sha256=afC4vhWzvuRq6-P80LdB-BmDGD5kADtgYGM2SaKz8Ao,1625
407
404
  django_spire/contrib/progress/static/django_spire/css/contrib/progress/progress.css,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
408
- django_spire/contrib/progress/static/django_spire/js/contrib/progress/progress.js,sha256=BeT52b8rHoBzk3tMpXX3bO3R-bDYTjyrEHjjc1AS2LQ,2228
409
- django_spire/contrib/progress/templates/django_spire/contrib/progress/card/card.html,sha256=2Ajv0UBJEGwc4InVe69aLe2u4K56yh2JAuvO6uH24j8,3353
410
- django_spire/contrib/progress/templates/django_spire/contrib/progress/modal/content.html,sha256=Qxer-q4GBC5h8onzno2NUlPrXluw_tO1kWlGComQ98E,5232
405
+ django_spire/contrib/progress/static/django_spire/js/contrib/progress/progress.js,sha256=vhjwLWTqkjcuK7j_z9ndCGdWfBFmRAM4MvF8zpEKJL0,1730
406
+ django_spire/contrib/progress/templates/django_spire/contrib/progress/card/card.html,sha256=UB5PpaZzaSihiE6BnThYrM21qVrf1iT1fF-TbtkoZrc,3389
407
+ django_spire/contrib/progress/templates/django_spire/contrib/progress/modal/content.html,sha256=613hvePQFurk4Q0Id8NuQz9Umbh35eJSOR-IQipo_o4,4857
411
408
  django_spire/contrib/queryset/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
412
409
  django_spire/contrib/queryset/enums.py,sha256=dkzI464Q53huqtNGfnM-biuKpHUQU78WwuWvt3Pm8mI,145
413
410
  django_spire/contrib/queryset/filter_tools.py,sha256=V_s6pG2uyqvoTNtBXXXNKXE69ipwexxWAR-X30H1-XI,1129
@@ -1372,8 +1369,8 @@ django_spire/theme/urls/page_urls.py,sha256=Oak3x_xwQEb01NKdrsB1nk6yPaOEnheuSG1m
1372
1369
  django_spire/theme/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1373
1370
  django_spire/theme/views/json_views.py,sha256=PWwVTaty0BVGbj65L5cxex6JNhc-xVAI_rEYjbJWqEM,1893
1374
1371
  django_spire/theme/views/page_views.py,sha256=WenjOa6Welpu3IMolY56ZwBjy4aK9hpbiMNuygjAl1A,3922
1375
- django_spire-0.23.9.dist-info/licenses/LICENSE.md,sha256=ZAeCT76WvaoEZE9xPhihyWjTwH0wQZXQmyRsnV2VPFs,1091
1376
- django_spire-0.23.9.dist-info/METADATA,sha256=pZhtFdrVEEo-IDoUkT28tzpPjfN-SFkXAKi1EWvcS-U,5127
1377
- django_spire-0.23.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
1378
- django_spire-0.23.9.dist-info/top_level.txt,sha256=xf3QV1e--ONkVpgMDQE9iqjQ1Vg4--_6C8wmO-KxPHQ,13
1379
- django_spire-0.23.9.dist-info/RECORD,,
1372
+ django_spire-0.23.10.dist-info/licenses/LICENSE.md,sha256=ZAeCT76WvaoEZE9xPhihyWjTwH0wQZXQmyRsnV2VPFs,1091
1373
+ django_spire-0.23.10.dist-info/METADATA,sha256=tdZ2P4q_PlnHfvRd4XibSjAzFHyxjzKG9PDX3EjAVU4,5128
1374
+ django_spire-0.23.10.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
1375
+ django_spire-0.23.10.dist-info/top_level.txt,sha256=xf3QV1e--ONkVpgMDQE9iqjQ1Vg4--_6C8wmO-KxPHQ,13
1376
+ django_spire-0.23.10.dist-info/RECORD,,
@@ -1,36 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import TYPE_CHECKING
4
-
5
- from django_spire.contrib.progress.tracker import ProgressTracker
6
-
7
- if TYPE_CHECKING:
8
- from typing import Any
9
-
10
- from django_spire.contrib.progress.enums import ProgressStatus
11
-
12
-
13
- class ProgressTrackingMixin:
14
- _tracker: ProgressTracker | None = None
15
-
16
- def get_tracker_key(self) -> str:
17
- raise NotImplementedError
18
-
19
- @property
20
- def tracker(self) -> ProgressTracker:
21
- if self._tracker is None:
22
- self._tracker = ProgressTracker(self.get_tracker_key())
23
-
24
- return self._tracker
25
-
26
- def progress_error(self, message: str) -> None:
27
- self.tracker.error(message)
28
-
29
- def update_progress(
30
- self,
31
- status: ProgressStatus,
32
- message: str,
33
- progress: int,
34
- **kwargs: Any
35
- ) -> None:
36
- self.tracker.update(status, message, progress, **kwargs)
@@ -1,140 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import math
4
- import random
5
- import threading
6
- import time
7
-
8
- from typing import TYPE_CHECKING
9
-
10
- from django_spire.contrib.progress.enums import ProgressStatus
11
- from django_spire.contrib.progress.task import ProgressMessages, Task, TaskResult
12
-
13
- if TYPE_CHECKING:
14
- from typing import Any, Callable
15
-
16
- from django_spire.contrib.progress.tracker import ProgressTracker
17
-
18
-
19
- class TaskProgressUpdater:
20
- def __init__(self, tracker: ProgressTracker, task: Task) -> None:
21
- self._messages = ProgressMessages()
22
- self._task = task
23
- self._tracker = tracker
24
-
25
- def complete(self, message: str | None = None) -> None:
26
- self._tracker._update_task(
27
- self._task.name,
28
- ProgressStatus.COMPLETE,
29
- self._format(message or self._messages.complete),
30
- 100
31
- )
32
-
33
- def error(self, message: str) -> None:
34
- self._tracker._update_task(
35
- self._task.name,
36
- ProgressStatus.ERROR,
37
- self._format(message),
38
- 0
39
- )
40
-
41
- def start(self) -> None:
42
- self._tracker._update_task(
43
- self._task.name,
44
- ProgressStatus.PROCESSING,
45
- self._format(self._messages.starting),
46
- 2
47
- )
48
-
49
- def update(self, message: str, progress: int) -> None:
50
- self._tracker._update_task(
51
- self._task.name,
52
- ProgressStatus.PROCESSING,
53
- self._format(message),
54
- progress
55
- )
56
-
57
- def _format(self, message: str) -> str:
58
- return f'{self._task.label}: {message}'
59
-
60
-
61
- class ProgressSimulator:
62
- def __init__(
63
- self,
64
- updater: TaskProgressUpdater,
65
- max_progress: int = 90,
66
- update_interval: float = 0.15
67
- ) -> None:
68
- self._max_progress = max_progress
69
- self._messages = ProgressMessages()
70
- self._update_interval = update_interval
71
- self._updater = updater
72
-
73
- def run(self, stop_event: threading.Event) -> None:
74
- start_time = time.time()
75
- duration = random.uniform(8.0, 15.0)
76
-
77
- while not stop_event.is_set():
78
- elapsed = time.time() - start_time
79
- t = min(elapsed / duration, 1.0)
80
-
81
- progress = self._ease_out_expo(t) * self._max_progress
82
- progress = min(int(progress), self._max_progress)
83
-
84
- jitter = random.uniform(-1.5, 1.5) if progress < self._max_progress - 5 else 0
85
- progress = max(2, min(int(progress + jitter), self._max_progress))
86
-
87
- message = self._get_message_for_progress(progress)
88
- self._updater.update(message, progress)
89
-
90
- if progress >= self._max_progress:
91
- break
92
-
93
- time.sleep(self._update_interval + random.uniform(0, 0.1))
94
-
95
- def _ease_out_expo(self, t: float) -> float:
96
- if t >= 1.0:
97
- return 1.0
98
-
99
- return 1.0 - math.pow(2, -10 * t)
100
-
101
- def _get_message_for_progress(self, progress: int) -> str:
102
- steps = self._messages.steps
103
- index = min(int(progress / (self._max_progress / len(steps))), len(steps) - 1)
104
-
105
- return steps[index]
106
-
107
-
108
- class TaskRunner:
109
- def __init__(self, tracker: ProgressTracker, task: Task) -> None:
110
- self._stop_event = threading.Event()
111
- self._task = task
112
- self._tracker = tracker
113
- self._updater = TaskProgressUpdater(tracker, task)
114
-
115
- def run_parallel(self, results: dict[str, TaskResult]) -> None:
116
- self._execute(results, lambda: self._task.func())
117
-
118
- def run_sequential(self, results: dict[str, TaskResult]) -> None:
119
- unwrapped = {name: result.value for name, result in results.items()}
120
- self._execute(results, lambda: self._task.func(unwrapped))
121
-
122
- def stop(self) -> None:
123
- self._stop_event.set()
124
-
125
- def _execute(self, results: dict[str, TaskResult], executor: Callable[[], Any]) -> None:
126
- simulator = ProgressSimulator(self._updater)
127
- progress_thread = threading.Thread(target=simulator.run, args=(self._stop_event,))
128
- progress_thread.start()
129
-
130
- try:
131
- self._updater.start()
132
- value = executor()
133
- results[self._task.name] = TaskResult(value=value)
134
- self._updater.complete()
135
- except BaseException as e:
136
- results[self._task.name] = TaskResult(error=e)
137
- self._updater.error(str(e))
138
- finally:
139
- self._stop_event.set()
140
- progress_thread.join(timeout=1)
@@ -1,64 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass, field
4
- from typing import TYPE_CHECKING
5
-
6
- from django_spire.contrib.progress.enums import ProgressStatus
7
-
8
- if TYPE_CHECKING:
9
- from typing import Any
10
-
11
-
12
- @dataclass
13
- class TaskState:
14
- message: str = 'Waiting...'
15
- progress: int = 0
16
- status: ProgressStatus = ProgressStatus.PENDING
17
-
18
- @classmethod
19
- def from_dict(cls, data: dict[str, Any]) -> TaskState:
20
- return cls(
21
- message=data.get('message', 'Waiting...'),
22
- progress=data.get('progress', 0),
23
- status=ProgressStatus(data.get('step', ProgressStatus.PENDING)),
24
- )
25
-
26
- def to_dict(self) -> dict[str, Any]:
27
- return {
28
- 'message': self.message,
29
- 'progress': self.progress,
30
- 'step': self.status.value,
31
- }
32
-
33
-
34
- @dataclass
35
- class TrackerState:
36
- message: str = 'Initializing...'
37
- progress: int = 0
38
- status: ProgressStatus = ProgressStatus.PENDING
39
- task_order: list[str] = field(default_factory=list)
40
- tasks: dict[str, TaskState] = field(default_factory=dict)
41
-
42
- @classmethod
43
- def from_dict(cls, data: dict[str, Any]) -> TrackerState:
44
- tasks = {
45
- name: TaskState.from_dict(task_data)
46
- for name, task_data in data.get('tasks', {}).items()
47
- }
48
-
49
- return cls(
50
- message=data.get('message', 'Initializing...'),
51
- progress=data.get('progress', 0),
52
- status=ProgressStatus(data.get('step', ProgressStatus.PENDING)),
53
- task_order=data.get('task_order', []),
54
- tasks=tasks,
55
- )
56
-
57
- def to_dict(self) -> dict[str, Any]:
58
- return {
59
- 'message': self.message,
60
- 'progress': self.progress,
61
- 'step': self.status.value,
62
- 'task_order': self.task_order,
63
- 'tasks': {name: task.to_dict() for name, task in self.tasks.items()},
64
- }
@@ -1,40 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass, field
4
- from typing import TYPE_CHECKING
5
-
6
- if TYPE_CHECKING:
7
- from typing import Any, Callable
8
-
9
-
10
- @dataclass
11
- class ProgressMessages:
12
- complete: str = 'Complete'
13
- error: str = 'Error'
14
- starting: str = 'Starting...'
15
- steps: list[str] = field(default_factory=lambda: [
16
- 'Initializing...',
17
- 'Processing...',
18
- 'Analyzing...',
19
- 'Working...',
20
- 'Almost there...',
21
- 'Finalizing...',
22
- ])
23
-
24
-
25
- @dataclass
26
- class Task:
27
- func: Callable
28
- label: str
29
- name: str
30
- parallel: bool = False
31
-
32
-
33
- @dataclass
34
- class TaskResult:
35
- error: BaseException | None = None
36
- value: Any = None
37
-
38
- @property
39
- def failed(self) -> bool:
40
- return self.error is not None