queueio 0.3.0__tar.gz → 0.5.0__tar.gz

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 (73) hide show
  1. queueio-0.5.0/.claude/settings.local.json +9 -0
  2. queueio-0.5.0/.devcontainer/devcontainer.json +7 -0
  3. queueio-0.5.0/.devcontainer/docker-compose.yml +10 -0
  4. queueio-0.5.0/.github/workflows/build.yml +42 -0
  5. queueio-0.5.0/.github/workflows/publish.yml +17 -0
  6. queueio-0.5.0/CHANGELOG.md +105 -0
  7. {queueio-0.3.0 → queueio-0.5.0}/PKG-INFO +1 -1
  8. queueio-0.5.0/dill.pyi +4 -0
  9. queueio-0.5.0/invocation.md +32 -0
  10. queueio-0.5.0/principles.md +28 -0
  11. {queueio-0.3.0 → queueio-0.5.0}/pyproject.toml +3 -4
  12. queueio-0.5.0/pyrightconfig.json +4 -0
  13. queueio-0.5.0/queueio/__init__.py +14 -0
  14. queueio-0.5.0/queueio/__main__.py +184 -0
  15. queueio-0.5.0/queueio/broker.py +50 -0
  16. queueio-0.5.0/queueio/broker_test.py +529 -0
  17. queueio-0.5.0/queueio/conftest.py +32 -0
  18. queueio-0.5.0/queueio/consumer.py +95 -0
  19. queueio-0.5.0/queueio/continuation.py +28 -0
  20. queueio-0.5.0/queueio/event.py +15 -0
  21. queueio-0.5.0/queueio/freethreading_test.py +28 -0
  22. queueio-0.5.0/queueio/gather.py +77 -0
  23. queueio-0.5.0/queueio/id.py +8 -0
  24. queueio-0.5.0/queueio/invocation.py +126 -0
  25. queueio-0.5.0/queueio/journal.py +23 -0
  26. queueio-0.5.0/queueio/journal_test.py +155 -0
  27. queueio-0.5.0/queueio/message.py +8 -0
  28. queueio-0.5.0/queueio/monitor.py +120 -0
  29. queueio-0.5.0/queueio/pause.py +21 -0
  30. queueio-0.5.0/queueio/pika/__init__.py +0 -0
  31. queueio-0.5.0/queueio/pika/broker.py +68 -0
  32. queueio-0.5.0/queueio/pika/broker_test.py +23 -0
  33. queueio-0.5.0/queueio/pika/journal.py +58 -0
  34. queueio-0.5.0/queueio/pika/journal_test.py +20 -0
  35. queueio-0.5.0/queueio/pika/receiver.py +80 -0
  36. queueio-0.5.0/queueio/pika/threadsafe.py +252 -0
  37. queueio-0.5.0/queueio/queue.py +183 -0
  38. queueio-0.5.0/queueio/queue_test.py +729 -0
  39. queueio-0.5.0/queueio/queueio.py +207 -0
  40. queueio-0.5.0/queueio/queueio_test.py +558 -0
  41. queueio-0.5.0/queueio/queuespec.md +31 -0
  42. queueio-0.5.0/queueio/queuespec.py +59 -0
  43. queueio-0.5.0/queueio/queuespec_test.py +67 -0
  44. queueio-0.5.0/queueio/queuevar.md +102 -0
  45. queueio-0.5.0/queueio/queuevar.py +71 -0
  46. queueio-0.5.0/queueio/receiver.py +47 -0
  47. queueio-0.5.0/queueio/registry.py +19 -0
  48. queueio-0.5.0/queueio/result.py +14 -0
  49. queueio-0.5.0/queueio/routine.py +16 -0
  50. queueio-0.5.0/queueio/samples/__init__.py +0 -0
  51. queueio-0.5.0/queueio/samples/basic.py +22 -0
  52. queueio-0.5.0/queueio/samples/basic_test.py +41 -0
  53. queueio-0.5.0/queueio/samples/expanded.py +45 -0
  54. queueio-0.5.0/queueio/samples/expanded_test.py +44 -0
  55. queueio-0.5.0/queueio/samples/priority.py +19 -0
  56. queueio-0.5.0/queueio/samples/priority_test.py +42 -0
  57. queueio-0.5.0/queueio/samples/queuevar.py +21 -0
  58. queueio-0.5.0/queueio/samples/queuevar_test.py +42 -0
  59. queueio-0.5.0/queueio/select.py +170 -0
  60. queueio-0.5.0/queueio/stream.py +76 -0
  61. queueio-0.5.0/queueio/stub/__init__.py +0 -0
  62. queueio-0.5.0/queueio/stub/broker.py +78 -0
  63. queueio-0.5.0/queueio/stub/broker_test.py +22 -0
  64. queueio-0.5.0/queueio/stub/journal.py +36 -0
  65. queueio-0.5.0/queueio/stub/journal_test.py +19 -0
  66. queueio-0.5.0/queueio/stub/receiver.py +83 -0
  67. queueio-0.5.0/queueio/suspension.py +33 -0
  68. queueio-0.5.0/queueio/thread.py +50 -0
  69. queueio-0.5.0/queueio/worker.py +249 -0
  70. {queueio-0.3.0 → queueio-0.5.0}/.gitignore +0 -0
  71. {queueio-0.3.0 → queueio-0.5.0}/LICENSE +0 -0
  72. {queueio-0.3.0 → queueio-0.5.0}/README.md +0 -0
  73. {queueio-0.3.0 → queueio-0.5.0}/logo.svg +0 -0
