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.
@@ -0,0 +1,16 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ # Lock files
13
+ *.lock
14
+
15
+ .vscode
16
+ .coverage
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,10 @@
1
+ from .base import Backend, BackendError
2
+ from .memory import MemoryBackend
3
+ from .redis import RedisBackend
4
+
5
+ __all__ = [
6
+ "Backend",
7
+ "BackendError",
8
+ "MemoryBackend",
9
+ "RedisBackend",
10
+ ]
@@ -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