Skip to content

Commit c57e6f6

Browse files
committed
fastapi always installed
1 parent 0fa96c7 commit c57e6f6

36 files changed

+2034
-1047
lines changed

.dev/lint

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@ black fluid tests ${BLACK_ARG}
1414
echo "run ruff"
1515
ruff check fluid tests ${RUFF_ARG}
1616
echo "run mypy"
17-
mypy fluid tests
17+
mypy fluid/scheduler

.vscode/launch.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,19 @@
1818
"RedirectOutput"
1919
]
2020
},
21+
{
22+
"name": "API Serve",
23+
"type": "python",
24+
"request": "launch",
25+
"program": "${workspaceFolder}/examples/main.py",
26+
"cwd": "${workspaceFolder}",
27+
"justMyCode": false,
28+
"args": [
29+
"ls"
30+
],
31+
"debugOptions": [
32+
"RedirectOutput"
33+
]
34+
},
2135
]
2236
}

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
BSD 3-Clause License
22

3-
Copyright (c) 2023, Quantmind
3+
Copyright (c) 2024, Quantmind
44
All rights reserved.
55

66
Redistribution and use in source and binary forms, with or without

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,4 @@ outdated: ## Show outdated packages
4848

4949
.PHONY: example
5050
example: ## run task scheduler example
51-
@poetry run python -m examples.all_features
51+
@poetry run python -m examples.main

examples/all_features.py

Lines changed: 0 additions & 17 deletions
This file was deleted.

examples/main.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from examples import tasks
2+
from fluid.scheduler import TaskScheduler
3+
from fluid.utils import log
4+
5+
task_manager = TaskScheduler()
6+
task_manager.register_from_module(tasks)
7+
task_manager_cli = task_manager.cli()
8+
9+
10+
if __name__ == "__main__":
11+
log.config()
12+
task_manager_cli()

examples/tasks.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import asyncio
22
import os
3+
import time
34
from datetime import timedelta
45
from typing import cast
56

67
from fluid.scheduler import TaskRun, every, task
8+
from fluid.scheduler.broker import RedisBroker
79

810

911
@task
@@ -29,9 +31,10 @@ async def disabled(context: TaskRun) -> float:
2931
return sleep
3032

3133

32-
@task(cpu_bound=True)
33-
async def cpu_bound(context: TaskRun) -> int:
34-
await asyncio.sleep(0.1)
35-
redis = context.task_manager.broker.redis_cli
36-
await redis.setex(context.run_id, os.getpid(), 10)
37-
return 0
34+
@task(cpu_bound=True, schedule=every(timedelta(seconds=5)))
35+
async def cpu_bound(context: TaskRun) -> None:
36+
"""A CPU bound task running on subprocess"""
37+
time.sleep(1)
38+
broker = cast(RedisBroker, context.task_manager.broker)
39+
redis = broker.redis_cli
40+
await redis.setex(context.id, os.getpid(), 10)

fluid/scheduler/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
from .broker import Broker, QueuedTask
1+
from .broker import Broker
22
from .consumer import TaskConsumer, TaskManager
33
from .crontab import Scheduler, crontab
44
from .every import every
5-
from .models import Task, TaskInfo, TaskPriority, TaskRun, TaskState, task
5+
from .models import Task, TaskInfo, TaskPriority, TaskRun, TaskState, task, QueuedTask
66
from .scheduler import TaskScheduler
77