@@ -0,0 +1,9 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "mcp__acp__Bash",
5
+ "mcp__acp__Edit",
6
+ "mcp__acp__Write"
7
+ ]
8
+ }
9
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "dockerComposeFile": ["docker-compose.yml"],
3
+ "service": "dev",
4
+ "features": {
5
+ "ghcr.io/va-h/devcontainers-features/uv:1": {}
6
+ }
7
+ }
@@ -0,0 +1,10 @@
1
+ services:
2
+ dev:
3
+ image: mcr.microsoft.com/devcontainers/python
4
+ volumes:
5
+ - ../..:/workspaces
6
+ command: sleep infinity
7
+ rabbitmq:
8
+ image: rabbitmq:management
9
+ ports:
10
+ - 15672:15672
@@ -0,0 +1,42 @@
1
+ name: Build
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ build:
7
+ runs-on: ubuntu-latest
8
+ timeout-minutes: 5
9
+ services:
10
+ rabbitmq:
11
+ image: rabbitmq
12
+ env:
13
+ RABBITMQ_DEFAULT_USER: guest
14
+ RABBITMQ_DEFAULT_PASS: guest
15
+ ports:
16
+ - 5672:5672
17
+ options: >-
18
+ --health-cmd "rabbitmq-diagnostics -q ping"
19
+ --health-interval 10s
20
+ --health-timeout 5s
21
+ --health-retries 5
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+ - name: Install uv
25
+ uses: astral-sh/setup-uv@v3
26
+ - name: Set up Python
27
+ run: uv python install 3.14t
28
+ - name: Install dependencies
29
+ run: uv sync --all-groups
30
+ - name: Check formatting
31
+ run: uv run ruff format --check
32
+ - name: Run ruff linting
33
+ run: uv run ruff check
34
+ - name: Run basedpyright
35
+ run: uv run basedpyright
36
+ - name: Run tests
37
+ run: uv run pytest
38
+ timeout-minutes: 2
39
+ - name: Verify built package
40
+ run: |
41
+ uv build
42
+ uvx --from dist/queueio-*.whl queueio --help
@@ -0,0 +1,17 @@
1
+ name: Publish to PyPI
2
+ on:
3
+ release:
4
+ types:
5
+ - published
6
+
7
+ jobs:
8
+ publish:
9
+ permissions:
10
+ id-token: write
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: astral-sh/setup-uv@v5
15
+ - run: uv build
16
+ - name: Publish package distributions to PyPI
17
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,105 @@
1
+ Changelog
2
+ =========
3
+
4
+ All notable changes to this project will be documented in this file.
5
+
6
+ The format is loosely based on [Keep a Changelog](https://keepachangelog.com).
7
+
8
+ [Unreleased]
9
+ ------------
10
+
11
+ [0.5.0] - 2026-02-08
12
+ --------------------
13
+
14
+ ### Breaking Changes
15
+
16
+ - `invocation.priority` removed;
17
+ use `invocation.context[priority]` instead.
18
+ - `Invocation.Submitted.priority` replaced with `context`.
19
+ - Serialized invocations from previous versions are incompatible.
20
+
21
+ ### Added
22
+
23
+ - `QueueVar` for user-defined serializable context variables
24
+ that propagate across queue boundaries.
25
+ - `QueueContext` mapping to access captured context on invocations.
26
+ - `priority` is now a `QueueVar` instance
27
+ (same context manager syntax as before).
28
+
29
+ [0.4.0] - 2026-02-07
30
+ --------------------
31
+
32
+ ### Breaking Changes
33
+
34
+ - Serialized invocations from previous versions are incompatible.
35
+ - Queues must be recreated with `sync --recreate`
36
+ to support priority ordering.
37
+
38
+ ### Added
39
+
40
+ - Priority support for invocations (0-9, default 4).
41
+ Use the `priority()` context manager to set the priority
42
+ for invocations created within its scope.
43
+ Priority propagates to descendent invocations.
44
+ - `sync --recreate` flag to delete and recreate all queues.
45
+ - `Invocation.Submitted` events now include `priority`.
46
+
47
+ ### Fixed
48
+
49
+ - The `queueio` CLI entry point was broken
50
+ by a build configuration that excluded the package source.
51
+
52
+ [0.3.0] - 2026-02-06
53
+ --------------------
54
+
55
+ ### Added
56
+
57
+ - The journal now uses a threadsafe pika connection,
58
+ rather than guarding a blocking connection.
59
+ - The integration tests and examples have better queue names.
60
+
61
+ [0.2.2] - 2025-11-23
62
+ --------------------
63
+
64
+ ### Fixed
65
+
66
+ - Use the queueio logo from GitHub so it will show on PyPI.
67
+
68
+ [0.2.1] - 2025-11-22
69
+ --------------------
70
+
71
+ ### Fixed
72
+
73
+ - Include the queueio logo in the distribution so it will show on PyPI.
74
+
75
+ [0.2.0] - 2025-11-22
76
+ --------------------
77
+
78
+ ### Acknowledgement
79
+
80
+ Thank you to Nick Anderegg for allowing me to use the queueio name for this project.
81
+
82
+ ### Added
83
+
84
+ - `routine` decorator to declare sync or async functions as background routines.
85
+ - `activate` context manager to activate the queueio system.
86
+ - `pause` to coordinate a pause of a routine to queueio.
87
+ - `gather` to run multiple routines concurrently and gather the results.
88
+ - `Routine.submit()` method to submit a routine invocation to the queue.
89
+ - Configuration in the `tool.queueio` section of `pyproject.toml`.
90
+ - `pika` configures the pika library to connect to the AMQP broker.
91
+ - `register` configures the modules that declare routines.
92
+ - `QUEUEIO_PIKA` environment variable
93
+ to override the `pika` configuration in `pyproject.toml`.
94
+ - `queueio sync` command to synchronize queues to the broker.
95
+ - `queueio run` command to run the queueio worker.
96
+ - The queuespec syntax to `queue run` to consume multiple queues with shared capacity.
97
+ - `queueio monitor` command to monitor activity in the queueio system.
98
+
99
+ [Unreleased]: https://github.com/ryanhiebert/queueio/compare/tag/0.5.0...HEAD
100
+ [0.5.0]: https://github.com/ryanhiebert/queueio/compare/tag/0.4.0...tag/0.5.0
101
+ [0.4.0]: https://github.com/ryanhiebert/queueio/compare/tag/0.3.0...tag/0.4.0
102
+ [0.3.0]: https://github.com/ryanhiebert/queueio/compare/tag/0.2.2...tag/0.3.0
103
+ [0.2.2]: https://github.com/ryanhiebert/queueio/compare/tag/0.2.1...tag/0.2.2
104
+ [0.2.1]: https://github.com/ryanhiebert/queueio/compare/tag/0.2.0...tag/0.2.1
105
+ [0.2.0]: https://github.com/ryanhiebert/queueio/releases/tag/0.2.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: queueio
3
- Version: 0.3.0
3
+ Version: 0.5.0
4
4
  Summary: Python background queues with an async twist
5
5
  Author-email: Ryan Hiebert <ryan@ryanhiebert.com>
6
6
  License-Expression: MIT
queueio-0.5.0/dill.pyi ADDED
@@ -0,0 +1,4 @@
1
+ from typing import Any
2
+
3
+ def dumps(obj: Any) -> bytes: ...
4
+ def loads(data: bytes) -> Any: ...
@@ -0,0 +1,32 @@
1
+ Invocations
2
+ ===========
3
+
4
+ When you call a routine, it returns an `Invocation`
5
+ that can be submitted or awaited.
6
+
7
+ ```python
8
+ from basic import yielding
9
+
10
+ # Submit directly
11
+ yielding(7).submit()
12
+
13
+ # Or await from an async routine
14
+ await yielding(7)
15
+ ```
16
+
17
+ Priority
18
+ --------
19
+
20
+ Invocations have a priority from 0 (lowest) to 9 (highest),
21
+ defaulting to 4.
22
+ Set priority using the `priority()` context manager:
23
+
24
+ ```python
25
+ from queueio import priority
26
+
27
+ with priority(7):
28
+ yielding(7).submit()
29
+ await yielding(7)
30
+ ```
31
+
32
+ Priority propagates to descendent invocations.
@@ -0,0 +1,28 @@
1
+ Principles
2
+ ==========
3
+
4
+ There's been a great deal of momentum to embrace async functions for IO,
5
+ but they split the world and are confusing, especially to new developers.
6
+ Threads or green threads are often a better choice for concurrency
7
+ because it doesn't require learning about coroutines
8
+ before you even need concurrency.
9
+
10
+ Still, async functions are a powerful way to implement state machines.
11
+ Complex workflows can be represented as async functions
12
+ to allow thinking about things in a pull-based fashion,
13
+ while also scaling well for processes that must take a long time.
14
+ Ideally, running coroutines could be serialized and resumed,
15
+ perhaps even on a different machine.
16
+
17
+ This vision motivates queueio.
18
+ Routines need to coordinate with the worker
19
+ because queue-level concurrency must be carefully managed.
20
+ Instead of complex construction of pipelines to make this possible,
21
+ queueio allows you to write complex workflows
22
+ in a traditional imperative style.
23
+
24
+ queueio encourages you to use synchronous IO in routines.
25
+ The yield points in async functions are only needed
26
+ when a routine needs to coordinate with the worker,
27
+ such as to indicate that the worker can use
28
+ the capacity reserved for it to do other work.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "queueio"
3
- version = "0.3.0"
3
+ version = "0.5.0"
4
4
  description = "Python background queues with an async twist"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -51,6 +51,8 @@ force-single-line = true
51
51
  register = [
52
52
  "queueio.samples.basic",
53
53
  "queueio.samples.expanded",
54
+ "queueio.samples.priority",
55
+ "queueio.samples.queuevar",
54
56
  ]
55
57
  pika = "amqp://localhost:5672"
56
58
 
@@ -61,9 +63,6 @@ timeout = 30
61
63
  branch = true
62
64
  source = ["queueio"]
63
65
 
64
- [tool.hatch.build]
65
- include = ["logo.svg"]
66
-
67
66
  [build-system]
68
67
  requires = ["hatchling"]
69
68
  build-backend = "hatchling.build"
@@ -0,0 +1,4 @@
1
+ {
2
+ "pythonVersion": "3.14",
3
+ "typeCheckingMode": "standard"
4
+ }
@@ -0,0 +1,14 @@
1
+ from contextlib import contextmanager
2
+
3
+ from .gather import gather as gather
4
+ from .pause import pause as pause
5
+ from .queueio import QueueIO as RealQueueIO
6
+ from .queueio import priority as priority
7
+ from .queuevar import QueueVar as QueueVar
8
+ from .registry import routine as routine
9
+
10
+
11
+ @contextmanager
12
+ def activate():
13
+ with RealQueueIO().activate():
14
+ yield
@@ -0,0 +1,184 @@
1
+ from contextlib import suppress
2
+ from typing import Annotated
3
+
4
+ import typer
5
+ from typer import Argument
6
+ from typer import Typer
7
+
8
+ from .monitor import Monitor
9
+ from .queueio import QueueIO
10
+ from .queuespec import QueueSpec
11
+ from .worker import Worker
12
+
13
+ app = Typer()
14
+
15
+ routine_app = Typer()
16
+ queue_app = Typer()
17
+
18
+ app.add_typer(
19
+ routine_app,
20
+ name="routine",
21
+ help="A function to coordinate background execution.",
22
+ rich_help_panel="Entities",
23
+ )
24
+ app.add_typer(
25
+ queue_app,
26
+ name="queue",
27
+ help="An ordered collection of work items to process.",
28
+ rich_help_panel="Entities",
29
+ )
30
+
31
+
32
+ @routine_app.command("list")
33
+ def routine_list():
34
+ """Show all registered routines."""
35
+ queueio = QueueIO()
36
+ try:
37
+ routines = queueio.routines()
38
+
39
+ if not routines:
40
+ print("No routines registered.")
41
+ return
42
+
43
+ # Calculate column widths
44
+ name_width = max(len("Name"), max(len(routine.name) for routine in routines))
45
+ function_paths = []
46
+ for routine in routines:
47
+ module = routine.fn.__module__
48
+ qualname = routine.fn.__qualname__
49
+ function_paths.append(f"{module}.{qualname}")
50
+ path_width = max(len("Path"), max(len(path) for path in function_paths))
51
+
52
+ print(f"{'Name':<{name_width}} | {'Path':<{path_width}}")
53
+ print(f"{'-' * name_width}-+-{'-' * path_width}")
54
+ for routine, path in zip(routines, function_paths, strict=False):
55
+ print(f"{routine.name:<{name_width}} | {path:<{path_width}}")
56
+ finally:
57
+ queueio.shutdown()
58
+
59
+
60
+ @app.command(rich_help_panel="Commands")
61
+ def monitor(raw: bool = False):
62
+ """Monitor queueio events.
63
+
64
+ Show a live view of queueio activity. Use --raw for detailed event output.
65
+ """
66
+ if raw:
67
+ queueio = QueueIO()
68
+ events = queueio.subscribe({object})
69
+ try:
70
+ while True:
71
+ print(events.get())
72
+ except KeyboardInterrupt:
73
+ print("Shutting down gracefully.")
74
+ finally:
75
+ queueio.shutdown()
76
+ else:
77
+ Monitor().run()
78
+
79
+
80
+ @app.command(rich_help_panel="Commands")
81
+ def run(
82
+ queuespec: Annotated[
83
+ QueueSpec,
84
+ Argument(
85
+ parser=QueueSpec.parse,
86
+ help="Queue configuration in format 'queue=concurrency'. "
87
+ "Examples: 'production=10', 'api,background=5'",
88
+ metavar="QUEUE[,QUEUE2,...]=CONCURRENCY",
89
+ ),
90
+ ],
91
+ ):
92
+ """Run a worker to process from a queue.
93
+
94
+ The worker will process invocations from the specified queue,
95
+ as many at a time as specified by the concurrency.
96
+ """
97
+ queueio = QueueIO()
98
+ Worker(queueio, queuespec)()
99
+
100
+
101
+ @app.command(rich_help_panel="Commands")
102
+ def sync(
103
+ recreate: Annotated[
104
+ bool,
105
+ typer.Option(
106
+ help="Delete and recreate queues that have incompatible arguments. "
107
+ "WARNING: This will lose any pending messages in those queues.",
108
+ ),
109
+ ] = False,
110
+ ):
111
+ """Sync known queues to the broker."""
112
+ queueio = QueueIO()
113
+ try:
114
+ routines = queueio.routines()
115
+
116
+ if not routines:
117
+ print("No routines registered.")
118
+ return
119
+
120
+ queues = sorted({routine.queue for routine in routines})
121
+
122
+ print(f"Syncing queues for {len(routines)} routine(s):")
123
+ if recreate:
124
+ for queue in queues:
125
+ print(f" Recreating queue: {queue}")
126
+ with suppress(Exception):
127
+ queueio.delete(queue=queue)
128
+
129
+ failed = []
130
+ for queue in queues:
131
+ print(f" Ensuring queue exists: {queue}")
132
+ try:
133
+ queueio.create(queue=queue)
134
+ except Exception:
135
+ failed.append(queue)
136
+
137
+ if failed:
138
+ print(
139
+ f"\nError: {len(failed)} queue(s) have incompatible arguments: "
140
+ f"{', '.join(failed)}\n"
141
+ f"Re-run with --recreate to delete and recreate them.\n"
142
+ f"WARNING: This will lose any pending messages in those queues."
143
+ )
144
+ raise typer.Exit(1)
145
+
146
+ print(f"Successfully synced {len(queues)} queue(s)")
147
+ finally:
148
+ queueio.shutdown()
149
+
150
+
151
+ @queue_app.command("purge")
152
+ def queue_purge(
153
+ queues: Annotated[
154
+ str,
155
+ Argument(
156
+ help="Comma-separated list of queues to purge. "
157
+ "Examples: 'queueio', 'production,background'",
158
+ metavar="QUEUE[,QUEUE2,...]",
159
+ ),
160
+ ],
161
+ ):
162
+ """Purge all messages from some queues.
163
+
164
+ This will remove all pending messages from the given queues.
165
+ Use with caution as this operation cannot be undone.
166
+ """
167
+ queueio = QueueIO()
168
+ try:
169
+ queue_list = [q.strip() for q in queues.split(",") if q.strip()]
170
+ if not queue_list:
171
+ print("Error: No valid queue names provided")
172
+ return
173
+
174
+ for queue in queue_list:
175
+ print(f"Purging queue: {queue}")
176
+ queueio.purge(queue=queue)
177
+
178
+ print(f"Successfully purged {len(queue_list)} queue(s)")
179
+ finally:
180
+ queueio.shutdown()
181
+
182
+
183
+ if __name__ == "__main__":
184
+ app()
@@ -0,0 +1,50 @@
1
+ from abc import ABC
2
+ from abc import abstractmethod
3
+
4
+ from .queuespec import QueueSpec
5
+ from .receiver import Receiver
6
+
7
+
8
+ class Broker(ABC):
9
+ """A broker enables sending and receiving messages with a queue.
10
+
11
+ Messages sent by a broker are assumed to be idempotent, and may be
12
+ delivered multiple times in some conditions in order to ensure
13
+ at-least-once delivery.
14
+ """
15
+
16
+ @classmethod
17
+ @abstractmethod
18
+ def from_uri(cls, uri: str, /):
19
+ """Create a broker instance from a URI."""
20
+ raise NotImplementedError("Subclasses must implement this method.")
21
+
22
+ @abstractmethod
23
+ def enqueue(self, body: bytes, /, *, queue: str, priority: int):
24
+ """Enqueue a message."""
25
+ raise NotImplementedError("Subclasses must implement this method.")
26
+
27
+ @abstractmethod
28
+ def create(self, *, queue: str):
29
+ """Create a queue if it doesn't exist."""
30
+ raise NotImplementedError("Subclasses must implement this method.")
31
+
32
+ @abstractmethod
33
+ def delete(self, *, queue: str):
34
+ """Delete a queue."""
35
+ raise NotImplementedError("Subclasses must implement this method.")
36
+
37
+ @abstractmethod
38
+ def purge(self, *, queue: str):
39
+ """Purge all messages from the queue."""
40
+ raise NotImplementedError("Subclasses must implement this method.")
41
+
42
+ @abstractmethod
43
+ def receive(self, queuespec: QueueSpec, /) -> Receiver:
44
+ """Receive messages as specified by the QueueSpec."""
45
+ raise NotImplementedError("Subclasses must implement this method.")
46
+
47
+ @abstractmethod
48
+ def shutdown(self):
49
+ """Signal the final shutdown of the broker."""
50
+ raise NotImplementedError("Subclasses must implement this method.")