jqueue 0.1.0__tar.gz → 1.0.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.
- {jqueue-0.1.0 → jqueue-1.0.0}/.github/workflows/release.yml +2 -0
- {jqueue-0.1.0 → jqueue-1.0.0}/.gitignore +1 -0
- jqueue-1.0.0/AGENTS.md +251 -0
- jqueue-1.0.0/PKG-INFO +1026 -0
- jqueue-1.0.0/README.md +991 -0
- {jqueue-0.1.0 → jqueue-1.0.0}/jqueue/__init__.py +7 -0
- jqueue-1.0.0/jqueue/_version.py +34 -0
- {jqueue-0.1.0 → jqueue-1.0.0}/pyproject.toml +12 -2
- jqueue-1.0.0/tools/benchmark_storage.py +720 -0
- {jqueue-0.1.0 → jqueue-1.0.0}/uv.lock +1 -2
- jqueue-0.1.0/PKG-INFO +0 -712
- jqueue-0.1.0/README.md +0 -677
- {jqueue-0.1.0 → jqueue-1.0.0}/.github/workflows/ci.yml +0 -0
- {jqueue-0.1.0 → jqueue-1.0.0}/LICENSE +0 -0
- {jqueue-0.1.0 → jqueue-1.0.0}/jqueue/adapters/__init__.py +0 -0
- {jqueue-0.1.0 → jqueue-1.0.0}/jqueue/adapters/storage/__init__.py +0 -0
- {jqueue-0.1.0 → jqueue-1.0.0}/jqueue/adapters/storage/filesystem.py +0 -0
- {jqueue-0.1.0 → jqueue-1.0.0}/jqueue/adapters/storage/gcs.py +0 -0
- {jqueue-0.1.0 → jqueue-1.0.0}/jqueue/adapters/storage/memory.py +0 -0
- {jqueue-0.1.0 → jqueue-1.0.0}/jqueue/adapters/storage/s3.py +0 -0
- {jqueue-0.1.0 → jqueue-1.0.0}/jqueue/core/__init__.py +0 -0
- {jqueue-0.1.0 → jqueue-1.0.0}/jqueue/core/broker.py +0 -0
- {jqueue-0.1.0 → jqueue-1.0.0}/jqueue/core/codec.py +0 -0
- {jqueue-0.1.0 → jqueue-1.0.0}/jqueue/core/direct.py +0 -0
- {jqueue-0.1.0 → jqueue-1.0.0}/jqueue/core/group_commit.py +0 -0
- {jqueue-0.1.0 → jqueue-1.0.0}/jqueue/core/heartbeat.py +0 -0
- {jqueue-0.1.0 → jqueue-1.0.0}/jqueue/domain/__init__.py +0 -0
- {jqueue-0.1.0 → jqueue-1.0.0}/jqueue/domain/errors.py +0 -0
- {jqueue-0.1.0 → jqueue-1.0.0}/jqueue/domain/models.py +0 -0
- {jqueue-0.1.0 → jqueue-1.0.0}/jqueue/ports/__init__.py +0 -0
- {jqueue-0.1.0 → jqueue-1.0.0}/jqueue/ports/storage.py +0 -0
- {jqueue-0.1.0 → jqueue-1.0.0}/ports-and-adapters.md +0 -0
- {jqueue-0.1.0 → jqueue-1.0.0}/tests/__init__.py +0 -0
- {jqueue-0.1.0 → jqueue-1.0.0}/tests/test_broker_queue.py +0 -0
- {jqueue-0.1.0 → jqueue-1.0.0}/tests/test_codec.py +0 -0
- {jqueue-0.1.0 → jqueue-1.0.0}/tests/test_direct_queue.py +0 -0
- {jqueue-0.1.0 → jqueue-1.0.0}/tests/test_domain_errors.py +0 -0
- {jqueue-0.1.0 → jqueue-1.0.0}/tests/test_domain_models.py +0 -0
- {jqueue-0.1.0 → jqueue-1.0.0}/tests/test_group_commit.py +0 -0
- {jqueue-0.1.0 → jqueue-1.0.0}/tests/test_heartbeat.py +0 -0
- {jqueue-0.1.0 → jqueue-1.0.0}/tests/test_storage_filesystem.py +0 -0
- {jqueue-0.1.0 → jqueue-1.0.0}/tests/test_storage_gcs.py +0 -0
- {jqueue-0.1.0 → jqueue-1.0.0}/tests/test_storage_memory.py +0 -0
- {jqueue-0.1.0 → jqueue-1.0.0}/tests/test_storage_s3.py +0 -0
jqueue-1.0.0/AGENTS.md
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# AGENTS.md — Coding Agent Guidelines for jqueue
|
|
2
|
+
|
|
3
|
+
This document provides coding agents with essential information about the jqueue codebase: build commands, code style, architecture patterns, and conventions.
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
**jqueue** is a storage-agnostic async job queue using object storage with compare-and-set (CAS) semantics. It implements the [turbopuffer object-storage queue pattern](https://turbopuffer.com/blog/object-storage-queue).
|
|
8
|
+
|
|
9
|
+
- **Language**: Python 3.12+ (strictly typed)
|
|
10
|
+
- **Framework**: AsyncIO with async/await throughout
|
|
11
|
+
- **Architecture**: Ports & Adapters (Hexagonal Architecture)
|
|
12
|
+
- **Package Manager**: `uv` (NOT pip or poetry)
|
|
13
|
+
|
|
14
|
+
## Build, Test, and Lint Commands
|
|
15
|
+
|
|
16
|
+
### Development Setup
|
|
17
|
+
```bash
|
|
18
|
+
# Install dev dependencies
|
|
19
|
+
uv sync --extra dev
|
|
20
|
+
|
|
21
|
+
# Install all extras (s3, gcs, dev)
|
|
22
|
+
uv sync --extra s3 --extra gcs --extra dev
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Testing
|
|
26
|
+
```bash
|
|
27
|
+
# Run all tests
|
|
28
|
+
uv run pytest tests/ -v
|
|
29
|
+
|
|
30
|
+
# Run a single test file
|
|
31
|
+
uv run pytest tests/test_broker_queue.py -v
|
|
32
|
+
|
|
33
|
+
# Run a single test function
|
|
34
|
+
uv run pytest tests/test_broker_queue.py::test_enqueue_returns_job -v
|
|
35
|
+
|
|
36
|
+
# Run tests matching a pattern
|
|
37
|
+
uv run pytest tests/ -v -k "enqueue"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Linting and Formatting
|
|
41
|
+
```bash
|
|
42
|
+
# Check formatting (don't modify files)
|
|
43
|
+
uv run ruff format --check .
|
|
44
|
+
|
|
45
|
+
# Apply formatting
|
|
46
|
+
uv run ruff format .
|
|
47
|
+
|
|
48
|
+
# Lint code (checks E, F, I, UP rules)
|
|
49
|
+
uv run ruff check .
|
|
50
|
+
|
|
51
|
+
# Auto-fix lint issues
|
|
52
|
+
uv run ruff check --fix .
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Type Checking
|
|
56
|
+
```bash
|
|
57
|
+
# Run strict type checking
|
|
58
|
+
uv run mypy .
|
|
59
|
+
|
|
60
|
+
# Type check a specific file
|
|
61
|
+
uv run mypy jqueue/core/broker.py
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Building
|
|
65
|
+
```bash
|
|
66
|
+
# Build distribution packages
|
|
67
|
+
uv build
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Architecture Pattern
|
|
71
|
+
|
|
72
|
+
jqueue follows **Ports & Adapters** (Hexagonal Architecture):
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
jqueue/
|
|
76
|
+
├── domain/ # Pure domain models and errors (Pydantic-based)
|
|
77
|
+
│ ├── models.py # Job, QueueState, JobStatus
|
|
78
|
+
│ └── errors.py # JQueueError, CASConflictError, JobNotFoundError, StorageError
|
|
79
|
+
├── ports/ # Protocol interfaces (structural typing)
|
|
80
|
+
│ └── storage.py # ObjectStoragePort (the single port)
|
|
81
|
+
├── core/ # Business logic
|
|
82
|
+
│ ├── broker.py # BrokerQueue (high-throughput with group commit)
|
|
83
|
+
│ ├── direct.py # DirectQueue (simple one-operation-per-write)
|
|
84
|
+
│ ├── group_commit.py # Group commit batching algorithm
|
|
85
|
+
│ ├── heartbeat.py # HeartbeatManager for job liveness
|
|
86
|
+
│ └── codec.py # JSON serialization
|
|
87
|
+
└── adapters/ # Concrete implementations
|
|
88
|
+
└── storage/
|
|
89
|
+
├── memory.py # InMemoryStorage (testing)
|
|
90
|
+
├── filesystem.py # LocalFileSystemStorage (POSIX)
|
|
91
|
+
├── s3.py # S3Storage (requires aioboto3)
|
|
92
|
+
└── gcs.py # GCSStorage (requires google-cloud-storage)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Key Principles**:
|
|
96
|
+
- **Immutable domain models**: Pydantic with `frozen=True`
|
|
97
|
+
- **Protocol-based interfaces**: No inheritance, structural typing only
|
|
98
|
+
- **Dependency injection**: Via constructor parameters
|
|
99
|
+
- **Async-first**: All I/O operations use async/await
|
|
100
|
+
- **CAS semantics**: Compare-and-set for concurrency safety
|
|
101
|
+
|
|
102
|
+
## Code Style Guidelines
|
|
103
|
+
|
|
104
|
+
### Imports
|
|
105
|
+
- Use absolute imports: `from jqueue.domain.models import Job`
|
|
106
|
+
- Group imports: stdlib → third-party → local
|
|
107
|
+
- Use `from __future__ import annotations` for forward references
|
|
108
|
+
- Ruff enforces isort-style import ordering (rule `I`)
|
|
109
|
+
|
|
110
|
+
### Type Hints
|
|
111
|
+
- **Strict typing**: All functions require type hints (mypy strict mode)
|
|
112
|
+
- Use `X | None` (not `Optional[X]`) — Python 3.10+ union syntax
|
|
113
|
+
- Use Protocol for interfaces, not abstract base classes
|
|
114
|
+
- Use `tuple[X, ...]` for variable-length homogeneous tuples
|
|
115
|
+
- Use `list[X]`, `dict[K, V]`, not `List`, `Dict` from typing
|
|
116
|
+
- Return type annotations are mandatory, including `-> None`
|
|
117
|
+
|
|
118
|
+
### Naming Conventions
|
|
119
|
+
- **Functions/methods**: `snake_case`
|
|
120
|
+
- **Classes**: `PascalCase`
|
|
121
|
+
- **Constants**: `UPPER_SNAKE_CASE`
|
|
122
|
+
- **Private attributes**: prefix with single underscore (`_lock`, `_task`)
|
|
123
|
+
- **Domain concepts**: Use domain language (Job, QueueState, entrypoint)
|
|
124
|
+
|
|
125
|
+
### Docstrings
|
|
126
|
+
- **Module-level**: Explain purpose and usage
|
|
127
|
+
- **Class-level**: Brief description + Parameters section
|
|
128
|
+
- **Public methods**: Use Google/NumPy style with sections:
|
|
129
|
+
```python
|
|
130
|
+
"""
|
|
131
|
+
Brief one-line summary.
|
|
132
|
+
|
|
133
|
+
Longer description if needed.
|
|
134
|
+
|
|
135
|
+
Parameters
|
|
136
|
+
----------
|
|
137
|
+
param_name : type
|
|
138
|
+
Description
|
|
139
|
+
|
|
140
|
+
Returns
|
|
141
|
+
-------
|
|
142
|
+
return_type
|
|
143
|
+
Description
|
|
144
|
+
|
|
145
|
+
Raises
|
|
146
|
+
------
|
|
147
|
+
ExceptionType
|
|
148
|
+
When this happens
|
|
149
|
+
"""
|
|
150
|
+
```
|
|
151
|
+
- Private methods may omit docstrings if purpose is obvious
|
|
152
|
+
|
|
153
|
+
### Error Handling
|
|
154
|
+
- All custom exceptions inherit from `JQueueError`
|
|
155
|
+
- Use specific exceptions: `CASConflictError`, `JobNotFoundError`, `StorageError`
|
|
156
|
+
- Wrap external exceptions in `StorageError` with cause:
|
|
157
|
+
```python
|
|
158
|
+
try:
|
|
159
|
+
await external_operation()
|
|
160
|
+
except Exception as e:
|
|
161
|
+
raise StorageError("Operation failed", cause=e) from e
|
|
162
|
+
```
|
|
163
|
+
- Document raised exceptions in docstrings
|
|
164
|
+
|
|
165
|
+
### Immutability Pattern
|
|
166
|
+
Domain models are frozen (immutable). Use functional update style:
|
|
167
|
+
```python
|
|
168
|
+
# Good: Return new instance
|
|
169
|
+
updated_job = job.model_copy(update={"status": JobStatus.IN_PROGRESS})
|
|
170
|
+
# or use helper methods
|
|
171
|
+
updated_job = job.with_status(JobStatus.IN_PROGRESS)
|
|
172
|
+
|
|
173
|
+
# Bad: Don't mutate
|
|
174
|
+
job.status = JobStatus.IN_PROGRESS # ❌ Raises FrozenInstanceError
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Async/Await
|
|
178
|
+
- All I/O operations must be async
|
|
179
|
+
- Use `async with` for context managers (BrokerQueue, HeartbeatManager)
|
|
180
|
+
- Use `asyncio.gather()` for concurrent operations
|
|
181
|
+
- Mark async functions with `async def`, never use sync wrappers
|
|
182
|
+
|
|
183
|
+
### Data Classes and Models
|
|
184
|
+
- Use `@dataclasses.dataclass` for simple containers
|
|
185
|
+
- Use Pydantic `BaseModel` for domain models (validation, serialization)
|
|
186
|
+
- Pydantic models: set `model_config = ConfigDict(frozen=True)`
|
|
187
|
+
- Dataclasses: no frozen required unless immutability needed
|
|
188
|
+
|
|
189
|
+
## Testing Conventions
|
|
190
|
+
|
|
191
|
+
### Test File Organization
|
|
192
|
+
- Location: `/tests/` (flat directory)
|
|
193
|
+
- Naming: `test_<module>.py` (e.g., `test_broker_queue.py`)
|
|
194
|
+
- Use section comments to group related tests:
|
|
195
|
+
```python
|
|
196
|
+
# ---------------------------------------------------------------------------
|
|
197
|
+
# enqueue operations
|
|
198
|
+
# ---------------------------------------------------------------------------
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Test Function Naming
|
|
202
|
+
- Descriptive names: `test_enqueue_returns_job`, `test_dequeue_empty_returns_empty_list`
|
|
203
|
+
- Return type annotation: `-> None`
|
|
204
|
+
|
|
205
|
+
### Test Patterns
|
|
206
|
+
```python
|
|
207
|
+
async def test_operation_behavior() -> None:
|
|
208
|
+
# Arrange
|
|
209
|
+
storage = InMemoryStorage()
|
|
210
|
+
async with BrokerQueue(storage) as q:
|
|
211
|
+
# Act
|
|
212
|
+
result = await q.enqueue("task", b"data")
|
|
213
|
+
|
|
214
|
+
# Assert
|
|
215
|
+
assert result.status == JobStatus.QUEUED
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Fixtures and Helpers
|
|
219
|
+
- Use pytest fixtures for shared setup
|
|
220
|
+
- Use `InMemoryStorage()` for tests (zero dependencies)
|
|
221
|
+
- Test async code with `pytest-asyncio` (auto mode enabled)
|
|
222
|
+
|
|
223
|
+
## Common Patterns
|
|
224
|
+
|
|
225
|
+
### Creating a Queue
|
|
226
|
+
```python
|
|
227
|
+
from jqueue import BrokerQueue, InMemoryStorage
|
|
228
|
+
|
|
229
|
+
async with BrokerQueue(InMemoryStorage()) as q:
|
|
230
|
+
job = await q.enqueue("send_email", b'{"to": "user@example.com"}')
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Implementing a Storage Adapter
|
|
234
|
+
Implement the `ObjectStoragePort` Protocol:
|
|
235
|
+
```python
|
|
236
|
+
class CustomStorage:
|
|
237
|
+
async def read(self) -> tuple[bytes, str | None]:
|
|
238
|
+
"""Return (content, etag). etag is None if object doesn't exist."""
|
|
239
|
+
...
|
|
240
|
+
|
|
241
|
+
async def write(self, content: bytes, if_match: str | None = None) -> str:
|
|
242
|
+
"""CAS write. Raise CASConflictError if if_match doesn't match."""
|
|
243
|
+
...
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## References
|
|
247
|
+
|
|
248
|
+
- **Architecture**: See `ports-and-adapters.md` for detailed design rationale
|
|
249
|
+
- **README**: Comprehensive usage examples and API documentation
|
|
250
|
+
- **Repository**: https://github.com/janbjorge/jqueue
|
|
251
|
+
- **License**: MIT
|