88
__all__ = [

fluid/scheduler/broker.py

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@
99
from yarl import URL
1010

1111
from fluid import settings
12-
from fluid.utils.redis import Redis, FluidRedis
12+
from redis.asyncio import Redis
13+
from fluid.utils.redis import FluidRedis
1314
import json
1415
from .errors import UnknownTaskError
1516

16-
from .models import QueuedTask, Task, TaskInfo, TaskPriority, TaskRun
17+
from .models import Task, TaskInfo, TaskPriority, TaskRun, TaskInfoUpdate
1718

1819
if TYPE_CHECKING: # pragma: no cover
1920
from .consumer import TaskManager
@@ -42,9 +43,7 @@ def task_queue_names(self) -> tuple[str, ...]:
4243
"""Names of the task queues"""
4344

4445
@abstractmethod
45-
async def queue_task(
46-
self, task_manager: TaskManager, queued_task: QueuedTask
47-
) -> TaskRun:
46+
async def queue_task(self, task_run: TaskRun) -> None:
4847
"""Queue a task"""
4948

5049
@abstractmethod
@@ -129,7 +128,7 @@ def redis(self) -> FluidRedis:
129128
return FluidRedis.create(str(self.url.with_query({})), name=self.name)
130129

131130
@property
132-
def redis_cli(self) -> Redis:
131+
def redis_cli(self) -> Redis[bytes]:
133132
return self.redis.redis_cli
134133

135134
@property
@@ -170,9 +169,10 @@ async def get_tasks_info(self, *task_names: str) -> list[TaskInfo]:
170169

171170
async def update_task(self, task: Task, params: dict[str, Any]) -> TaskInfo:
172171
pipe = self.redis_cli.pipeline()
172+
info = json.loads(TaskInfoUpdate(**params).model_dump_json())
173173
pipe.hset(
174174
self.task_hash_name(task.name),
175-
mapping={name: json.dumps(value) for name, value in params.items()},
175+
mapping={name: json.dumps(value) for name, value in info.items()},
176176
)
177177
pipe.hgetall(self.task_hash_name(task.name))
178178
_, info = await pipe.execute()
@@ -193,8 +193,8 @@ async def close(self) -> None:
193193

194194
async def get_task_run(self, task_manager: TaskManager) -> TaskRun | None:
195195
if self.task_queue_names:
196-
if redis_data := await self.redis_cli.brpop( # type: ignore [misc]
197-
self.task_queue_names, # type: ignore [arg-type]
196+
if redis_data := await self.redis_cli.brpop(
197+
self.task_queue_names,
198198
timeout=1,
199199
):
200200
data = json.loads(redis_data[1])
@@ -205,31 +205,18 @@ async def get_task_run(self, task_manager: TaskManager) -> TaskRun | None:
205205
return TaskRun(**data)
206206
return None
207207

208-
async def queue_task(
209-
self, task_manager: TaskManager, queued_task: QueuedTask
210-
) -> TaskRun:
211-
task_run = self.create_task_run(task_manager, queued_task)
212-
await self.redis_cli.lpush( # type: ignore [misc]
208+
async def queue_task(self, task_run: TaskRun) -> None:
209+
await self.redis_cli.lpush(
213210
self.task_queue_name(task_run.priority),
214211
task_run.model_dump_json(),
215212
)
216-
return task_run
217213

218214
def lock(self, name: str, timeout: float | None = None) -> Lock:
219215
return self.redis_cli.lock(name, timeout=timeout)
220216

221217
def _decode_task(self, task: Task, data: dict[bytes, Any]) -> TaskInfo:
222218
info = {name.decode(): json.loads(value) for name, value in data.items()}
223-
return TaskInfo(
224-
name=task.name,
225-
description=task.description,
226-
schedule=str(task.schedule) if task.schedule else None,
227-
priority=task.priority,
228-
enabled=info.get("enabled", True),
229-
last_run_duration=info.get("last_run_duration"),
230-
last_run_end=info.get("last_run_end"),
231-
last_run_state=info.get("last_run_state"),
232-
)
219+
return task.info(**info)
233220

234221

235222
Broker.register_broker("redis", RedisBroker)

fluid/scheduler/cli.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING, Any
4+
5+
import click
6+
from rich.console import Console
7+
from rich.table import Table
8+
9+
from fluid.tools_fastapi import serve_app
10+
from .endpoints import setup_fastapi
11+
12+
if TYPE_CHECKING:
13+
from .consumer import TaskManager
14+
from .models import TaskRun
15+
16+
17+
DEFAULT_COMMANDS: list[click.Command] = []
18+
19+
20+
class TaskManagerCLI(click.Group):
21+
def __init__(self, task_manager: TaskManager, **kwargs: Any):
22+
kwargs.setdefault("commands", DEFAULT_COMMANDS)
23+
super().__init__(**kwargs)
24+
self.task_manager = task_manager
25+
26+
27+
def ctx_task_manager(ctx: click.Context) -> TaskManager:
28+
return ctx.parent.command.task_manager # type: ignore
29+
30+
31+
@click.command()
32+
@click.pass_context
33+
def ls(ctx: click.Context) -> None:
34+
"""list all tasks"""
35+
task_manager = ctx_task_manager(ctx)
36+
table = Table(title="Tasks")
37+
table.add_column("Name", style="cyan", no_wrap=True)
38+
table.add_column("Schedule", style="magenta")
39+
table.add_column("CPU bound", style="magenta")
40+
table.add_column("Description", style="green")
41+
for name in sorted(task_manager.registry):
42+
task = task_manager.registry[name]
43+
table.add_row(
44+
name,
45+
str(task.schedule),
46+
"yes" if task.cpu_bound else "no",
47+
task.description,
48+
)
49+
console = Console()
50+
console.print(table)
51+
52+
53+
@click.command()
54+
@click.pass_context
55+
@click.argument("task")
56+
@click.option(
57+
"--dry-run",
58+
is_flag=True,
59+
help="dry run (if the tasks supports it)",
60+
default=False,
61+
)
62+
def execute(ctx: click.Context, task: str, dry_run: bool) -> None:
63+
"""execute a task"""
64+
task_manager = ctx_task_manager(ctx)
65+
run = task_manager.execute_sync(task, dry_run=dry_run)
66+
console = Console()
67+
console.print(task_run_table(run))
68+
69+
70+
@click.command("serve", short_help="Start app server.")
71+
@click.option(
72+
"--host",
73+
"-h",
74+
default="0.0.0.0",
75+
help="The interface to bind to",
76+
show_default=True,
77+
)
78+
@click.option(
79+
"--port",
80+
"-p",
81+
default=8080,
82+
help="The port to bind to",
83+
show_default=True,
84+
)
85+
@click.option(
86+
"--reload",
87+
is_flag=True,
88+
default=False,
89+
help="Enable auto-reload",
90+
show_default=True,
91+
)
92+
@click.pass_context
93+
def serve(ctx: click.Context, host: str, port: int, reload: bool) -> None:
94+
"""Run the service."""
95+
task_manager = ctx_task_manager(ctx)
96+
app = setup_fastapi(task_manager)
97+
serve_app(app, host, port, reload)
98+
99+
100+
DEFAULT_COMMANDS = (ls, execute, serve)
101+
102+
103+
def task_run_table(task_run: TaskRun) -> Table:
104+
table = Table(title="Task Run", show_header=False)
105+
color = "red" if task_run.state.is_failure else "green"
106+
table.add_column("Name", style="cyan")
107+
table.add_column("Description", style=color)
108+
table.add_row("name", task_run.task.name)
109+
table.add_row("description", task_run.task.description)
110+
table.add_row("run_id", task_run.id)
111+
table.add_row("state", task_run.state)
112+
if task_run.start:
113+
table.add_row("started", task_run.start.isoformat())
114+
if task_run.end:
115+
table.add_row("completed", task_run.end.isoformat())
116+
table.add_row("duration ms", str(task_run.duration_ms))
117+
return table

0 commit comments

Comments
 (0)