sheppy 0.0.1__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.
- sheppy-0.0.1/.gitignore +16 -0
- sheppy-0.0.1/LICENSE +21 -0
- sheppy-0.0.1/PKG-INFO +205 -0
- sheppy-0.0.1/README.md +191 -0
- sheppy-0.0.1/pyproject.toml +81 -0
- sheppy-0.0.1/src/sheppy/__init__.py +24 -0
- sheppy-0.0.1/src/sheppy/backend/__init__.py +10 -0
- sheppy-0.0.1/src/sheppy/backend/base.py +91 -0
- sheppy-0.0.1/src/sheppy/backend/memory.py +290 -0
- sheppy-0.0.1/src/sheppy/backend/redis.py +451 -0
- sheppy-0.0.1/src/sheppy/cli/__main__.py +3 -0
- sheppy-0.0.1/src/sheppy/cli/cli.py +55 -0
- sheppy-0.0.1/src/sheppy/cli/commands/cron/list.py +53 -0
- sheppy-0.0.1/src/sheppy/cli/commands/queue/list.py +41 -0
- sheppy-0.0.1/src/sheppy/cli/commands/task/add.py +103 -0
- sheppy-0.0.1/src/sheppy/cli/commands/task/info.py +78 -0
- sheppy-0.0.1/src/sheppy/cli/commands/task/list.py +106 -0
- sheppy-0.0.1/src/sheppy/cli/commands/task/retry.py +65 -0
- sheppy-0.0.1/src/sheppy/cli/commands/task/schedule.py +122 -0
- sheppy-0.0.1/src/sheppy/cli/commands/task/test.py +100 -0
- sheppy-0.0.1/src/sheppy/cli/commands/work.py +102 -0
- sheppy-0.0.1/src/sheppy/cli/utils.py +75 -0
- sheppy-0.0.1/src/sheppy/models.py +150 -0
- sheppy-0.0.1/src/sheppy/py.typed +0 -0
- sheppy-0.0.1/src/sheppy/queue.py +200 -0
- sheppy-0.0.1/src/sheppy/task_factory.py +145 -0
- sheppy-0.0.1/src/sheppy/testqueue.py +167 -0
- sheppy-0.0.1/src/sheppy/utils/fastapi.py +18 -0
- sheppy-0.0.1/src/sheppy/utils/task_execution.py +285 -0
- sheppy-0.0.1/src/sheppy/utils/validation.py +133 -0
- sheppy-0.0.1/src/sheppy/worker.py +326 -0
sheppy-0.0.1/.gitignore
ADDED
sheppy-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 malvex
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
sheppy-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sheppy
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Author-email: Martin <15851033+malvex@users.noreply.github.com>
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Requires-Dist: anyio>=3.0.1
|
|
8
|
+
Requires-Dist: croniter>=6.0.0
|
|
9
|
+
Requires-Dist: pydantic>=2.1.1
|
|
10
|
+
Requires-Dist: redis>=6.0.0
|
|
11
|
+
Requires-Dist: typer>=0.16.1
|
|
12
|
+
Requires-Dist: watchfiles>=1.1.0
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# Sheppy 🐕
|
|
16
|
+
|
|
17
|
+
Sheppy is a fast, modern, and easy to use Python task queue that's simple enough to learn in minutes, yet scales to millions of tasks. It is designed to be as simple as possible, with sane defaults, while providing all essential features for working with background tasks.
|
|
18
|
+
|
|
19
|
+
## Why Sheppy?
|
|
20
|
+
|
|
21
|
+
- **Blazing Fast**: The fastest Python task queue without compromises
|
|
22
|
+
- **Async Native**: Built on asyncio from the ground up - no async backporting, just modern Python
|
|
23
|
+
- **Dead Simple**: Just `@task` decorator and `Queue` - that's it
|
|
24
|
+
- **Type Safe**: Full Pydantic integration for automatic validation and serialization
|
|
25
|
+
- **Multi-backend Support**: Native support for different backends (note: only Redis is implemented at this time)
|
|
26
|
+
- **FastAPI Compatible**: Seamless integration with FastAPI's dependency injection
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install sheppy
|
|
32
|
+
# or if you're using uv:
|
|
33
|
+
uv add sheppy
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## TL;DR Quick Start
|
|
37
|
+
|
|
38
|
+
This is all you need to know:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
import asyncio
|
|
42
|
+
from datetime import datetime, timedelta
|
|
43
|
+
from sheppy import Queue, task, RedisBackend
|
|
44
|
+
|
|
45
|
+
queue = Queue(RedisBackend("redis://127.0.0.1:6379"))
|
|
46
|
+
|
|
47
|
+
@task
|
|
48
|
+
async def say_hello(to: str) -> str:
|
|
49
|
+
s = f"Hello, {to}!"
|
|
50
|
+
print(s)
|
|
51
|
+
return s
|
|
52
|
+
|
|
53
|
+
async def main():
|
|
54
|
+
t1 = say_hello("World")
|
|
55
|
+
await queue.add(t1)
|
|
56
|
+
await queue.add(say_hello("Moon"))
|
|
57
|
+
await queue.schedule(say_hello("Patient Person"), at=timedelta(seconds=10)) # runs in 10 seconds from now
|
|
58
|
+
await queue.schedule(say_hello("New Year"), at=datetime.fromisoformat("2026-01-01 00:00:00 +00:00"))
|
|
59
|
+
|
|
60
|
+
# await the task completion
|
|
61
|
+
updated_task = await queue.wait_for(t1)
|
|
62
|
+
|
|
63
|
+
if updated_task.error:
|
|
64
|
+
print(f"Task failed with error: {updated_task.error}")
|
|
65
|
+
elif updated_task.completed:
|
|
66
|
+
print(f"Task succeed with result: {updated_task.result}")
|
|
67
|
+
assert updated_task.result == "Hello, World!"
|
|
68
|
+
else:
|
|
69
|
+
# note: this won't happen though because wait_for doesn't return pending tasks
|
|
70
|
+
print("Task is still pending!")
|
|
71
|
+
|
|
72
|
+
if __name__ == "__main__":
|
|
73
|
+
asyncio.run(main())
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Run it:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# run the app:
|
|
80
|
+
python examples/tldr.py # nothing will happen because worker isn't running
|
|
81
|
+
|
|
82
|
+
# in another terminal, you can list queued tasks:
|
|
83
|
+
sheppy task list # (shows 2 pending and 2 scheduled tasks)
|
|
84
|
+
|
|
85
|
+
# run worker process to process the tasks
|
|
86
|
+
sheppy work # (you should see the tasks to get processed, and the app should finish!)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
And that's it!
|
|
90
|
+
|
|
91
|
+
## Tl;Dr multiple queues (including priority queues)
|
|
92
|
+
|
|
93
|
+
Task can have different priority by simply defining multiple queues with different priority.
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
backend = RedisBackend("redis://127.0.0.1:6379")
|
|
97
|
+
different_backend = RedisBackend("redis://10.10.10.10:6379")
|
|
98
|
+
|
|
99
|
+
email_queue = Queue(backend, name="email-queue")
|
|
100
|
+
high_priority_email_queue = Queue(backend, name="high-priority-email-queue")
|
|
101
|
+
data_exports_queue = Queue(different_backend, name="data-exports")
|
|
102
|
+
|
|
103
|
+
normal_email = send_email(Email(...))
|
|
104
|
+
important_email = send_email(Email(...))
|
|
105
|
+
data_export = export_data(user_id=1234)
|
|
106
|
+
|
|
107
|
+
await email_queue.add(normal_email)
|
|
108
|
+
await high_priority_email_queue.add(important_email)
|
|
109
|
+
await data_exports_queue.add(data_export)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
And then you run workers like this:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
# the first queue in the arg list will be always processed first
|
|
116
|
+
# the second queue in the arg list will be processed only if first queue is empty
|
|
117
|
+
sheppy work -q "high-priority-email-queue" -q "email-queue" --redis-url "redis://127.0.0.1:6379"
|
|
118
|
+
|
|
119
|
+
# in separate terminal, we run worker for different queue with a different backend
|
|
120
|
+
sheppy work -q "data-exports" --redis-url "redis://10.10.10.10:6379"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Type Safety with Pydantic
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from pydantic import BaseModel
|
|
127
|
+
from sheppy import task
|
|
128
|
+
|
|
129
|
+
class UserData(BaseModel):
|
|
130
|
+
name: str
|
|
131
|
+
email: str
|
|
132
|
+
age: int
|
|
133
|
+
|
|
134
|
+
class ProcessResult(BaseModel):
|
|
135
|
+
user_id: int
|
|
136
|
+
status: str
|
|
137
|
+
|
|
138
|
+
@task
|
|
139
|
+
async def process_user(data: UserData) -> ProcessResult:
|
|
140
|
+
# automatic validation of inputs and outputs!
|
|
141
|
+
user_id = 42
|
|
142
|
+
return ProcessResult(user_id=user_id, status="active")
|
|
143
|
+
|
|
144
|
+
# this will validate automatically
|
|
145
|
+
data = UserData(name="Alice", email="alice@example.com", age=30)
|
|
146
|
+
task = process_user(data)
|
|
147
|
+
|
|
148
|
+
# you can also provide dict and it will be automatically validated
|
|
149
|
+
user_data = {"name": "Bob", "email": "bob@example.com", "age": 30}
|
|
150
|
+
task = process_user(user_data)
|
|
151
|
+
|
|
152
|
+
# input is validated immediately, before the task can even be queued
|
|
153
|
+
user_data = {"invalid": "input"}
|
|
154
|
+
task = process_user(user_data) # throws a ValidationError exception!
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Easy Testing Without the Async Hassle
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
import asyncio
|
|
161
|
+
from sheppy import TestQueue, task
|
|
162
|
+
|
|
163
|
+
@task
|
|
164
|
+
async def multiply(x: int) -> int:
|
|
165
|
+
# this is an async task (but sync tasks are also supported!)
|
|
166
|
+
await asyncio.sleep(1)
|
|
167
|
+
return x * 2
|
|
168
|
+
|
|
169
|
+
def test_multiply_without_async():
|
|
170
|
+
queue = TestQueue()
|
|
171
|
+
|
|
172
|
+
# add async task to queue
|
|
173
|
+
task = multiply(5)
|
|
174
|
+
queue.add(task) # no await needed!
|
|
175
|
+
|
|
176
|
+
# process it synchronously
|
|
177
|
+
task = queue.process_next()
|
|
178
|
+
|
|
179
|
+
# check results
|
|
180
|
+
assert task.result == 10
|
|
181
|
+
assert task.completed
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
No `async def test_`, no `@pytest.mark.asyncio`, no event loop management. Just simple, synchronous tests.
|
|
185
|
+
|
|
186
|
+
## Requirements
|
|
187
|
+
|
|
188
|
+
- Python 3.10+
|
|
189
|
+
- Redis 8+
|
|
190
|
+
|
|
191
|
+
## Developing
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
git clone https://github.com/malvex/sheppy.git
|
|
195
|
+
cd sheppy
|
|
196
|
+
uv sync --group dev
|
|
197
|
+
|
|
198
|
+
pytest -v tests/ --tb=short
|
|
199
|
+
mypy src/
|
|
200
|
+
ruff check src/
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## License
|
|
204
|
+
|
|
205
|
+
This project is licensed under the terms of the MIT license.
|
sheppy-0.0.1/README.md
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# Sheppy 🐕
|
|
2
|
+
|
|
3
|
+
Sheppy is a fast, modern, and easy to use Python task queue that's simple enough to learn in minutes, yet scales to millions of tasks. It is designed to be as simple as possible, with sane defaults, while providing all essential features for working with background tasks.
|
|
4
|
+
|
|
5
|
+
## Why Sheppy?
|
|
6
|
+
|
|
7
|
+
- **Blazing Fast**: The fastest Python task queue without compromises
|
|
8
|
+
- **Async Native**: Built on asyncio from the ground up - no async backporting, just modern Python
|
|
9
|
+
- **Dead Simple**: Just `@task` decorator and `Queue` - that's it
|
|
10
|
+
- **Type Safe**: Full Pydantic integration for automatic validation and serialization
|
|
11
|
+
- **Multi-backend Support**: Native support for different backends (note: only Redis is implemented at this time)
|
|
12
|
+
- **FastAPI Compatible**: Seamless integration with FastAPI's dependency injection
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install sheppy
|
|
18
|
+
# or if you're using uv:
|
|
19
|
+
uv add sheppy
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## TL;DR Quick Start
|
|
23
|
+
|
|
24
|
+
This is all you need to know:
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
import asyncio
|
|
28
|
+
from datetime import datetime, timedelta
|
|
29
|
+
from sheppy import Queue, task, RedisBackend
|
|
30
|
+
|
|
31
|
+
queue = Queue(RedisBackend("redis://127.0.0.1:6379"))
|
|
32
|
+
|
|
33
|
+
@task
|
|
34
|
+
async def say_hello(to: str) -> str:
|
|
35
|
+
s = f"Hello, {to}!"
|
|
36
|
+
print(s)
|
|
37
|
+
return s
|
|
38
|
+
|
|
39
|
+
async def main():
|
|
40
|
+
t1 = say_hello("World")
|
|
41
|
+
await queue.add(t1)
|
|
42
|
+
await queue.add(say_hello("Moon"))
|
|
43
|
+
await queue.schedule(say_hello("Patient Person"), at=timedelta(seconds=10)) # runs in 10 seconds from now
|
|
44
|
+
await queue.schedule(say_hello("New Year"), at=datetime.fromisoformat("2026-01-01 00:00:00 +00:00"))
|
|
45
|
+
|
|
46
|
+
# await the task completion
|
|
47
|
+
updated_task = await queue.wait_for(t1)
|
|
48
|
+
|
|
49
|
+
if updated_task.error:
|
|
50
|
+
print(f"Task failed with error: {updated_task.error}")
|
|
51
|
+
elif updated_task.completed:
|
|
52
|
+
print(f"Task succeed with result: {updated_task.result}")
|
|
53
|
+
assert updated_task.result == "Hello, World!"
|
|
54
|
+
else:
|
|
55
|
+
# note: this won't happen though because wait_for doesn't return pending tasks
|
|
56
|
+
print("Task is still pending!")
|
|
57
|
+
|
|
58
|
+
if __name__ == "__main__":
|
|
59
|
+
asyncio.run(main())
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Run it:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# run the app:
|
|
66
|
+
python examples/tldr.py # nothing will happen because worker isn't running
|
|
67
|
+
|
|
68
|
+
# in another terminal, you can list queued tasks:
|
|
69
|
+
sheppy task list # (shows 2 pending and 2 scheduled tasks)
|
|
70
|
+
|
|
71
|
+
# run worker process to process the tasks
|
|
72
|
+
sheppy work # (you should see the tasks to get processed, and the app should finish!)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
And that's it!
|
|
76
|
+
|
|
77
|
+
## Tl;Dr multiple queues (including priority queues)
|
|
78
|
+
|
|
79
|
+
Task can have different priority by simply defining multiple queues with different priority.
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
backend = RedisBackend("redis://127.0.0.1:6379")
|
|
83
|
+
different_backend = RedisBackend("redis://10.10.10.10:6379")
|
|
84
|
+
|
|
85
|
+
email_queue = Queue(backend, name="email-queue")
|
|
86
|
+
high_priority_email_queue = Queue(backend, name="high-priority-email-queue")
|
|
87
|
+
data_exports_queue = Queue(different_backend, name="data-exports")
|
|
88
|
+
|
|
89
|
+
normal_email = send_email(Email(...))
|
|
90
|
+
important_email = send_email(Email(...))
|
|
91
|
+
data_export = export_data(user_id=1234)
|
|
92
|
+
|
|
93
|
+
await email_queue.add(normal_email)
|
|
94
|
+
await high_priority_email_queue.add(important_email)
|
|
95
|
+
await data_exports_queue.add(data_export)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
And then you run workers like this:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
# the first queue in the arg list will be always processed first
|
|
102
|
+
# the second queue in the arg list will be processed only if first queue is empty
|
|
103
|
+
sheppy work -q "high-priority-email-queue" -q "email-queue" --redis-url "redis://127.0.0.1:6379"
|
|
104
|
+
|
|
105
|
+
# in separate terminal, we run worker for different queue with a different backend
|
|
106
|
+
sheppy work -q "data-exports" --redis-url "redis://10.10.10.10:6379"
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Type Safety with Pydantic
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from pydantic import BaseModel
|
|
113
|
+
from sheppy import task
|
|
114
|
+
|
|
115
|
+
class UserData(BaseModel):
|
|
116
|
+
name: str
|
|
117
|
+
email: str
|
|
118
|
+
age: int
|
|
119
|
+
|
|
120
|
+
class ProcessResult(BaseModel):
|
|
121
|
+
user_id: int
|
|
122
|
+
status: str
|
|
123
|
+
|
|
124
|
+
@task
|
|
125
|
+
async def process_user(data: UserData) -> ProcessResult:
|
|
126
|
+
# automatic validation of inputs and outputs!
|
|
127
|
+
user_id = 42
|
|
128
|
+
return ProcessResult(user_id=user_id, status="active")
|
|
129
|
+
|
|
130
|
+
# this will validate automatically
|
|
131
|
+
data = UserData(name="Alice", email="alice@example.com", age=30)
|
|
132
|
+
task = process_user(data)
|
|
133
|
+
|
|
134
|
+
# you can also provide dict and it will be automatically validated
|
|
135
|
+
user_data = {"name": "Bob", "email": "bob@example.com", "age": 30}
|
|
136
|
+
task = process_user(user_data)
|
|
137
|
+
|
|
138
|
+
# input is validated immediately, before the task can even be queued
|
|
139
|
+
user_data = {"invalid": "input"}
|
|
140
|
+
task = process_user(user_data) # throws a ValidationError exception!
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Easy Testing Without the Async Hassle
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
import asyncio
|
|
147
|
+
from sheppy import TestQueue, task
|
|
148
|
+
|
|
149
|
+
@task
|
|
150
|
+
async def multiply(x: int) -> int:
|
|
151
|
+
# this is an async task (but sync tasks are also supported!)
|
|
152
|
+
await asyncio.sleep(1)
|
|
153
|
+
return x * 2
|
|
154
|
+
|
|
155
|
+
def test_multiply_without_async():
|
|
156
|
+
queue = TestQueue()
|
|
157
|
+
|
|
158
|
+
# add async task to queue
|
|
159
|
+
task = multiply(5)
|
|
160
|
+
queue.add(task) # no await needed!
|
|
161
|
+
|
|
162
|
+
# process it synchronously
|
|
163
|
+
task = queue.process_next()
|
|
164
|
+
|
|
165
|
+
# check results
|
|
166
|
+
assert task.result == 10
|
|
167
|
+
assert task.completed
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
No `async def test_`, no `@pytest.mark.asyncio`, no event loop management. Just simple, synchronous tests.
|
|
171
|
+
|
|
172
|
+
## Requirements
|
|
173
|
+
|
|
174
|
+
- Python 3.10+
|
|
175
|
+
- Redis 8+
|
|
176
|
+
|
|
177
|
+
## Developing
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
git clone https://github.com/malvex/sheppy.git
|
|
181
|
+
cd sheppy
|
|
182
|
+
uv sync --group dev
|
|
183
|
+
|
|
184
|
+
pytest -v tests/ --tb=short
|
|
185
|
+
mypy src/
|
|
186
|
+
ruff check src/
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## License
|
|
190
|
+
|
|
191
|
+
This project is licensed under the terms of the MIT license.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "sheppy"
|
|
3
|
+
dynamic = ["version"]
|
|
4
|
+
description = ""
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "Martin", email = "15851033+malvex@users.noreply.github.com" }
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
dependencies = [
|
|
12
|
+
"anyio>=3.0.1",
|
|
13
|
+
"croniter>=6.0.0",
|
|
14
|
+
"pydantic>=2.1.1",
|
|
15
|
+
"redis>=6.0.0",
|
|
16
|
+
"typer>=0.16.1",
|
|
17
|
+
"watchfiles>=1.1.0",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[dependency-groups]
|
|
21
|
+
dev = [
|
|
22
|
+
"ruff>=0.12.7",
|
|
23
|
+
"mypy>=1.17.1",
|
|
24
|
+
"pytest>=8.4.1",
|
|
25
|
+
"pytest-asyncio>=1.1.0",
|
|
26
|
+
"pytest-cov>=6.2.1",
|
|
27
|
+
"types-croniter>=6.0.0.20250809",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[build-system]
|
|
31
|
+
requires = ["hatchling"]
|
|
32
|
+
build-backend = "hatchling.build"
|
|
33
|
+
|
|
34
|
+
[tool.hatch.version]
|
|
35
|
+
path = "src/sheppy/__init__.py"
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.wheel]
|
|
38
|
+
packages = ["src/sheppy"]
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.sdist]
|
|
41
|
+
only-include = ["src/sheppy"]
|
|
42
|
+
|
|
43
|
+
[project.scripts]
|
|
44
|
+
sheppy = "sheppy.cli.cli:app"
|
|
45
|
+
|
|
46
|
+
[tool.mypy]
|
|
47
|
+
strict = true
|
|
48
|
+
|
|
49
|
+
[tool.ruff.lint]
|
|
50
|
+
select = [
|
|
51
|
+
"E", # pycodestyle errors
|
|
52
|
+
"W", # pycodestyle warnings
|
|
53
|
+
"F", # pyflakes
|
|
54
|
+
"I", # isort
|
|
55
|
+
"B", # flake8-bugbear
|
|
56
|
+
"SIM", # flake8-simplify
|
|
57
|
+
"C4", # flake8-comprehensions
|
|
58
|
+
"UP", # pyupgrade
|
|
59
|
+
"ARG001", # unused arguments in functions
|
|
60
|
+
"T201", # print statements are not allowed
|
|
61
|
+
"PLC0415" # `import` should be at the top-level of a file
|
|
62
|
+
]
|
|
63
|
+
ignore = [
|
|
64
|
+
"E501", # line too long
|
|
65
|
+
"B008", # do not perform function calls in argument defaults
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
[tool.ruff.lint.per-file-ignores]
|
|
69
|
+
"tests/contract/test_input_validation*.py" = [
|
|
70
|
+
"ARG001", # Unused function argument: `y`
|
|
71
|
+
"UP006", # Use `type` instead of `Type` for type annotation
|
|
72
|
+
"UP007", # Use `X | Y` for type annotations
|
|
73
|
+
"UP035", # `typing.Type` is deprecated, use `type` instead
|
|
74
|
+
"UP045", # Use `X | None` for type annotations
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
[tool.pytest.ini_options]
|
|
78
|
+
asyncio_mode = "auto"
|
|
79
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
80
|
+
addopts = "-rXs --strict-config --strict-markers"
|
|
81
|
+
xfail_strict = true
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from .backend import Backend, BackendError, MemoryBackend, RedisBackend
|
|
2
|
+
from .models import Task
|
|
3
|
+
from .queue import Queue
|
|
4
|
+
from .task_factory import task
|
|
5
|
+
from .testqueue import TestQueue
|
|
6
|
+
from .utils.fastapi import Depends
|
|
7
|
+
from .worker import Worker
|
|
8
|
+
|
|
9
|
+
__version__ = "0.0.1"
|
|
10
|
+
|
|
11
|
+
__all__ = [ # noqa
|
|
12
|
+
# fastapi
|
|
13
|
+
"Depends",
|
|
14
|
+
# task
|
|
15
|
+
"task", "Task",
|
|
16
|
+
# queue
|
|
17
|
+
"Queue",
|
|
18
|
+
# testqueue
|
|
19
|
+
"TestQueue",
|
|
20
|
+
# worker
|
|
21
|
+
"Worker",
|
|
22
|
+
# backend/
|
|
23
|
+
"Backend", "MemoryBackend", "RedisBackend", "BackendError",
|
|
24
|
+
]
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BackendError(Exception):
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Backend(ABC):
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
async def connect(self) -> None:
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
async def disconnect(self) -> None:
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def is_connected(self) -> bool:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
async def append(self, queue_name: str, tasks: list[dict[str, Any]], unique: bool = True) -> list[bool]:
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
async def pop(self, queue_name: str, limit: int = 1, timeout: float | None = None) -> list[dict[str, Any]]:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
async def get_tasks(self, queue_name: str, task_ids: list[str]) -> dict[str, dict[str, Any]]:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
async def get_all_tasks(self, queue_name: str) -> list[dict[str, Any]]:
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
@abstractmethod
|
|
42
|
+
async def get_pending(self, queue_name: str, count: int = 1) -> list[dict[str, Any]]:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
@abstractmethod
|
|
46
|
+
async def schedule(self, queue_name: str, task_data: dict[str, Any], at: datetime, unique: bool = True) -> bool:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
@abstractmethod
|
|
50
|
+
async def get_scheduled(self, queue_name: str) -> list[dict[str, Any]]:
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
async def pop_scheduled(self, queue_name: str, now: datetime | None = None) -> list[dict[str, Any]]:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
@abstractmethod
|
|
58
|
+
async def store_result(self, queue_name: str, task_data: dict[str, Any]) -> bool:
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
@abstractmethod
|
|
62
|
+
async def get_results(self, queue_name: str, task_ids: list[str], timeout: float | None = None) -> dict[str, dict[str, Any]]:
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
@abstractmethod
|
|
66
|
+
async def size(self, queue_name: str) -> int:
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
@abstractmethod
|
|
70
|
+
async def clear(self, queue_name: str) -> int:
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
@abstractmethod
|
|
74
|
+
async def get_stats(self, queue_name: str) -> dict[str, int]:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
@abstractmethod
|
|
78
|
+
async def list_queues(self) -> dict[str, int]:
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
@abstractmethod
|
|
82
|
+
async def add_cron(self, queue_name: str, deterministic_id: str, task_cron: dict[str, Any]) -> bool:
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
@abstractmethod
|
|
86
|
+
async def delete_cron(self, queue_name: str, deterministic_id: str) -> bool:
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
@abstractmethod
|
|
90
|
+
async def get_crons(self, queue_name: str) -> list[dict[str, Any]]:
|
|
91
|
+
pass
|