Release Notes
v0.9.0 - Management & Observability API (Phase 5/6) - 2026-09-05
Phase 5 of the roadmap to v1.0.0 adds three management features: update_task(), rich job/task queries, and stats(). See the new Observability docs page.
What's new
update_task()— changes a scheduled task in place and keeps itstask_id. You can change the name, interval, scheduling mode, args/kwargs, timeout, retry settings, jitter, and the progress callback. All parameters are keyword-only; omit the parameters you do not want to change.timeout=Nonedisables the timeout, andprogress_callback=Noneremoves the callback. When you changeinterval, quiv reschedules the next run tonow + interval. When you update arunningtask, the changes apply from its next run. The method emits the newEvent.TASK_UPDATEDwith the updatedTask. You cannot changerun_once,delay, or the handlerfunc.- Rich job/task queries —
get_all_jobs()accepts new filters:task_id, andsince/untilfor a time window onstarted_at(pass timezone-aware UTC values). It also acceptsorder_by("started_at"or"ended_at"),descending, andlimit/offsetfor pagination.get_all_tasks()acceptsstatus,limit, andoffset, and returns tasks ordered bynext_run_at. stats()— returns aQuivStatssnapshot (a frozen dataclass, exported fromquiv). It contains the active job count, the pool size and utilization, task counts by status, the earliest upcoming run, and the retained job-history count.- The FastAPI example app has three new endpoints:
GET /tasks/stats,GET /tasks/{task_id}/jobs?limit=&offset=, andPATCH /tasks/{task_id}.
Fixes
add_task()no longer requires anintervalfor a run-once task (#65). A run-once task never repeats, so quiv never reads its interval. Omitintervalwhen you passrun_once=True. An interval given withrun_once=Trueis ignored, andTask.interval_secondsreadsNone. A recurring task still requiresinterval > 0.interval=Nonenow raisesConfigurationError(#65). Earlier versions raisedTypeErrorfrom an unguarded comparison.Nonenow reaches the same error as0and-1.run_task_immediately()raisesTaskNotFoundErrorfor an unknown task id (#67). Earlier versions raisedHandlerNotRegisteredError, which names a different fault.get_task(),remove_task()andrun_task_immediately()now report the same error for the same cause. A run-once task deletes itself when it finishes, so its id stops resolving after it runs.HandlerNotRegisteredErrorkeeps its own meaning: the task exists, but no handler is registered for it.
Behavior changes
TaskNotScheduledErroris deprecated. quiv no longer raises it. It is now a subclass ofTaskNotFoundError, and the name stays exported, so imports keep working. Anexcept TaskNotScheduledErrorclause no longer catches these errors. Change it toexcept TaskNotFoundError. quiv removes the alias in 1.0.0.
Full Changelog: https://github.com/nandyalu/quiv/compare/v0.8.0...v0.9.0
v0.8.0 - Execution Features (Phase 4/6) - 2026-08-21
Phase 4 of the roadmap to v1.0.0 adds three failure-handling features: per-task timeout, retry with exponential backoff, and jitter. See the new Failure Handling docs page.
What's new
- Per-task timeout —
add_task(..., timeout=30)sets a time limit for each job. When a job runs longer thantimeoutseconds, quiv sets the job's stop event, in the same way ascancel_job(). The job then finalizes ascancelledwith a timeout error message. The timeout is cooperative. If the handler ignores its stop event, it keeps its pool thread until it returns; quiv never kills threads. The scheduler loop wakes for the nearest timeout deadline, so a timeout fires within milliseconds of that deadline. - Retry with exponential backoff —
add_task(..., max_retries=3, retry_backoff=10)runs a failed job again. A job is failed when an exception escapes the handler. The next attempt starts afterretry_backoff * 2**(failures - 1)seconds: the first retry waitsretry_backoffseconds, the second waits twice that, and so on. Cancelled jobs do not retry; this includes timeouts. A successful run resets the failure counter. When retries are exhausted, a recurring task returns to its normal schedule and a run-once task is deleted. EachJobrecords itsattemptnumber. The newEvent.JOB_RETRYINGfires afterJOB_FAILEDwhen quiv schedules a retry. - Jitter —
add_task(..., jitter=5)adds a random offset between 0 andjitterseconds to each next run of a recurring task. Use it when many tasks share the same interval boundaries and would start at the same time. quiv draws a new offset for every run. Jitter does not apply to the initialdelayor to retry backoff. Taskexposes the new fieldstimeout_seconds,max_retries,retry_backoff_seconds,retry_attempt, andjitter_seconds.Jobexposesattempt.- The four new options are keyword-only. The rest of the
add_task()signature is unchanged, including positionalargs,kwargs, andprogress_callback. Existing calls continue to work.
Fixes
- Fixed-interval scheduling could set
next_run_atto a time that is not in the future. This happened when a job finished within clock resolution of its start time, or when the elapsed time landed exactly on an interval boundary. The task then dispatched again immediately. The next run is now always the next interval boundary that is strictly in the future. - When a timed-out job's handler also raised an exception, the job's
error_messageshowed only the exception text and hid the timeout. The timeout message now comes first, and the handler's exception is appended.
Full Changelog: https://github.com/nandyalu/quiv/compare/v0.7.0...v0.8.0
v0.7.0 - Database Locking Rework (Phase 3/6) - 2026-08-09
Phase 3 of the roadmap to v1.0.0: lock-free reads under SQLite WAL.
What's new
- Lock-free reads — read queries (
get_task,get_job,get_all_tasks,get_all_jobs, due-task queries) no longer serialize behind the persistence layer's global lock; SQLite WAL mode provides the reader/writer coordination. Read-modify-write operations (task/job lifecycle transitions, pause/resume, cleanup) still serialize on a dedicated write lock, preserving the no-lost-update guarantees. - SQLite pragmas — connections now set
synchronous=NORMAL(the standard WAL pairing: fsync on checkpoint instead of per-commit; the DB is an ephemeral temp file, so durability-on-crash was never a goal) and an explicitbusy_timeout=10000matching the existing driver-level timeout.
Performance
With a writer and a heavy get_all_jobs reader running concurrently, p99 latency of a small get_task read drops from ~89 ms to ~1.1 ms (~80×) — small reads no longer queue behind large scans or writes on a global lock.
Aggregate wall time on a synthetic hammer benchmark (5,000 mixed ops, 70% reads / 30% writes, 16 threads) measured 31.0 s before vs 44.5 s after. Investigated per the phase plan's guard: not connection-pool contention (unchanged with a 32-connection pool) and not WAL checkpoint starvation (a 20 MiB WAL with serialized reads stayed fast) — it is CPython GIL convoying on CPU-bound ORM deserialization when all 16 threads busy-loop, where serialized execution is faster in aggregate. An artifact of the synthetic saturation workload, not of realistic loads, and one that disappears on free-threaded builds.
The synchronous=NORMAL pragma alone improves write throughput ~20% (31.0 s → 24.6 s with reads still serialized).
Housekeeping
- New concurrency stress-test suite (
tests/test_persistence_concurrency.py): concurrent writers with no lost updates, readers seeing consistent rows during writes, a full scheduler run atpool_size=32under constant read load, and a pause/resume race. - Docs, release notes, and CLAUDE.md updated; version bumped to
0.7.0.
Full Changelog: https://github.com/nandyalu/quiv/compare/v0.6.0...v0.7.0
v0.6.0 - Scheduler Core Efficiency (Phase 2/6) - 2026-08-05
Phase 2 of the roadmap to v1.0.0: smart sleep loop and handler signature caching.
What's new
- Sub-second intervals — the scheduler loop now sleeps until the next due task on an interruptible wait instead of polling every second.
add_task(interval=0.2)works; dispatch jitter drops from up to ~1 s to milliseconds. - Zero idle polling — an idle scheduler issues no database queries (bounded by a 60-second safety-net wake-up). Schedule changes (
add_task,run_task_immediately,resume_task,remove_task) and job completions wake the loop immediately, so deferred tasks dispatch as soon as a pool slot frees andshutdown()returns promptly instead of waiting out the current sleep. - Handler signature caching — each handler's injectable kwargs (
_job_id,_stop_event,_progress_hook) are introspected once per handler lifetime (weakly cached) instead of threeinspect.signature()calls per dispatch.
Fixes
- When the pool is saturated, the loop no longer busy-polls the database at ~100 Hz over overdue tasks it cannot dispatch anyway; it sleeps until a finishing job wakes it.
Housekeeping
- 7 new tests: timing tests for dispatch latency, sub-second runs, idle no-polling, backpressure dispatch-on-slot-free, and prompt shutdown, plus a smoke test codifying the manual 0.1 s-interval throughput check. Docs, release notes, and AI-tooling artifacts updated; version bumped to
0.6.0.
Full Changelog: https://github.com/nandyalu/quiv/compare/v0.5.0...v0.6.0
v0.5.0 - Correctness & Stability (Phase 1/6) - 2026-07-26
Phase 1 of the roadmap to v1.0.0: three concurrency bug fixes.
Behavior changes
run_task_immediately()now raisesTaskNotActiveErrorwhen the task is notactive. Previously arunningtask could be dispatched a second time concurrently (breaking the no-overlap guarantee) and apausedtask was silently un-paused. Resume paused tasks explicitly withresume_task().
What's new
shutdown(timeout=...)/stop(timeout=...)— optional bound on how long shutdown waits for the scheduler thread and in-flight jobs. Jobs that do not exit within the deadline are abandoned on their worker threads with a warning, so a hung handler can no longer block application shutdown forever. Default (timeout=None) keeps the previous wait-forever behavior.- New exception
TaskNotActiveError(exported fromquiv).
Fixes
remove_task()racing the dispatch loop could raise aKeyErrorthat stalled the scheduler for 5 seconds; dispatch now skips removed tasks gracefully, and the shared handler/callback/stop-event registries are protected by a lock.shutdown()now signals cancellation only torunningjobs instead of scanning the entire retained job history.
Housekeeping
- Roadmap phases renumbered: v0.3.x/v0.4.x were already consumed by earlier feature releases, so Phase 1 ships as
v0.5.0and later phases shift down accordingly (Phase 6 staysv1.0.0). - 7 new regression tests; docs, release notes, and AI-tooling artifacts updated; version bumped to
0.5.0. - Docs cleanup: all Markdown files unwrapped to one paragraph per continuous line (zensical can break rendering when a sentence/paragraph is split across source lines). No content changes.
v0.4.1 - AI Tooling & Async teardown fix - 2026-07-20
What's new
-
AI tooling — quiv now teaches AI coding assistants (Claude Code, Cursor, Copilot, …) how to use it correctly, in #51:
- Packaged agent guide — every install ships a condensed agent-facing reference at
quiv/AGENTS.mdinside the package (lands insite-packagesnext to the code): API surface, handler injection rules, cancellation semantics, FastAPI wiring, and common pitfalls. AI tools exploring installed dependencies pick it up with zero setup. - llms.txt — the docs site publishes llms.txt (index linking every docs page as raw Markdown) and llms-full.txt (the full docs in one file) following the llms.txt convention.
- Claude Code plugin — the quiv repository doubles as a plugin marketplace shipping a
quivskill that Claude loads automatically whenever a conversation touches quiv. Install once with/plugin marketplace add nandyalu/quivthen/plugin install quiv@quiv;/plugin update quivpicks up the latest guidance.
See the new AI Tools docs page for all three entry points.
- Packaged agent guide — every install ships a condensed agent-facing reference at
Fixes
- Per-invocation async event loops now finalize outstanding async generators and shut down the loop's default executor before closing. Previously, if a handler raised while an async generator was still active, Python could emit
Task was destroyed but it is pendingandasync_generator_athrow was never awaitedwarnings during teardown. The original handler exception remains visible. by @d4rk22 in #50
Other changes
- ci: skip coverage PR comment for fork PRs
- upd: roadmap for v1.0.0
- build(deps-dev): bump zensical from 0.0.43 to 0.0.45 by @dependabot[bot] in #42
- build(deps): bump actions/checkout from 6 to 7 by @dependabot[bot] in #44
- build(deps-dev): bump pytest from 9.0.3 to 9.1.1 by @dependabot[bot] in #45
- build(deps): bump sqlmodel from 0.0.38 to 0.0.39 by @dependabot[bot] in #46
- build(deps-dev): bump zensical from 0.0.45 to 0.0.50 by @dependabot[bot] in #47
- build(deps): bump tzdata from 2026.2 to 2026.3 by @dependabot[bot] in #48
- build(deps-dev): bump mypy from 2.1.0 to 2.2.0 by @dependabot[bot] in #49
- fix: finalize async resources before closing job loops by @d4rk22 in #50
New Contributors
Full Changelog: https://github.com/nandyalu/quiv/compare/v0.4.0...v0.4.1
v0.4.0 - run_on_main helper method - 2026-05-31
What's new
-
quiv.run_on_main(func, *args, **kwargs)— fire-and-forget helper that dispatches a callable onto the active Quiv instance's main event loop. Importable at module level (from quiv import run_on_main) and callable from anywhere in a task handler's call stack - no_progress_hookparameter to thread through intermediate functions.- Auto-detects whether the caller is already on the main loop's thread: sync targets run inline on-loop and async targets are scheduled via
main_loop.create_task. From a worker thread, sync targets dispatch viacall_soon_threadsafeand async targets viarun_coroutine_threadsafe. - The same helper works from a FastAPI route handler on the main loop and from inside a Quiv task — one utility shared between request and task code (e.g., a WebSocket broadcast whose connected-clients state lives on uvicorn's loop).
- Exceptions raised by the target are logged on the active Quiv's logger and swallowed, mirroring
_progress_hookand event-listener semantics.
See Running on the main event loop for the full walkthrough.
- Auto-detects whether the caller is already on the main loop's thread: sync targets run inline on-loop and async targets are scheduled via
Other changes
Quiv.start()now registers the instance as the process-level "active" Quiv (cleared byshutdown()) sorun_on_maincan resolve a target loop when called outside a task context. Multiple concurrent instances log a warning naming the most recently started instance as the winner for out-of-task callers.- Inside
_run_job, aContextVaris set to the runningQuivinstance for the duration of each handler invocation; this propagates into nested sync calls, the per-job async event loop, andasyncio.create_taskspawned inside an async handler.
Full Changelog: https://github.com/nandyalu/quiv/compare/v0.3.5...v0.4.0
v0.3.5 - Cleanup noise & update packages - 2026-05-27
What's new
- Removed a debug logging statement from the
set_timezone_to_utcmethod to reduce log verbosity by @nandyalu in #38
Other changes
- Updated various python libraries to latest versions.
- build(deps): bump actions/upload-pages-artifact from 4 to 5 by @dependabot[bot] in #24
- build(deps-dev): bump zensical from 0.0.32 to 0.0.33 by @dependabot[bot] in #25
- build(deps-dev): bump mypy from 1.20.0 to 1.20.1 by @dependabot[bot] in #26
- build(deps-dev): bump zensical from 0.0.33 to 0.0.36 by @dependabot[bot] in #29
- build(deps): bump tzdata from 2026.1 to 2026.2 by @dependabot[bot] in #28
- build(deps-dev): bump mypy from 1.20.1 to 1.20.2 by @dependabot[bot] in #27
- build(deps): bump orgoro/coverage from 3.2 to 3.3 by @dependabot[bot] in #30
- build(deps-dev): bump zensical from 0.0.36 to 0.0.39 by @dependabot[bot] in #31
- build(deps-dev): bump zensical from 0.0.39 to 0.0.41 by @dependabot[bot] in #32
- build(deps-dev): bump mypy from 1.20.2 to 2.0.0 by @dependabot[bot] in #33
- build(deps-dev): bump zensical from 0.0.41 to 0.0.42 by @dependabot[bot] in #34
- build(deps-dev): bump mypy from 2.0.0 to 2.1.0 by @dependabot[bot] in #35
- build(deps): bump pymdown-extensions from 10.21.2 to 10.21.3 in the uv group across 1 directory by @dependabot[bot] in #36
- build(deps-dev): bump zensical from 0.0.42 to 0.0.43 by @dependabot[bot] in #37
Full Changelog: https://github.com/nandyalu/quiv/compare/v0.3.4...v0.3.5
v0.3.4 - Job and Task attrs datetime aware - 2026-04-10
Bug fixes
- Job datetime normalization:
Jobdatetimes (started_at,ended_at) loaded from SQLite are now correctly normalized to UTC-aware. Previously, the@model_validatoronJobwas dead code because SQLAlchemy bypasses Pydantic validators when hydrating table classes.
Improvements
- Generic datetime normalization via
@reconstructor: Added a@reconstructormethod onQuivModelBasethat automatically normalizes alldatetimefields to UTC-aware on every DB load. This applies to bothTaskDBandJobmodels (and any future models), replacing the per-model dead-code validators. - Removed dead
@model_validatorfromTaskDBandJobmodels. delete_task()now has test coverage forTaskNotFoundErroron missing task.- updated python packages to latest versions.
Documentation
- New Testing page documenting all 108 tests with a breakdown of edge cases covered across 18 categories.
CI
docs-release.ymlnow waits for the release to be visible in the GitHub API before generating the changelog, fixing a race condition where the current release was missing from the generated docs.
v0.3.3 - fixed inteval option for tasks - 2026-04-09
Breaking changes
- Default interval scheduling changed to fixed intervals:
fixed_intervaldefaults toTrue, meaning next run is now scheduled from the job start time rather than completion time. Setfixed_interval=Falseto restore the previous wait-between-runs behavior.
What's new
-
fixed_intervalper-task scheduling mode:add_task()accepts a newfixed_intervalparameter:True(default) — next run at fixed intervals from job start time. If a run exceeds the interval, missed intervals are skipped.False— next runintervalseconds after job completion (old behavior).
Other changes
finalize_task_after_job()acceptsjob_started_atfor fixed-interval scheduling.
Full Changelog: https://github.com/nandyalu/quiv/compare/v0.3.2...v0.3.3
v0.3.2 - Removed unique task name constraint - 2026-04-09
Breaking changes
-
Task operations now use
task_idinstead oftask_name: All public methods that previously accepted atask_namestring now accept thetask_id(UUID string) returned byadd_task(). This removes the uniqueness constraint on task names — multiple tasks can now share the sametask_name.Affected methods: -
remove_task(task_id)— previouslyremove_task(task_name)-pause_task(task_id)— previouslypause_task(task_name)-resume_task(task_id)— previouslyresume_task(task_name)-run_task_immediately(task_id)— previouslyrun_task_immediately(task_name)-get_task(task_id)— previouslyget_task(task_name)(by-name lookup)Removed methods: -
get_task_by_id()— merged intoget_task(task_id)Migration: Store the return value of
add_task()and pass it to all task operations:# Before scheduler.add_task("my-task", handler, interval=60) scheduler.pause_task("my-task") # After task_id = scheduler.add_task("my-task", handler, interval=60) scheduler.pause_task(task_id) -
Event listeners receive typed model objects instead of dicts
Event listener callbacks now receive
TaskandJobmodel objects directly instead of untypeddict[str, Any].TASK_*events:callback(event: Event, task: Task)JOB_*events:callback(event: Event, task: Task, job: Job)
Migration:
# Before def on_completed(event, data): print(data["task_name"], data["duration"]) # After from quiv.models import Task, Job def on_completed(event: Event, task: Task, job: Job): print(task.task_name, job.duration_seconds)
What's new
-
Duplicate task names allowed
add_task()no longer raisesConfigurationErroron duplicatetask_name. Each call returns a uniquetask_id(UUID), so multiple tasks can share a display name. This is especially useful for one-shot tasks that may be scheduled repeatedly with the same name. -
duration_secondsanderror_messageon the Job modelThe
Jobmodel now includes two new fields:duration_seconds: float | None— job runtime in seconds, set when the job finisheserror_message: str | None— error description, set when a job fails
Both fields are persisted in the database and available via
get_job()andget_all_jobs(), making it easy to inspect job history without parsing logs. -
Typed event listener callbacks
Event listeners now receive real
TaskandJobmodel objects with full IDE autocomplete and type checking.JOB_*events include the parentTaskalongside theJob, so listeners have full context without extra lookups.
Documentation
- Updated all docs to reflect
task_id-based API across getting-started, API reference, architecture, event listeners, bigger applications, progress callbacks, and exceptions pages. - Updated all code examples to use
task_idfor runtime operations. - Added admonitions and footnotes throughout docs for better readability.
- Added
_job_idtracing section to the "Why quiv?" page, describing how Trailarr uses injected job IDs as trace context for log correlation. - Rewrote event listeners documentation with typed callback signatures, updated examples, and new FastAPI WebSocket example using model objects.
Other changes
- Internal handler and progress callback registries are now keyed by
task_idinstead oftask_name. prepare_invocation()in the execution layer usestask_idfor progress callback dispatch.- Removed
get_task_by_name(),get_task_id_by_name()from the persistence layer. - Renamed
get_task_by_id()toget_task()in the persistence layer. delete_task()andqueue_task_for_immediate_run()in the persistence layer now accepttask_idinstead oftask_name.finalize_job()now accepts optionalduration_secondsanderror_messageparameters.- Removed unused
timezoneimport from persistence module.
Full Changelog: https://github.com/nandyalu/quiv/compare/v0.3.1...v0.3.2
v0.3.1 - Better task pickling - 2026-04-07
Breaking Changes
- The public methods
get_task(),get_task_by_id(), andget_all_tasks()returnTaskobjects with unpickledargs(tuple) andkwargs(dict) — ready for JSON serialization in FastAPI endpoints. - Internal model renamed: The SQLModel database model is now
TaskDB(internal only, not exported). External modelTaskis still the same - but is now only used for Public API responses and has correct types.
What's New
- Async callbacks without event loop: Async progress callbacks and event listeners now run in a temporary event loop when no main loop is available, instead of being skipped with a warning.
- Eager main loop resolution: The main event loop is now resolved at
start()time (in addition to lazy resolution on first callback), improving reliability in FastAPI apps.
Documentation
- Added return types to all method signatures in API docs (e.g.,
get_task(task_name: str) -> Task. - Updated architecture, progress-callbacks, and event-listeners docs to reflect new async callback behavior
Full Changelog: https://github.com/nandyalu/quiv/compare/v0.3.0...v0.3.1
v0.3.0 - Better pickling and Event listeners - 2026-04-07
What's Changed
- Changed argument serialization for task persistence from JSON to pickle, allowing most Python objects (except lambdas and inner functions) to be scheduled as arguments by @nandyalu in #19
- Added support for global event listeners via
add_listener(event, callback)andremove_listener(event, callback), including both sync and async callbacks, with robust dispatch and error handling. Events include all major task and job lifecycle transitions. by @nandyalu in #19 - Introduced
startup()as an alias forstart(), andstop()as an alias forshutdown(), makingstart/stoppairs more natural in user code by @nandyalu in #19 Job.idnow usesUUIDand can be injected into task function (as_job_id) if function accepts it - can be used for task tracing / logging by @nandyalu in #19- Updated all relevant documentation by @nandyalu in #19
Full Changelog: https://github.com/nandyalu/quiv/compare/v0.2.4...v0.3.0
v0.2.4 - Preserve task args order - 2026-04-07
What's Changed
add_taskargs as tuple to preserve order by @nandyalu in #18loggeracceptslogging.Loggeras well aslogging.LoggerAdapterby @nandyalu in #18
Full Changelog: https://github.com/nandyalu/quiv/compare/v0.2.3...v0.2.4
v0.2.3 - Exception logging improvements - 2026-04-06
What's Changed
- Bump zensical from 0.0.24 to 0.0.27 by @dependabot[bot] in #8
- Bump zensical from 0.0.27 to 0.0.28 by @dependabot[bot] in #9
- Bump pytest-cov from 7.0.0 to 7.1.0 by @dependabot[bot] in #10
- Bump actions/configure-pages from 5 to 6 by @dependabot[bot] in #11
- Bump actions/deploy-pages from 4 to 5 by @dependabot[bot] in #12
- Bump zensical from 0.0.28 to 0.0.30 by @dependabot[bot] in #13
- Bump sqlmodel from 0.0.37 to 0.0.38 by @dependabot[bot] in #14
- Bump tzdata from 2025.3 to 2026.1 by @dependabot[bot] in #15
- Bump mypy from 1.19.1 to 1.20.0 by @dependabot[bot] in #16
- Improve job logging with task names and error details by @nandyalu in #17
Full Changelog: https://github.com/nandyalu/quiv/compare/v0.2.2...v0.2.3
v0.2.2 - SQLModel Registry Fix - 2026-03-13
What's Changed
-
fix: quiv registry to not include user models. Updated the private
registryusage forSQLModelmodels ofQuivto the method from https://github.com/fastapi/sqlmodel/discussions/1539#discussioncomment-14229572 by @nandyalu in #7 -
Added a test to ensure user's
SQLModelwithtable=Truedoes not get created in Quiv database by @nandyalu in #7
Full Changelog: https://github.com/nandyalu/quiv/compare/v0.2.0...v0.2.2
v0.2.1 - Fix Release Build - 2026-03-11
What's Changed
Full Changelog: https://github.com/nandyalu/quiv/compare/v0.2.0...v0.2.1
v0.2.0-Bug Fixes and minor updates - 2026-03-09
Breaking Changes
timezone_namerenamed totimezone— BothQuiv()andQuivConfig()now usetimezonefor the display timezone parameter. Update anytimezone_name=keyword arguments.register_handler/register_progress_callbackare now private — Renamed to_register_handler/_register_progress_callback. Useadd_task()instead, which handles registration internally.add_task()raises on duplicate task names — Callremove_task()first if you need to replace a task.
New Features
remove_task(task_name)— Remove a task and its handler/callback registrations.- Cooperative cancellation —
_stop_eventinjection with per-jobthreading.Event. See Cancellation docs. - Progress callbacks with four dispatch paths — Async/sync callbacks work with or without an event loop. See Progress Callbacks docs.
- Lazy event loop resolution —
Quiv()can be instantiated at module level before any asyncio loop exists. The event loop is resolved on first progress callback dispatch. - Backpressure — Scheduler defers dispatch when the thread pool is full. Late-starting jobs log a warning suggesting to increase
pool_size. TaskStatus.RUNNING— Tasks are markedRUNNINGduring execution, preventing concurrent runs of the same task.
Bug Fixes
- Removed
logger.setLevel(logging.DEBUG)— the library no longer forces a log level - Removed
asyncio.get_event_loop()at init — fixes deprecation warnings and module-level instantiation shutdown()now cleans up SQLite WAL (-wal,-shm) sidecar files- Cancellation detection no longer depends on handler accepting
_stop_event
Improvements
TaskStatus/JobStatusare now proper(str, Enum)classes- History cleanup uses SQL-level filtering with
col()wrapper (runs every 60s, not every tick) - Next run scheduled from job completion time, not dispatch time
- All log timestamps use the configured display timezone consistently
Documentation
- New pages: Bigger Applications, Progress Callbacks, Cancellation (with mermaid diagrams)
- Architecture page now has a sequence diagram
- API page: added pool size guidance, logger/timezone note blocks
- Tabbed uv/pip install commands across all pages
- GitHub repo link in docs header with edit/view source buttons
Tests
- 8 new tests covering backpressure, late start warnings, concurrent run prevention, remove_task, progress callbacks without event loop, and task lifecycle
- All existing tests updated for API changes
Repository
- Added CODEOWNERS, issue templates, CONTRIBUTING.md, CODE_OF_CONDUCT.md
- CI workflows now include
pyproject.tomlin path filters - Removed
masterbranch reference from docs deploy workflow
Initial Release (v0.1.0) - 2026-03-08
Initial Release