mirror of
https://github.com/open-webui/oikb.git
synced 2026-07-09 16:00:53 +00:00
0.2.7: cron expression support for daemon scheduling
interval key now accepts both simple intervals (30m, 1h) and standard cron expressions (0 6 * * 1-5). Auto-detected based on field count — no config flag needed. Adds croniter>=2.0 dep.
This commit is contained in:
parent
6efcee9af8
commit
2aa721147b
5 changed files with 55 additions and 15 deletions
|
|
@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
|
||||
## [0.2.7] - 2025-05-21
|
||||
|
||||
### Added
|
||||
|
||||
- Cron expression support for daemon scheduling — use `interval: "0 6 * * 1-5"` alongside simple intervals (`30m`, `1h`). Auto-detected, no config flag needed.
|
||||
- `croniter>=2.0` added as a core dependency.
|
||||
|
||||
## [0.2.6] - 2025-05-21
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ oikb daemon --port 8080
|
|||
```
|
||||
|
||||
Features:
|
||||
- **Scheduled sync** — configurable per-source intervals (`30m`, `1h`, `6h`)
|
||||
- **Scheduled sync** — simple intervals (`30m`, `1h`) or cron expressions (`0 6 * * 1-5`)
|
||||
- **Webhooks** — instant sync on push via `/webhooks/github`, `/webhooks/gitlab`, `/webhooks/slack`, `/webhooks/confluence`
|
||||
- **Health checks** — `GET /health` for Docker/K8s readiness probes
|
||||
- **Prometheus metrics** — `GET /metrics` exports sync counters, duration histograms, and error rates
|
||||
|
|
@ -59,13 +59,13 @@ sources:
|
|||
- name: wiki
|
||||
source: github:owner/repo
|
||||
kb-id: 8f3a2b1c-...
|
||||
interval: 1h
|
||||
interval: 1h # simple interval
|
||||
webhook: true
|
||||
|
||||
- name: handbook
|
||||
source: confluence:ENG
|
||||
kb-id: 4e7d9a0f-...
|
||||
interval: 6h
|
||||
interval: "0 6 * * 1-5" # cron: weekdays at 6am
|
||||
```
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "oikb"
|
||||
version = "0.2.6"
|
||||
version = "0.2.7"
|
||||
description = "Sync anything to Open WebUI Knowledge Bases"
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
|
|
@ -10,6 +10,7 @@ requires-python = ">=3.11"
|
|||
license = {file = "LICENSE"}
|
||||
dependencies = [
|
||||
"click>=8.1",
|
||||
"croniter>=2.0",
|
||||
"httpx>=0.27",
|
||||
"prometheus-client>=0.20",
|
||||
"pyyaml>=6.0",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
"""oikb — Open WebUI Knowledge Base CLI."""
|
||||
|
||||
__version__ = "0.2.6"
|
||||
__version__ = "0.2.7"
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ async def verify_api_key(
|
|||
app = FastAPI(
|
||||
title="oikb",
|
||||
description="Sync engine for Open WebUI Knowledge Bases. Trigger syncs, check status, and query history.",
|
||||
version="0.2.6",
|
||||
version="0.2.7",
|
||||
)
|
||||
|
||||
# Runtime state populated by start_daemon().
|
||||
|
|
@ -50,7 +50,13 @@ _entries: list[dict] = []
|
|||
_shutdown_event: asyncio.Event | None = None
|
||||
|
||||
|
||||
# ── Interval parser ──────────────────────────────────────────────
|
||||
# ── Interval / cron parser ───────────────────────────────────────
|
||||
|
||||
|
||||
def _is_cron(value: str) -> bool:
|
||||
"""Check if a string looks like a cron expression (5-6 space-separated fields)."""
|
||||
return isinstance(value, str) and len(value.strip().split()) >= 5
|
||||
|
||||
|
||||
def parse_interval(value: str | int) -> int:
|
||||
"""Parse an interval string like '5m', '1h', '30s' to seconds."""
|
||||
|
|
@ -63,6 +69,18 @@ def parse_interval(value: str | int) -> int:
|
|||
return int(value)
|
||||
|
||||
|
||||
def _next_cron_delay(cron_expr: str) -> float:
|
||||
"""Compute seconds until the next cron fire time."""
|
||||
from datetime import datetime
|
||||
|
||||
from croniter import croniter
|
||||
|
||||
now = datetime.now()
|
||||
cron = croniter(cron_expr, now)
|
||||
next_time = cron.get_next(datetime)
|
||||
return max((next_time - now).total_seconds(), 1.0)
|
||||
|
||||
|
||||
# ── FastAPI routes ───────────────────────────────────────────────
|
||||
|
||||
@app.get(
|
||||
|
|
@ -231,23 +249,37 @@ async def _run_entry(entry: dict) -> None:
|
|||
|
||||
|
||||
async def _schedule_entry(entry: dict) -> None:
|
||||
"""Run sync for one .oikb.yaml entry on a loop."""
|
||||
interval = parse_interval(entry.get("interval", "30m"))
|
||||
source = entry.get("source", "unknown")
|
||||
"""Run sync for one .oikb.yaml entry on a loop.
|
||||
|
||||
log.info(f"Scheduling {source} every {interval}s")
|
||||
Supports both simple intervals ('30m', '1h') and cron expressions
|
||||
('0 */6 * * *', '0 6 * * 1-5').
|
||||
"""
|
||||
raw_interval = entry.get("interval", "30m")
|
||||
source = entry.get("source", "unknown")
|
||||
use_cron = _is_cron(str(raw_interval))
|
||||
|
||||
if use_cron:
|
||||
log.info(f"Scheduling {source} with cron: {raw_interval}")
|
||||
else:
|
||||
interval = parse_interval(raw_interval)
|
||||
log.info(f"Scheduling {source} every {interval}s")
|
||||
|
||||
while not _shutdown_event.is_set():
|
||||
await _run_entry(entry)
|
||||
|
||||
# Calculate next sync time.
|
||||
_scheduler_state.setdefault(source, {})["next_sync_in"] = f"{interval}s"
|
||||
# Compute wait until next run.
|
||||
if use_cron:
|
||||
delay = _next_cron_delay(str(raw_interval))
|
||||
_scheduler_state.setdefault(source, {})["next_sync_in"] = f"{int(delay)}s"
|
||||
else:
|
||||
delay = float(interval)
|
||||
_scheduler_state.setdefault(source, {})["next_sync_in"] = f"{interval}s"
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(_shutdown_event.wait(), timeout=interval)
|
||||
await asyncio.wait_for(_shutdown_event.wait(), timeout=delay)
|
||||
break # Shutdown requested.
|
||||
except asyncio.TimeoutError:
|
||||
pass # Interval elapsed, run again.
|
||||
pass # Time elapsed, run again.
|
||||
|
||||
|
||||
async def _run_scheduler(entries: list[dict]) -> None:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue