quebec 0.1.1__cp39-abi3-win_amd64.whl → 0.2.1__cp39-abi3-win_amd64.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.
- quebec/__init__.py +246 -43
- quebec/logger.py +9 -2
- quebec/quebec.pyd +0 -0
- {quebec-0.1.1.dist-info → quebec-0.2.1.dist-info}/METADATA +69 -3
- quebec-0.2.1.dist-info/RECORD +7 -0
- {quebec-0.1.1.dist-info → quebec-0.2.1.dist-info}/WHEEL +1 -1
- quebec-0.1.1.dist-info/RECORD +0 -7
- {quebec-0.1.1.dist-info → quebec-0.2.1.dist-info}/licenses/LICENSE +0 -0
quebec/__init__.py
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
from .quebec import * # NOQA
|
|
2
|
+
from . import quebec
|
|
3
|
+
from .quebec import Quebec, ActiveJob
|
|
2
4
|
import logging
|
|
3
5
|
import time
|
|
4
6
|
import queue
|
|
5
7
|
import threading
|
|
6
|
-
from
|
|
7
|
-
from typing import
|
|
8
|
-
import
|
|
9
|
-
from .logger import job_id_var
|
|
8
|
+
from datetime import datetime, timedelta, timezone
|
|
9
|
+
from typing import List, Type, Any, Optional, Union
|
|
10
|
+
from .logger import job_id_var, queue_var
|
|
10
11
|
|
|
11
12
|
__doc__ = quebec.__doc__
|
|
12
13
|
if hasattr(quebec, "__all__"):
|
|
@@ -15,15 +16,60 @@ if hasattr(quebec, "__all__"):
|
|
|
15
16
|
logger = logging.getLogger(__name__)
|
|
16
17
|
|
|
17
18
|
|
|
18
|
-
class
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
class JobBuilder:
|
|
20
|
+
"""Builder for configuring job options before enqueueing.
|
|
21
|
+
|
|
22
|
+
This allows chaining configuration like:
|
|
23
|
+
MyJob.set(wait=3600).perform_later(qc, arg1, arg2)
|
|
24
|
+
MyJob.set(queue='high', priority=10).perform_later(qc, arg1)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, job_class: Type, **options):
|
|
28
|
+
self.job_class = job_class
|
|
29
|
+
self.options = options
|
|
30
|
+
|
|
31
|
+
def _calculate_scheduled_at(self) -> Optional[datetime]:
|
|
32
|
+
"""Calculate scheduled_at from wait or wait_until options."""
|
|
33
|
+
wait = self.options.get('wait')
|
|
34
|
+
wait_until = self.options.get('wait_until')
|
|
35
|
+
|
|
36
|
+
if wait_until is not None:
|
|
37
|
+
if isinstance(wait_until, datetime):
|
|
38
|
+
# Ensure timezone-aware for correct timestamp conversion
|
|
39
|
+
if wait_until.tzinfo is None:
|
|
40
|
+
# Assume naive datetime is UTC
|
|
41
|
+
wait_until = wait_until.replace(tzinfo=timezone.utc)
|
|
42
|
+
return wait_until
|
|
43
|
+
raise ValueError("wait_until must be a datetime object")
|
|
44
|
+
|
|
45
|
+
if wait is not None:
|
|
46
|
+
# Use timezone-aware UTC datetime
|
|
47
|
+
now = datetime.now(timezone.utc)
|
|
48
|
+
if isinstance(wait, (int, float)):
|
|
49
|
+
return now + timedelta(seconds=wait)
|
|
50
|
+
elif isinstance(wait, timedelta):
|
|
51
|
+
return now + wait
|
|
52
|
+
raise ValueError("wait must be a number (seconds) or timedelta")
|
|
53
|
+
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
def perform_later(self, qc: 'Quebec', *args, **kwargs) -> 'ActiveJob':
|
|
57
|
+
"""Enqueue the job with configured options."""
|
|
58
|
+
scheduled_at = self._calculate_scheduled_at()
|
|
59
|
+
|
|
60
|
+
# Pass internal options via kwargs (will be filtered out before serialization)
|
|
61
|
+
if scheduled_at is not None:
|
|
62
|
+
kwargs['_scheduled_at'] = scheduled_at.timestamp()
|
|
63
|
+
|
|
64
|
+
if 'queue' in self.options:
|
|
65
|
+
kwargs['_queue'] = self.options['queue']
|
|
66
|
+
|
|
67
|
+
if 'priority' in self.options:
|
|
68
|
+
kwargs['_priority'] = self.options['priority']
|
|
69
|
+
|
|
70
|
+
# Call the original perform_later
|
|
71
|
+
return self.job_class.perform_later(qc, *args, **kwargs)
|
|
21
72
|
|
|
22
|
-
# @dataclasses.dataclass
|
|
23
|
-
# class RetryStrategy:
|
|
24
|
-
# wait: int = 3 # seconds
|
|
25
|
-
# attempts: int = 5 # attempts
|
|
26
|
-
# # exceptions: tuple = (Exception,)
|
|
27
73
|
|
|
28
74
|
class NoNewOverrideMeta(type):
|
|
29
75
|
def __new__(cls, name, bases, dct):
|
|
@@ -34,7 +80,37 @@ class NoNewOverrideMeta(type):
|
|
|
34
80
|
return super().__new__(cls, name, bases, dct)
|
|
35
81
|
|
|
36
82
|
class BaseClass(ActiveJob, metaclass=NoNewOverrideMeta):
|
|
37
|
-
|
|
83
|
+
@classmethod
|
|
84
|
+
def set(cls, wait: Union[int, float, timedelta] = None,
|
|
85
|
+
wait_until: datetime = None,
|
|
86
|
+
queue: str = None,
|
|
87
|
+
priority: int = None) -> JobBuilder:
|
|
88
|
+
"""Configure job options before enqueueing.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
wait: Delay in seconds (int/float) or timedelta before running
|
|
92
|
+
wait_until: Specific datetime when the job should run
|
|
93
|
+
queue: Queue name to enqueue the job to
|
|
94
|
+
priority: Job priority (lower number = higher priority)
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
JobBuilder instance for chaining with perform_later
|
|
98
|
+
|
|
99
|
+
Example:
|
|
100
|
+
MyJob.set(wait=3600).perform_later(qc, arg1) # Run in 1 hour
|
|
101
|
+
MyJob.set(wait_until=tomorrow).perform_later(qc, arg1)
|
|
102
|
+
MyJob.set(queue='critical', priority=1).perform_later(qc, arg1)
|
|
103
|
+
"""
|
|
104
|
+
options = {}
|
|
105
|
+
if wait is not None:
|
|
106
|
+
options['wait'] = wait
|
|
107
|
+
if wait_until is not None:
|
|
108
|
+
options['wait_until'] = wait_until
|
|
109
|
+
if queue is not None:
|
|
110
|
+
options['queue'] = queue
|
|
111
|
+
if priority is not None:
|
|
112
|
+
options['priority'] = priority
|
|
113
|
+
return JobBuilder(cls, **options)
|
|
38
114
|
|
|
39
115
|
|
|
40
116
|
class ThreadedRunner:
|
|
@@ -54,14 +130,16 @@ class ThreadedRunner:
|
|
|
54
130
|
self.queue.task_done()
|
|
55
131
|
self.execution.tid = str(threading.get_ident())
|
|
56
132
|
|
|
57
|
-
# Inject
|
|
58
|
-
|
|
133
|
+
# Inject jid and queue into context before execution, clean up after
|
|
134
|
+
jid_token = job_id_var.set(self.execution.jid)
|
|
135
|
+
queue_token = queue_var.set(self.execution.queue)
|
|
59
136
|
self.execution.perform()
|
|
60
137
|
logger.debug(self.execution.metric)
|
|
61
|
-
|
|
138
|
+
queue_var.reset(queue_token)
|
|
139
|
+
job_id_var.reset(jid_token)
|
|
62
140
|
except queue.Empty:
|
|
63
|
-
|
|
64
|
-
except (queue.ShutDown, KeyboardInterrupt)
|
|
141
|
+
pass # No job available, just continue waiting
|
|
142
|
+
except (queue.ShutDown, KeyboardInterrupt):
|
|
65
143
|
break
|
|
66
144
|
except Exception as e:
|
|
67
145
|
logger.error(f"Unexpected exception in ThreadedRunner: {e}", exc_info=True)
|
|
@@ -78,28 +156,153 @@ class ThreadedRunner:
|
|
|
78
156
|
except Exception as e:
|
|
79
157
|
logger.error(f"Error in cleanup: {e}", exc_info=True)
|
|
80
158
|
|
|
81
|
-
|
|
82
|
-
#
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
159
|
+
|
|
160
|
+
# Runtime state for Quebec instances (PyO3 classes don't support dynamic attributes)
|
|
161
|
+
_quebec_state: dict = {}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _quebec_start(
|
|
165
|
+
self,
|
|
166
|
+
*,
|
|
167
|
+
create_tables: bool = False,
|
|
168
|
+
control_plane: Optional[str] = None,
|
|
169
|
+
spawn: Optional[List[str]] = None,
|
|
170
|
+
threads: int = 1,
|
|
171
|
+
):
|
|
172
|
+
"""Non-blocking start. Returns immediately after all components are started.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
create_tables: Whether to create database tables (default False).
|
|
176
|
+
Set to True only if the current user has DDL permissions.
|
|
177
|
+
control_plane: Control plane listen address, e.g. '127.0.0.1:5006'.
|
|
178
|
+
spawn: List of components to spawn. Options: 'worker', 'dispatcher', 'scheduler'.
|
|
179
|
+
None means spawn all components.
|
|
180
|
+
threads: Number of worker threads to run jobs (default 1).
|
|
181
|
+
|
|
182
|
+
Example:
|
|
183
|
+
qc.start()
|
|
184
|
+
# ... do other work ...
|
|
185
|
+
qc.wait() # Block until shutdown
|
|
186
|
+
"""
|
|
187
|
+
if create_tables:
|
|
188
|
+
self.create_table()
|
|
189
|
+
|
|
190
|
+
self.setup_signal_handler()
|
|
191
|
+
|
|
192
|
+
if control_plane:
|
|
193
|
+
self.start_control_plane(control_plane)
|
|
194
|
+
|
|
195
|
+
# Spawn components based on spawn parameter
|
|
196
|
+
if spawn is None:
|
|
197
|
+
self.spawn_all()
|
|
198
|
+
else:
|
|
199
|
+
for component in spawn:
|
|
200
|
+
if component == 'worker':
|
|
201
|
+
self.spawn_job_claim_poller()
|
|
202
|
+
elif component == 'dispatcher':
|
|
203
|
+
self.spawn_dispatcher()
|
|
204
|
+
elif component == 'scheduler':
|
|
205
|
+
self.spawn_scheduler()
|
|
206
|
+
else:
|
|
207
|
+
raise ValueError(f"Unknown component: {component}")
|
|
208
|
+
|
|
209
|
+
# Set up threading infrastructure
|
|
210
|
+
shutdown_event = threading.Event()
|
|
211
|
+
job_queue = queue.Queue()
|
|
212
|
+
|
|
213
|
+
# Register internal shutdown handler to signal the event
|
|
214
|
+
@self.on_shutdown
|
|
215
|
+
def _internal_shutdown_handler():
|
|
216
|
+
shutdown_event.set()
|
|
217
|
+
|
|
218
|
+
if threads > 0:
|
|
219
|
+
self.feed_jobs_to_queue(job_queue)
|
|
220
|
+
|
|
221
|
+
def run_worker():
|
|
222
|
+
runner = ThreadedRunner(job_queue, shutdown_event)
|
|
223
|
+
runner.run()
|
|
224
|
+
|
|
225
|
+
# Start worker threads as daemon so program can exit after start()
|
|
226
|
+
worker_threads = []
|
|
227
|
+
for i in range(threads):
|
|
228
|
+
t = threading.Thread(target=run_worker, name=f'quebec-worker-{i}', daemon=True)
|
|
229
|
+
t.start()
|
|
230
|
+
worker_threads.append(t)
|
|
231
|
+
|
|
232
|
+
# Store state by instance id
|
|
233
|
+
_quebec_state[id(self)] = {
|
|
234
|
+
'shutdown_event': shutdown_event,
|
|
235
|
+
'job_queue': job_queue,
|
|
236
|
+
'worker_threads': worker_threads,
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return self # Enable chaining: qc.start().wait()
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _quebec_wait(self):
|
|
243
|
+
"""Block until shutdown signal is received.
|
|
244
|
+
|
|
245
|
+
Call this after start() to wait for graceful shutdown.
|
|
246
|
+
|
|
247
|
+
Example:
|
|
248
|
+
qc.start()
|
|
249
|
+
# ... do other work ...
|
|
250
|
+
qc.wait()
|
|
251
|
+
"""
|
|
252
|
+
state = _quebec_state.get(id(self))
|
|
253
|
+
if state is None:
|
|
254
|
+
raise RuntimeError("Quebec not started. Call start() first.")
|
|
255
|
+
|
|
256
|
+
try:
|
|
257
|
+
while not state['shutdown_event'].is_set():
|
|
258
|
+
time.sleep(0.5)
|
|
259
|
+
except KeyboardInterrupt:
|
|
260
|
+
logger.debug('KeyboardInterrupt, shutting down...')
|
|
261
|
+
self.graceful_shutdown()
|
|
262
|
+
finally:
|
|
263
|
+
# Wait for worker threads to finish
|
|
264
|
+
for t in state['worker_threads']:
|
|
265
|
+
t.join(timeout=5.0)
|
|
266
|
+
_quebec_state.pop(id(self), None)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _quebec_run(
|
|
270
|
+
self,
|
|
271
|
+
*,
|
|
272
|
+
create_tables: bool = False,
|
|
273
|
+
control_plane: Optional[str] = None,
|
|
274
|
+
spawn: Optional[List[str]] = None,
|
|
275
|
+
threads: int = 1,
|
|
276
|
+
):
|
|
277
|
+
"""Blocking run. Starts all components and waits until shutdown.
|
|
278
|
+
|
|
279
|
+
This is equivalent to calling start() followed by wait().
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
create_tables: Whether to create database tables (default False).
|
|
283
|
+
Set to True only if the current user has DDL permissions.
|
|
284
|
+
control_plane: Control plane listen address, e.g. '127.0.0.1:5006'.
|
|
285
|
+
spawn: List of components to spawn. Options: 'worker', 'dispatcher', 'scheduler'.
|
|
286
|
+
None means spawn all components.
|
|
287
|
+
threads: Number of worker threads to run jobs (default 1).
|
|
288
|
+
|
|
289
|
+
Example:
|
|
290
|
+
qc.run() # Start all components and block
|
|
291
|
+
qc.run(create_tables=True) # Create tables and start all
|
|
292
|
+
qc.run(spawn=['worker']) # Only start worker
|
|
293
|
+
qc.run(spawn=['worker', 'dispatcher'], control_plane='127.0.0.1:5006')
|
|
294
|
+
qc.run(threads=4) # Run with 4 worker threads
|
|
295
|
+
"""
|
|
296
|
+
self.start(
|
|
297
|
+
create_tables=create_tables,
|
|
298
|
+
control_plane=control_plane,
|
|
299
|
+
spawn=spawn,
|
|
300
|
+
threads=threads,
|
|
301
|
+
)
|
|
302
|
+
self.wait()
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# Attach methods to Quebec class
|
|
306
|
+
Quebec.start = _quebec_start
|
|
307
|
+
Quebec.wait = _quebec_wait
|
|
308
|
+
Quebec.run = _quebec_run
|
quebec/logger.py
CHANGED
|
@@ -4,13 +4,16 @@ from datetime import datetime, timezone
|
|
|
4
4
|
from typing import Optional
|
|
5
5
|
|
|
6
6
|
job_id_var: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar("job_id", default=None)
|
|
7
|
+
queue_var: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar("queue", default=None)
|
|
7
8
|
|
|
8
9
|
class ContextFilter(logging.Filter):
|
|
9
10
|
def filter(self, record: logging.LogRecord) -> bool:
|
|
10
11
|
try:
|
|
11
12
|
record.job_id = job_id_var.get(None)
|
|
13
|
+
record.queue = queue_var.get(None)
|
|
12
14
|
except Exception:
|
|
13
15
|
record.job_id = None
|
|
16
|
+
record.queue = None
|
|
14
17
|
return True
|
|
15
18
|
|
|
16
19
|
class QuebecFormatter(logging.Formatter):
|
|
@@ -22,8 +25,12 @@ class QuebecFormatter(logging.Formatter):
|
|
|
22
25
|
record.asctime = self.formatTime(record)
|
|
23
26
|
record.message = record.getMessage()
|
|
24
27
|
jid = getattr(record, 'job_id', None)
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
queue = getattr(record, 'queue', None)
|
|
29
|
+
if jid not in (None, '', '-'):
|
|
30
|
+
ctx = f' {{queue="{queue}" jid="{jid}" tid="{record.thread}"}}:'
|
|
31
|
+
else:
|
|
32
|
+
ctx = ""
|
|
33
|
+
origin = f"{record.name}:{record.filename}: {record.lineno}:"
|
|
27
34
|
return f"{record.asctime} {record.levelname:>5}{ctx} {origin} {record.message}"
|
|
28
35
|
|
|
29
36
|
|
quebec/quebec.pyd
CHANGED
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quebec
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Classifier: Programming Language :: Rust
|
|
5
5
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
6
6
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
@@ -17,9 +17,13 @@ Classifier: Operating System :: Microsoft :: Windows
|
|
|
17
17
|
Classifier: Operating System :: POSIX
|
|
18
18
|
Classifier: Operating System :: Unix
|
|
19
19
|
Classifier: Operating System :: MacOS
|
|
20
|
-
Requires-Dist: pytest
|
|
21
|
-
Requires-Dist: pytest-cov
|
|
20
|
+
Requires-Dist: pytest>=7.0.0 ; extra == 'test'
|
|
21
|
+
Requires-Dist: pytest-cov ; extra == 'test'
|
|
22
|
+
Requires-Dist: sphinx>=7.0 ; extra == 'docs'
|
|
23
|
+
Requires-Dist: shibuya ; extra == 'docs'
|
|
24
|
+
Requires-Dist: myst-parser ; extra == 'docs'
|
|
22
25
|
Provides-Extra: test
|
|
26
|
+
Provides-Extra: docs
|
|
23
27
|
License-File: LICENSE
|
|
24
28
|
Summary: Quebec is a simple background task queue for processing asynchronous tasks.
|
|
25
29
|
Keywords: solid_queue,postgresql,mysql,sqlite,queue
|
|
@@ -57,6 +61,12 @@ This project is inspired by [Solid Queue](https://github.com/rails/solid_queue).
|
|
|
57
61
|
- Signal handling
|
|
58
62
|
- Lifecycle hooks
|
|
59
63
|
|
|
64
|
+
### Control Plane
|
|
65
|
+
|
|
66
|
+
Built-in web dashboard for monitoring jobs, queues, and workers in real-time.
|
|
67
|
+
|
|
68
|
+

|
|
69
|
+
|
|
60
70
|
## Database Support
|
|
61
71
|
|
|
62
72
|
- SQLite
|
|
@@ -65,11 +75,67 @@ This project is inspired by [Solid Queue](https://github.com/rails/solid_queue).
|
|
|
65
75
|
|
|
66
76
|
## Quick Start
|
|
67
77
|
|
|
78
|
+
```python
|
|
79
|
+
import logging
|
|
80
|
+
from pathlib import Path
|
|
81
|
+
from quebec.logger import setup_logging
|
|
82
|
+
|
|
83
|
+
setup_logging(level=logging.DEBUG)
|
|
84
|
+
|
|
85
|
+
import quebec
|
|
86
|
+
|
|
87
|
+
db_path = Path('demo.db')
|
|
88
|
+
qc = quebec.Quebec(f'sqlite://{db_path}?mode=rwc')
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@qc.register_job
|
|
92
|
+
class FakeJob(quebec.BaseClass):
|
|
93
|
+
def perform(self, *args, **kwargs):
|
|
94
|
+
self.logger.info(f"Processing job {self.id}: args={args}, kwargs={kwargs}")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
if __name__ == "__main__":
|
|
98
|
+
# Enqueue a job
|
|
99
|
+
FakeJob.perform_later(qc, 123, foo='bar')
|
|
100
|
+
|
|
101
|
+
# Start Quebec (handles signal, spawns workers, runs main loop)
|
|
102
|
+
qc.run(
|
|
103
|
+
create_tables=not db_path.exists(),
|
|
104
|
+
control_plane='127.0.0.1:5006', # Optional: web dashboard
|
|
105
|
+
)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Or run the quickstart script directly:
|
|
109
|
+
|
|
68
110
|
```bash
|
|
69
111
|
curl -O https://raw.githubusercontent.com/ratazzi/quebec/refs/heads/master/quickstart.py
|
|
70
112
|
uv run quickstart.py
|
|
71
113
|
```
|
|
72
114
|
|
|
115
|
+
### `qc.run()` Options
|
|
116
|
+
|
|
117
|
+
| Parameter | Type | Default | Description |
|
|
118
|
+
|-----------|------|---------|-------------|
|
|
119
|
+
| `create_tables` | `bool` | `False` | Create database tables (requires DDL permissions) |
|
|
120
|
+
| `control_plane` | `str` | `None` | Web dashboard address, e.g. `'127.0.0.1:5006'` |
|
|
121
|
+
| `spawn` | `list[str]` | `None` | Components to spawn: `['worker', 'dispatcher', 'scheduler']`. `None` = all |
|
|
122
|
+
| `threads` | `int` | `1` | Number of worker threads to run jobs |
|
|
123
|
+
|
|
124
|
+
### Delayed Jobs
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from datetime import timedelta
|
|
128
|
+
|
|
129
|
+
# Run after 1 hour
|
|
130
|
+
FakeJob.set(wait=3600).perform_later(qc, arg1)
|
|
131
|
+
|
|
132
|
+
# Run at specific time
|
|
133
|
+
FakeJob.set(wait_until=tomorrow_9am).perform_later(qc, arg1)
|
|
134
|
+
|
|
135
|
+
# Override queue and priority
|
|
136
|
+
FakeJob.set(queue='critical', priority=1).perform_later(qc, arg1)
|
|
137
|
+
```
|
|
138
|
+
|
|
73
139
|
## Lifecycle Hooks
|
|
74
140
|
|
|
75
141
|
Quebec provides several lifecycle hooks that you can use to execute code at different stages of the application lifecycle:
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
quebec-0.2.1.dist-info/METADATA,sha256=yXF8dzOOSwTX5sv_sZTdfrEAt5ZTZC0vVN-724RjdwA,5061
|
|
2
|
+
quebec-0.2.1.dist-info/WHEEL,sha256=W8IbRrW6aFuwgkpVWKTtkIq8XwJIIpO5tHf4goK_jm8,95
|
|
3
|
+
quebec-0.2.1.dist-info/licenses/LICENSE,sha256=EMUpCdp2I-buVSjzgRTpd6TZDSnUcm1Pi4w8vOiwsQk,1095
|
|
4
|
+
quebec/__init__.py,sha256=382fYpy2xb2oAoEG-sRJVLXVjHr7ltkWcy7WBhSv3zM,10665
|
|
5
|
+
quebec/logger.py,sha256=gX0O1S77HWEQ2bkTFBV3kolSonvTP0p41xYV7WLmBNY,1806
|
|
6
|
+
quebec/quebec.pyd,sha256=kuy16xPzzlpjQIVm3HmMjoI9p_jv4K1oLumQurUy7RU,26043904
|
|
7
|
+
quebec-0.2.1.dist-info/RECORD,,
|
quebec-0.1.1.dist-info/RECORD
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
quebec-0.1.1.dist-info/METADATA,sha256=nkuSuqBEUtMBB3KLLOXkEIPGpOiHrAtrCwoozOiOwDE,3187
|
|
2
|
-
quebec-0.1.1.dist-info/WHEEL,sha256=dtDRtmb8ugdlQQiOTIfQg6qwccecUkp34WgpWvjGLGA,94
|
|
3
|
-
quebec-0.1.1.dist-info/licenses/LICENSE,sha256=EMUpCdp2I-buVSjzgRTpd6TZDSnUcm1Pi4w8vOiwsQk,1095
|
|
4
|
-
quebec/__init__.py,sha256=m8fQfMwoOohuOiIwqxXHSpvZ6mNBShYladtzmaHSh18,3575
|
|
5
|
-
quebec/logger.py,sha256=NuT2rxzUNaoRRF0eR5K-vKBgEY_JJr12NuXJtJBW7eM,1509
|
|
6
|
-
quebec/quebec.pyd,sha256=v8UaURJt3a6UV8X5QDYLyNRQPeUt_A1rr5pS4OOk99Q,26780160
|
|
7
|
-
quebec-0.1.1.dist-info/RECORD,,
|
|
File without changes
|