refresh-engine 0.1.0__tar.gz → 1.0.0__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.
- refresh_engine-1.0.0/PKG-INFO +207 -0
- refresh_engine-1.0.0/README.md +174 -0
- refresh_engine-1.0.0/pyproject.toml +201 -0
- refresh_engine-1.0.0/pyproject.toml.orig +167 -0
- {refresh_engine-0.1.0 → refresh_engine-1.0.0}/src/refresh_engine/__init__.py +150 -169
- refresh_engine-1.0.0/src/refresh_engine/_internal/__init__.py +5 -0
- refresh_engine-1.0.0/src/refresh_engine/_internal/concurrency.py +58 -0
- refresh_engine-1.0.0/src/refresh_engine/_internal/context.py +334 -0
- refresh_engine-1.0.0/src/refresh_engine/_internal/coordinator.py +238 -0
- refresh_engine-1.0.0/src/refresh_engine/_internal/execution.py +353 -0
- refresh_engine-1.0.0/src/refresh_engine/_internal/observation.py +174 -0
- refresh_engine-1.0.0/src/refresh_engine/_internal/pipeline.py +421 -0
- refresh_engine-1.0.0/src/refresh_engine/_internal/planning.py +236 -0
- refresh_engine-1.0.0/src/refresh_engine/_internal/reentrancy.py +36 -0
- refresh_engine-1.0.0/src/refresh_engine/_internal/values.py +97 -0
- refresh_engine-1.0.0/src/refresh_engine/contracts.py +335 -0
- refresh_engine-1.0.0/src/refresh_engine/engine.py +260 -0
- refresh_engine-1.0.0/src/refresh_engine/errors.py +85 -0
- refresh_engine-1.0.0/src/refresh_engine/events.py +333 -0
- refresh_engine-1.0.0/src/refresh_engine/fingerprints.py +356 -0
- refresh_engine-1.0.0/src/refresh_engine/models.py +601 -0
- refresh_engine-1.0.0/src/refresh_engine/observability.py +319 -0
- refresh_engine-1.0.0/src/refresh_engine/otel.py +193 -0
- refresh_engine-1.0.0/src/refresh_engine/scheduling.py +254 -0
- refresh_engine-1.0.0/src/refresh_engine/selectors.py +123 -0
- refresh_engine-1.0.0/src/refresh_engine/stores.py +62 -0
- refresh_engine-1.0.0/src/refresh_engine/testing.py +104 -0
- refresh_engine-0.1.0/.github/dependabot.yml +0 -12
- refresh_engine-0.1.0/.github/workflows/ci.yml +0 -49
- refresh_engine-0.1.0/.github/workflows/release.yml +0 -61
- refresh_engine-0.1.0/.github/workflows/security.yml +0 -26
- refresh_engine-0.1.0/.gitignore +0 -39
- refresh_engine-0.1.0/.readthedocs.yaml +0 -16
- refresh_engine-0.1.0/CHANGELOG.md +0 -33
- refresh_engine-0.1.0/CONTRIBUTING.md +0 -43
- refresh_engine-0.1.0/PKG-INFO +0 -95
- refresh_engine-0.1.0/README.md +0 -76
- refresh_engine-0.1.0/SECURITY.md +0 -26
- refresh_engine-0.1.0/benchmarks/README.md +0 -27
- refresh_engine-0.1.0/benchmarks/benchmark_refresh.py +0 -166
- refresh_engine-0.1.0/benchmarks/results/windows-python-3.12-2026-09-24.json +0 -354
- refresh_engine-0.1.0/docs/api/contracts.md +0 -48
- refresh_engine-0.1.0/docs/api/engine.md +0 -11
- refresh_engine-0.1.0/docs/api/index.md +0 -16
- refresh_engine-0.1.0/docs/api/models.md +0 -31
- refresh_engine-0.1.0/docs/api/services.md +0 -32
- refresh_engine-0.1.0/docs/api/strategies.md +0 -24
- refresh_engine-0.1.0/docs/architecture/decisions.md +0 -140
- refresh_engine-0.1.0/docs/architecture/lifecycle.md +0 -25
- refresh_engine-0.1.0/docs/architecture/overview.md +0 -62
- refresh_engine-0.1.0/docs/getting-started.md +0 -62
- refresh_engine-0.1.0/docs/guides/events-and-observability.md +0 -35
- refresh_engine-0.1.0/docs/guides/examples-and-results.md +0 -320
- refresh_engine-0.1.0/docs/guides/fingerprints-and-dependencies.md +0 -30
- refresh_engine-0.1.0/docs/guides/index.md +0 -15
- refresh_engine-0.1.0/docs/guides/refresh-modes.md +0 -33
- refresh_engine-0.1.0/docs/guides/sources-and-operations.md +0 -38
- refresh_engine-0.1.0/docs/guides/state-and-failure-safety.md +0 -76
- refresh_engine-0.1.0/docs/guides/triggers-and-scheduling.md +0 -35
- refresh_engine-0.1.0/docs/index.md +0 -25
- refresh_engine-0.1.0/examples/01_basic_and_modes.py +0 -37
- refresh_engine-0.1.0/examples/02_dependencies_and_fingerprints.py +0 -30
- refresh_engine-0.1.0/examples/03_triggers_and_selectors.py +0 -45
- refresh_engine-0.1.0/examples/04_async_and_external_scheduling.py +0 -37
- refresh_engine-0.1.0/examples/06_resilience_and_partial_discovery.py +0 -45
- refresh_engine-0.1.0/examples/07_events_and_observability.py +0 -42
- refresh_engine-0.1.0/examples/08_custom_store_and_overlap.py +0 -47
- refresh_engine-0.1.0/examples/09_filesystem_resources.py +0 -76
- refresh_engine-0.1.0/examples/10_scale_and_genericity.py +0 -51
- refresh_engine-0.1.0/examples/11_cancellation_and_shutdown.py +0 -46
- refresh_engine-0.1.0/examples/12_sqlite_state_store.py +0 -38
- refresh_engine-0.1.0/examples/13_abc_extensions.py +0 -50
- refresh_engine-0.1.0/examples/14_postgres_state_store.py +0 -57
- refresh_engine-0.1.0/examples/README.md +0 -37
- refresh_engine-0.1.0/examples/common.py +0 -46
- refresh_engine-0.1.0/examples/postgres_store.py +0 -148
- refresh_engine-0.1.0/examples/sqlite_store.py +0 -260
- refresh_engine-0.1.0/mkdocs.yml +0 -59
- refresh_engine-0.1.0/pyproject.toml +0 -83
- refresh_engine-0.1.0/scripts/check_docs_examples.py +0 -18
- refresh_engine-0.1.0/scripts/run_examples.py +0 -22
- refresh_engine-0.1.0/scripts/validate_distribution.py +0 -169
- refresh_engine-0.1.0/src/refresh_engine/api/__init__.py +0 -43
- refresh_engine-0.1.0/src/refresh_engine/api/abc.py +0 -176
- refresh_engine-0.1.0/src/refresh_engine/api/engine.py +0 -468
- refresh_engine-0.1.0/src/refresh_engine/api/protocols.py +0 -89
- refresh_engine-0.1.0/src/refresh_engine/core/models.py +0 -362
- refresh_engine-0.1.0/src/refresh_engine/detection/__init__.py +0 -21
- refresh_engine-0.1.0/src/refresh_engine/detection/detector.py +0 -54
- refresh_engine-0.1.0/src/refresh_engine/detection/fingerprints.py +0 -115
- refresh_engine-0.1.0/src/refresh_engine/errors.py +0 -57
- refresh_engine-0.1.0/src/refresh_engine/events/__init__.py +0 -6
- refresh_engine-0.1.0/src/refresh_engine/events/models.py +0 -42
- refresh_engine-0.1.0/src/refresh_engine/events/publisher.py +0 -77
- refresh_engine-0.1.0/src/refresh_engine/execution/__init__.py +0 -5
- refresh_engine-0.1.0/src/refresh_engine/execution/coordinator.py +0 -128
- refresh_engine-0.1.0/src/refresh_engine/execution/executor.py +0 -180
- refresh_engine-0.1.0/src/refresh_engine/observability/__init__.py +0 -9
- refresh_engine-0.1.0/src/refresh_engine/observability/logging.py +0 -49
- refresh_engine-0.1.0/src/refresh_engine/observability/metrics.py +0 -44
- refresh_engine-0.1.0/src/refresh_engine/planning/__init__.py +0 -6
- refresh_engine-0.1.0/src/refresh_engine/planning/dependency.py +0 -79
- refresh_engine-0.1.0/src/refresh_engine/planning/planner.py +0 -86
- refresh_engine-0.1.0/src/refresh_engine/scheduling/__init__.py +0 -13
- refresh_engine-0.1.0/src/refresh_engine/scheduling/async_scheduler.py +0 -126
- refresh_engine-0.1.0/src/refresh_engine/scheduling/external.py +0 -17
- refresh_engine-0.1.0/src/refresh_engine/stores/__init__.py +0 -11
- refresh_engine-0.1.0/src/refresh_engine/stores/memory.py +0 -77
- refresh_engine-0.1.0/src/refresh_engine/triggers/__init__.py +0 -16
- refresh_engine-0.1.0/src/refresh_engine/triggers/adapters.py +0 -38
- refresh_engine-0.1.0/src/refresh_engine/triggers/selectors.py +0 -46
- refresh_engine-0.1.0/tests/__init__.py +0 -1
- refresh_engine-0.1.0/tests/concurrency/test_execution.py +0 -134
- refresh_engine-0.1.0/tests/conftest.py +0 -1
- refresh_engine-0.1.0/tests/helpers.py +0 -77
- refresh_engine-0.1.0/tests/integration/test_incremental.py +0 -111
- refresh_engine-0.1.0/tests/integration/test_safety.py +0 -48
- refresh_engine-0.1.0/tests/scheduler/test_scheduler.py +0 -70
- refresh_engine-0.1.0/tests/unit/test_abstract_bases.py +0 -84
- refresh_engine-0.1.0/tests/unit/test_dependency.py +0 -55
- refresh_engine-0.1.0/tests/unit/test_events_store_selectors.py +0 -107
- refresh_engine-0.1.0/tests/unit/test_fingerprints.py +0 -91
- refresh_engine-0.1.0/tests/unit/test_public_api.py +0 -19
- refresh_engine-0.1.0/tests/unit/test_triggers_models.py +0 -49
- refresh_engine-0.1.0/uv.lock +0 -1138
- {refresh_engine-0.1.0 → refresh_engine-1.0.0}/LICENSE +0 -0
- {refresh_engine-0.1.0 → refresh_engine-1.0.0}/src/refresh_engine/py.typed +0 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: refresh-engine
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Asyncio-first, domain-neutral change detection and dependency-aware refresh orchestration for Python.
|
|
5
|
+
Keywords: asyncio,change-detection,dependency-graph,fingerprint,incremental,orchestration,refresh,scheduler,synchronization
|
|
6
|
+
Author: S. Muni Harish
|
|
7
|
+
License-Expression: Apache-2.0
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Framework :: AsyncIO
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Dist: structlog>=26.1.0
|
|
22
|
+
Requires-Dist: tenacity>=9.2.1
|
|
23
|
+
Requires-Dist: opentelemetry-api>=1.45.2,<2 ; extra == 'otel'
|
|
24
|
+
Requires-Dist: opentelemetry-sdk>=1.45.2,<2 ; extra == 'otel'
|
|
25
|
+
Requires-Python: >=3.12
|
|
26
|
+
Project-URL: Homepage, https://github.com/smuniharish/refresh-engine
|
|
27
|
+
Project-URL: Documentation, https://refresh-engine.readthedocs.io
|
|
28
|
+
Project-URL: Repository, https://github.com/smuniharish/refresh-engine
|
|
29
|
+
Project-URL: Issues, https://github.com/smuniharish/refresh-engine/issues
|
|
30
|
+
Project-URL: Changelog, https://refresh-engine.readthedocs.io/en/latest/changelog/
|
|
31
|
+
Provides-Extra: otel
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# refresh-engine
|
|
35
|
+
|
|
36
|
+
[](https://pypi.org/project/refresh-engine/)
|
|
37
|
+
[](https://pypi.org/project/refresh-engine/)
|
|
38
|
+
[](https://github.com/smuniharish/refresh-engine/actions/workflows/ci.yml)
|
|
39
|
+
[](https://refresh-engine.readthedocs.io/en/latest/)
|
|
40
|
+
[](https://github.com/smuniharish/refresh-engine/blob/HEAD/LICENSE)
|
|
41
|
+
|
|
42
|
+
**Keep anything in sync with its source by refreshing only what changed.**
|
|
43
|
+
|
|
44
|
+
refresh-engine is an asyncio-first, domain-neutral Python library for keeping
|
|
45
|
+
derived data current: search indexes, caches, embeddings, configuration,
|
|
46
|
+
registries, or anything else built from a source. You describe how to list and
|
|
47
|
+
read resources and how to apply a change; the engine detects what changed,
|
|
48
|
+
plans the work in dependency order, runs it safely, and remembers the outcome.
|
|
49
|
+
|
|
50
|
+
<picture>
|
|
51
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/smuniharish/refresh-engine/HEAD/docs/assets/diagrams/pipeline-dark.png">
|
|
52
|
+
<img alt="Discover, observe, detect changes, plan, execute, and persist" src="https://raw.githubusercontent.com/smuniharish/refresh-engine/HEAD/docs/assets/diagrams/pipeline-light.png">
|
|
53
|
+
</picture>
|
|
54
|
+
|
|
55
|
+
## Features
|
|
56
|
+
|
|
57
|
+
- **Precise change detection.** Deterministic fingerprints detect added,
|
|
58
|
+
modified, and deleted resources. Reliable cheap indicators, such as ETags or
|
|
59
|
+
version tokens, skip reading unchanged resources entirely.
|
|
60
|
+
- **Four refresh modes.** Incremental, full, dependency-aware, and targeted
|
|
61
|
+
refreshes, with targets chosen by ID or by composable selectors.
|
|
62
|
+
- **Dependency ordering.** Dependencies are refreshed before their dependents,
|
|
63
|
+
changes propagate to everything that depends on them, and cycles are isolated.
|
|
64
|
+
- **Safe by default.** Bounded concurrency, retries with jittered exponential
|
|
65
|
+
backoff, timeouts, and fail-fast or best-effort policies. A partial discovery
|
|
66
|
+
never deletes anything, and failed work is retried by the next refresh.
|
|
67
|
+
- **Durable state.** State advances only for successful resources, in one atomic
|
|
68
|
+
commit per refresh. Use the in-memory store or plug in your own database.
|
|
69
|
+
- **Scheduling and overlap control.** A drift-free interval scheduler and
|
|
70
|
+
policies that coalesce, queue, skip, or supersede overlapping requests.
|
|
71
|
+
- **Observability.** Typed lifecycle events with correlation IDs, structured
|
|
72
|
+
logging through [structlog](https://www.structlog.org) with secret redaction,
|
|
73
|
+
metrics, and optional OpenTelemetry spans and metrics using application-owned
|
|
74
|
+
providers.
|
|
75
|
+
- **Lightweight and typed.** Fully type-annotated, with two runtime
|
|
76
|
+
dependencies: structlog and [tenacity](https://tenacity.readthedocs.io).
|
|
77
|
+
|
|
78
|
+
## Installation
|
|
79
|
+
|
|
80
|
+
refresh-engine requires Python 3.12 or newer.
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pip install refresh-engine
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
For the optional OpenTelemetry integration, install
|
|
87
|
+
`refresh-engine[otel]`.
|
|
88
|
+
|
|
89
|
+
## Quick start
|
|
90
|
+
|
|
91
|
+
Implement a source that lists and reads resources, and an operation that applies
|
|
92
|
+
each change. The engine does the rest:
|
|
93
|
+
|
|
94
|
+
<!-- test -->
|
|
95
|
+
```python
|
|
96
|
+
import asyncio
|
|
97
|
+
|
|
98
|
+
from refresh_engine import (
|
|
99
|
+
DiscoveryResult,
|
|
100
|
+
PlanAction,
|
|
101
|
+
RefreshEngine,
|
|
102
|
+
ResourceSource,
|
|
103
|
+
RefreshRequest,
|
|
104
|
+
Resource,
|
|
105
|
+
ResourceSnapshot,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
CATALOG = {"apples": 120, "pears": 80}
|
|
109
|
+
SEARCH_INDEX: dict[str, int] = {}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class InventorySource(ResourceSource):
|
|
113
|
+
async def discover(self) -> DiscoveryResult:
|
|
114
|
+
return DiscoveryResult(Resource(item) for item in sorted(CATALOG))
|
|
115
|
+
|
|
116
|
+
async def snapshot(self, resource: Resource, /) -> ResourceSnapshot:
|
|
117
|
+
return ResourceSnapshot(
|
|
118
|
+
resource.resource_id, content=CATALOG[resource.resource_id]
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
async def update_index(
|
|
123
|
+
resource: Resource,
|
|
124
|
+
snapshot: ResourceSnapshot | None,
|
|
125
|
+
action: PlanAction,
|
|
126
|
+
request: RefreshRequest,
|
|
127
|
+
/,
|
|
128
|
+
) -> None:
|
|
129
|
+
if action is PlanAction.DELETE:
|
|
130
|
+
SEARCH_INDEX.pop(resource.resource_id, None)
|
|
131
|
+
elif snapshot is not None:
|
|
132
|
+
SEARCH_INDEX[resource.resource_id] = snapshot.content
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
async def main() -> None:
|
|
136
|
+
async with RefreshEngine(InventorySource(), update_index) as engine:
|
|
137
|
+
first = await engine.refresh()
|
|
138
|
+
print(f"{first.status}: refreshed {first.refreshed_count}", SEARCH_INDEX)
|
|
139
|
+
|
|
140
|
+
CATALOG["pears"] = 75
|
|
141
|
+
del CATALOG["apples"]
|
|
142
|
+
second = await engine.refresh()
|
|
143
|
+
print(
|
|
144
|
+
f"{second.status}: modified {second.modified_count},"
|
|
145
|
+
f" deleted {second.deleted_count}",
|
|
146
|
+
SEARCH_INDEX,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
asyncio.run(main())
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
```text
|
|
154
|
+
success: refreshed 2 {'apples': 120, 'pears': 80}
|
|
155
|
+
success: modified 1, deleted 1 {'pears': 75}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The second refresh touches only the modified and the deleted resource. Continue
|
|
159
|
+
with the [getting started guide](https://refresh-engine.readthedocs.io/en/latest/getting-started/)
|
|
160
|
+
to add durable state, dependencies, scheduling, and logging.
|
|
161
|
+
|
|
162
|
+
## Performance
|
|
163
|
+
|
|
164
|
+
The engine's overhead is small next to real I/O: with in-memory sources, a full
|
|
165
|
+
refresh of 100,000 resources takes about 3 seconds, and an incremental refresh
|
|
166
|
+
that finds nothing changed takes about half a second. See the
|
|
167
|
+
[benchmarks](https://github.com/smuniharish/refresh-engine/blob/HEAD/benchmarks/README.md)
|
|
168
|
+
for the method and details.
|
|
169
|
+
|
|
170
|
+
## Documentation
|
|
171
|
+
|
|
172
|
+
The [documentation](https://refresh-engine.readthedocs.io/en/latest/) covers:
|
|
173
|
+
|
|
174
|
+
- [Getting started](https://refresh-engine.readthedocs.io/en/latest/getting-started/)
|
|
175
|
+
and [concepts](https://refresh-engine.readthedocs.io/en/latest/concepts/)
|
|
176
|
+
- [Guides](https://refresh-engine.readthedocs.io/en/latest/guides/) for every
|
|
177
|
+
feature, including a
|
|
178
|
+
[production checklist](https://refresh-engine.readthedocs.io/en/latest/guides/production/)
|
|
179
|
+
- [Runnable examples](https://refresh-engine.readthedocs.io/en/latest/examples/),
|
|
180
|
+
including SQLite and PostgreSQL state stores
|
|
181
|
+
- The [API reference](https://refresh-engine.readthedocs.io/en/latest/api/)
|
|
182
|
+
- The [architecture](https://refresh-engine.readthedocs.io/en/latest/architecture/overview/)
|
|
183
|
+
and its guarantees
|
|
184
|
+
|
|
185
|
+
## Agent Skills
|
|
186
|
+
|
|
187
|
+
Coding agents such as Claude Code, Codex, Cursor, and GitHub Copilot can use the
|
|
188
|
+
refresh-engine [Agent Skill](https://refresh-engine.readthedocs.io/en/latest/agent-skills/)
|
|
189
|
+
to integrate the library correctly:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
npx skills add https://github.com/smuniharish/refresh-engine/tree/master/refresh-engine-skills/skills/refresh-engine
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Contributing
|
|
196
|
+
|
|
197
|
+
Contributions are welcome. Read the
|
|
198
|
+
[contributing guide](https://github.com/smuniharish/refresh-engine/blob/HEAD/CONTRIBUTING.md),
|
|
199
|
+
and report vulnerabilities as described in the
|
|
200
|
+
[security policy](https://github.com/smuniharish/refresh-engine/blob/HEAD/SECURITY.md).
|
|
201
|
+
Notable changes are listed in the
|
|
202
|
+
[changelog](https://github.com/smuniharish/refresh-engine/blob/HEAD/CHANGELOG.md).
|
|
203
|
+
|
|
204
|
+
## License
|
|
205
|
+
|
|
206
|
+
refresh-engine is licensed under the
|
|
207
|
+
[Apache License 2.0](https://github.com/smuniharish/refresh-engine/blob/HEAD/LICENSE).
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# refresh-engine
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/refresh-engine/)
|
|
4
|
+
[](https://pypi.org/project/refresh-engine/)
|
|
5
|
+
[](https://github.com/smuniharish/refresh-engine/actions/workflows/ci.yml)
|
|
6
|
+
[](https://refresh-engine.readthedocs.io/en/latest/)
|
|
7
|
+
[](https://github.com/smuniharish/refresh-engine/blob/HEAD/LICENSE)
|
|
8
|
+
|
|
9
|
+
**Keep anything in sync with its source by refreshing only what changed.**
|
|
10
|
+
|
|
11
|
+
refresh-engine is an asyncio-first, domain-neutral Python library for keeping
|
|
12
|
+
derived data current: search indexes, caches, embeddings, configuration,
|
|
13
|
+
registries, or anything else built from a source. You describe how to list and
|
|
14
|
+
read resources and how to apply a change; the engine detects what changed,
|
|
15
|
+
plans the work in dependency order, runs it safely, and remembers the outcome.
|
|
16
|
+
|
|
17
|
+
<picture>
|
|
18
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/smuniharish/refresh-engine/HEAD/docs/assets/diagrams/pipeline-dark.png">
|
|
19
|
+
<img alt="Discover, observe, detect changes, plan, execute, and persist" src="https://raw.githubusercontent.com/smuniharish/refresh-engine/HEAD/docs/assets/diagrams/pipeline-light.png">
|
|
20
|
+
</picture>
|
|
21
|
+
|
|
22
|
+
## Features
|
|
23
|
+
|
|
24
|
+
- **Precise change detection.** Deterministic fingerprints detect added,
|
|
25
|
+
modified, and deleted resources. Reliable cheap indicators, such as ETags or
|
|
26
|
+
version tokens, skip reading unchanged resources entirely.
|
|
27
|
+
- **Four refresh modes.** Incremental, full, dependency-aware, and targeted
|
|
28
|
+
refreshes, with targets chosen by ID or by composable selectors.
|
|
29
|
+
- **Dependency ordering.** Dependencies are refreshed before their dependents,
|
|
30
|
+
changes propagate to everything that depends on them, and cycles are isolated.
|
|
31
|
+
- **Safe by default.** Bounded concurrency, retries with jittered exponential
|
|
32
|
+
backoff, timeouts, and fail-fast or best-effort policies. A partial discovery
|
|
33
|
+
never deletes anything, and failed work is retried by the next refresh.
|
|
34
|
+
- **Durable state.** State advances only for successful resources, in one atomic
|
|
35
|
+
commit per refresh. Use the in-memory store or plug in your own database.
|
|
36
|
+
- **Scheduling and overlap control.** A drift-free interval scheduler and
|
|
37
|
+
policies that coalesce, queue, skip, or supersede overlapping requests.
|
|
38
|
+
- **Observability.** Typed lifecycle events with correlation IDs, structured
|
|
39
|
+
logging through [structlog](https://www.structlog.org) with secret redaction,
|
|
40
|
+
metrics, and optional OpenTelemetry spans and metrics using application-owned
|
|
41
|
+
providers.
|
|
42
|
+
- **Lightweight and typed.** Fully type-annotated, with two runtime
|
|
43
|
+
dependencies: structlog and [tenacity](https://tenacity.readthedocs.io).
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
refresh-engine requires Python 3.12 or newer.
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install refresh-engine
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
For the optional OpenTelemetry integration, install
|
|
54
|
+
`refresh-engine[otel]`.
|
|
55
|
+
|
|
56
|
+
## Quick start
|
|
57
|
+
|
|
58
|
+
Implement a source that lists and reads resources, and an operation that applies
|
|
59
|
+
each change. The engine does the rest:
|
|
60
|
+
|
|
61
|
+
<!-- test -->
|
|
62
|
+
```python
|
|
63
|
+
import asyncio
|
|
64
|
+
|
|
65
|
+
from refresh_engine import (
|
|
66
|
+
DiscoveryResult,
|
|
67
|
+
PlanAction,
|
|
68
|
+
RefreshEngine,
|
|
69
|
+
ResourceSource,
|
|
70
|
+
RefreshRequest,
|
|
71
|
+
Resource,
|
|
72
|
+
ResourceSnapshot,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
CATALOG = {"apples": 120, "pears": 80}
|
|
76
|
+
SEARCH_INDEX: dict[str, int] = {}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class InventorySource(ResourceSource):
|
|
80
|
+
async def discover(self) -> DiscoveryResult:
|
|
81
|
+
return DiscoveryResult(Resource(item) for item in sorted(CATALOG))
|
|
82
|
+
|
|
83
|
+
async def snapshot(self, resource: Resource, /) -> ResourceSnapshot:
|
|
84
|
+
return ResourceSnapshot(
|
|
85
|
+
resource.resource_id, content=CATALOG[resource.resource_id]
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
async def update_index(
|
|
90
|
+
resource: Resource,
|
|
91
|
+
snapshot: ResourceSnapshot | None,
|
|
92
|
+
action: PlanAction,
|
|
93
|
+
request: RefreshRequest,
|
|
94
|
+
/,
|
|
95
|
+
) -> None:
|
|
96
|
+
if action is PlanAction.DELETE:
|
|
97
|
+
SEARCH_INDEX.pop(resource.resource_id, None)
|
|
98
|
+
elif snapshot is not None:
|
|
99
|
+
SEARCH_INDEX[resource.resource_id] = snapshot.content
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
async def main() -> None:
|
|
103
|
+
async with RefreshEngine(InventorySource(), update_index) as engine:
|
|
104
|
+
first = await engine.refresh()
|
|
105
|
+
print(f"{first.status}: refreshed {first.refreshed_count}", SEARCH_INDEX)
|
|
106
|
+
|
|
107
|
+
CATALOG["pears"] = 75
|
|
108
|
+
del CATALOG["apples"]
|
|
109
|
+
second = await engine.refresh()
|
|
110
|
+
print(
|
|
111
|
+
f"{second.status}: modified {second.modified_count},"
|
|
112
|
+
f" deleted {second.deleted_count}",
|
|
113
|
+
SEARCH_INDEX,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
asyncio.run(main())
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
```text
|
|
121
|
+
success: refreshed 2 {'apples': 120, 'pears': 80}
|
|
122
|
+
success: modified 1, deleted 1 {'pears': 75}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The second refresh touches only the modified and the deleted resource. Continue
|
|
126
|
+
with the [getting started guide](https://refresh-engine.readthedocs.io/en/latest/getting-started/)
|
|
127
|
+
to add durable state, dependencies, scheduling, and logging.
|
|
128
|
+
|
|
129
|
+
## Performance
|
|
130
|
+
|
|
131
|
+
The engine's overhead is small next to real I/O: with in-memory sources, a full
|
|
132
|
+
refresh of 100,000 resources takes about 3 seconds, and an incremental refresh
|
|
133
|
+
that finds nothing changed takes about half a second. See the
|
|
134
|
+
[benchmarks](https://github.com/smuniharish/refresh-engine/blob/HEAD/benchmarks/README.md)
|
|
135
|
+
for the method and details.
|
|
136
|
+
|
|
137
|
+
## Documentation
|
|
138
|
+
|
|
139
|
+
The [documentation](https://refresh-engine.readthedocs.io/en/latest/) covers:
|
|
140
|
+
|
|
141
|
+
- [Getting started](https://refresh-engine.readthedocs.io/en/latest/getting-started/)
|
|
142
|
+
and [concepts](https://refresh-engine.readthedocs.io/en/latest/concepts/)
|
|
143
|
+
- [Guides](https://refresh-engine.readthedocs.io/en/latest/guides/) for every
|
|
144
|
+
feature, including a
|
|
145
|
+
[production checklist](https://refresh-engine.readthedocs.io/en/latest/guides/production/)
|
|
146
|
+
- [Runnable examples](https://refresh-engine.readthedocs.io/en/latest/examples/),
|
|
147
|
+
including SQLite and PostgreSQL state stores
|
|
148
|
+
- The [API reference](https://refresh-engine.readthedocs.io/en/latest/api/)
|
|
149
|
+
- The [architecture](https://refresh-engine.readthedocs.io/en/latest/architecture/overview/)
|
|
150
|
+
and its guarantees
|
|
151
|
+
|
|
152
|
+
## Agent Skills
|
|
153
|
+
|
|
154
|
+
Coding agents such as Claude Code, Codex, Cursor, and GitHub Copilot can use the
|
|
155
|
+
refresh-engine [Agent Skill](https://refresh-engine.readthedocs.io/en/latest/agent-skills/)
|
|
156
|
+
to integrate the library correctly:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
npx skills add https://github.com/smuniharish/refresh-engine/tree/master/refresh-engine-skills/skills/refresh-engine
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Contributing
|
|
163
|
+
|
|
164
|
+
Contributions are welcome. Read the
|
|
165
|
+
[contributing guide](https://github.com/smuniharish/refresh-engine/blob/HEAD/CONTRIBUTING.md),
|
|
166
|
+
and report vulnerabilities as described in the
|
|
167
|
+
[security policy](https://github.com/smuniharish/refresh-engine/blob/HEAD/SECURITY.md).
|
|
168
|
+
Notable changes are listed in the
|
|
169
|
+
[changelog](https://github.com/smuniharish/refresh-engine/blob/HEAD/CHANGELOG.md).
|
|
170
|
+
|
|
171
|
+
## License
|
|
172
|
+
|
|
173
|
+
refresh-engine is licensed under the
|
|
174
|
+
[Apache License 2.0](https://github.com/smuniharish/refresh-engine/blob/HEAD/LICENSE).
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["uv_build>=0.13.7,<0.14"]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "refresh-engine"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Asyncio-first, domain-neutral change detection and dependency-aware refresh orchestration for Python."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
license = "Apache-2.0"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
keywords = [
|
|
14
|
+
"asyncio",
|
|
15
|
+
"change-detection",
|
|
16
|
+
"dependency-graph",
|
|
17
|
+
"fingerprint",
|
|
18
|
+
"incremental",
|
|
19
|
+
"orchestration",
|
|
20
|
+
"refresh",
|
|
21
|
+
"scheduler",
|
|
22
|
+
"synchronization",
|
|
23
|
+
]
|
|
24
|
+
classifiers = [
|
|
25
|
+
"Development Status :: 5 - Production/Stable",
|
|
26
|
+
"Framework :: AsyncIO",
|
|
27
|
+
"Intended Audience :: Developers",
|
|
28
|
+
"Operating System :: OS Independent",
|
|
29
|
+
"Programming Language :: Python :: 3",
|
|
30
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
31
|
+
"Programming Language :: Python :: 3.12",
|
|
32
|
+
"Programming Language :: Python :: 3.13",
|
|
33
|
+
"Programming Language :: Python :: 3.14",
|
|
34
|
+
"Programming Language :: Python :: 3.15",
|
|
35
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
36
|
+
"Typing :: Typed",
|
|
37
|
+
]
|
|
38
|
+
dependencies = [
|
|
39
|
+
"structlog>=26.1.0",
|
|
40
|
+
"tenacity>=9.2.1",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[[project.authors]]
|
|
44
|
+
name = "S. Muni Harish"
|
|
45
|
+
|
|
46
|
+
[project.optional-dependencies]
|
|
47
|
+
otel = [
|
|
48
|
+
"opentelemetry-api>=1.45.2,<2",
|
|
49
|
+
"opentelemetry-sdk>=1.45.2,<2",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
[project.urls]
|
|
53
|
+
Homepage = "https://github.com/smuniharish/refresh-engine"
|
|
54
|
+
Documentation = "https://refresh-engine.readthedocs.io"
|
|
55
|
+
Repository = "https://github.com/smuniharish/refresh-engine"
|
|
56
|
+
Issues = "https://github.com/smuniharish/refresh-engine/issues"
|
|
57
|
+
Changelog = "https://refresh-engine.readthedocs.io/en/latest/changelog/"
|
|
58
|
+
|
|
59
|
+
[dependency-groups]
|
|
60
|
+
test = [
|
|
61
|
+
"coverage[toml]>=7.17.2",
|
|
62
|
+
"hypothesis>=6.170.15",
|
|
63
|
+
"pytest>=9.2.0",
|
|
64
|
+
"pytest-asyncio>=1.4.1.post1",
|
|
65
|
+
"pytest-cov>=7.1.0",
|
|
66
|
+
"pytest-timeout>=2.4.0",
|
|
67
|
+
"skills-ref>=0.1.1,<1",
|
|
68
|
+
]
|
|
69
|
+
lint = ["ruff>=0.17.4"]
|
|
70
|
+
types = ["pyrefly>=1.4.1"]
|
|
71
|
+
docs = [
|
|
72
|
+
"griffe-pydantic>=1.3.2,<2",
|
|
73
|
+
"mkdocstrings-python>=2.0.10,<3",
|
|
74
|
+
"zensical>=0.1.0,<0.2",
|
|
75
|
+
]
|
|
76
|
+
examples = ["psycopg[binary,pool]>=3.3.8,<4"]
|
|
77
|
+
package = ["twine>=7.0.0"]
|
|
78
|
+
audit = ["pip-audit>=2.10.1"]
|
|
79
|
+
|
|
80
|
+
[[dependency-groups.dev]]
|
|
81
|
+
include-group = "test"
|
|
82
|
+
|
|
83
|
+
[[dependency-groups.dev]]
|
|
84
|
+
include-group = "lint"
|
|
85
|
+
|
|
86
|
+
[[dependency-groups.dev]]
|
|
87
|
+
include-group = "types"
|
|
88
|
+
|
|
89
|
+
[tool.uv]
|
|
90
|
+
default-groups = ["dev"]
|
|
91
|
+
|
|
92
|
+
[tool.pytest]
|
|
93
|
+
minversion = "9.0"
|
|
94
|
+
addopts = [
|
|
95
|
+
"-ra",
|
|
96
|
+
"--strict-markers",
|
|
97
|
+
"--strict-config",
|
|
98
|
+
]
|
|
99
|
+
testpaths = ["tests"]
|
|
100
|
+
xfail_strict = true
|
|
101
|
+
filterwarnings = ["error"]
|
|
102
|
+
timeout = "120"
|
|
103
|
+
asyncio_mode = "auto"
|
|
104
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
105
|
+
asyncio_default_test_loop_scope = "function"
|
|
106
|
+
|
|
107
|
+
[tool.coverage.run]
|
|
108
|
+
branch = true
|
|
109
|
+
source_pkgs = ["refresh_engine"]
|
|
110
|
+
|
|
111
|
+
[tool.coverage.report]
|
|
112
|
+
fail_under = 100
|
|
113
|
+
show_missing = true
|
|
114
|
+
skip_covered = true
|
|
115
|
+
exclude_also = [
|
|
116
|
+
"if TYPE_CHECKING:",
|
|
117
|
+
'^\s*\.\.\.\s*$',
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
[tool.ruff]
|
|
121
|
+
target-version = "py312"
|
|
122
|
+
line-length = 88
|
|
123
|
+
|
|
124
|
+
[tool.ruff.format]
|
|
125
|
+
docstring-code-format = true
|
|
126
|
+
|
|
127
|
+
[tool.ruff.lint]
|
|
128
|
+
select = [
|
|
129
|
+
"ASYNC",
|
|
130
|
+
"B",
|
|
131
|
+
"C4",
|
|
132
|
+
"D",
|
|
133
|
+
"E",
|
|
134
|
+
"ERA",
|
|
135
|
+
"F",
|
|
136
|
+
"FURB",
|
|
137
|
+
"I",
|
|
138
|
+
"N",
|
|
139
|
+
"PERF",
|
|
140
|
+
"PGH",
|
|
141
|
+
"PIE",
|
|
142
|
+
"PT",
|
|
143
|
+
"RET",
|
|
144
|
+
"RUF",
|
|
145
|
+
"S",
|
|
146
|
+
"SIM",
|
|
147
|
+
"T20",
|
|
148
|
+
"UP",
|
|
149
|
+
"W",
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
[tool.ruff.lint.pydocstyle]
|
|
153
|
+
convention = "google"
|
|
154
|
+
|
|
155
|
+
[tool.ruff.lint.per-file-ignores]
|
|
156
|
+
"tests/**" = [
|
|
157
|
+
"D",
|
|
158
|
+
"S101",
|
|
159
|
+
"S311",
|
|
160
|
+
]
|
|
161
|
+
"src/refresh_engine/testing.py" = ["S101"]
|
|
162
|
+
"examples/**" = [
|
|
163
|
+
"D",
|
|
164
|
+
"S101",
|
|
165
|
+
"T201",
|
|
166
|
+
]
|
|
167
|
+
"benchmarks/**" = [
|
|
168
|
+
"D",
|
|
169
|
+
"S311",
|
|
170
|
+
"T201",
|
|
171
|
+
]
|
|
172
|
+
"scripts/**" = [
|
|
173
|
+
"D",
|
|
174
|
+
"S603",
|
|
175
|
+
"S607",
|
|
176
|
+
"T201",
|
|
177
|
+
]
|
|
178
|
+
"refresh-engine-skills/**" = ["T201"]
|
|
179
|
+
"tests/skill/**" = [
|
|
180
|
+
"D",
|
|
181
|
+
"S101",
|
|
182
|
+
"T201",
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
[tool.pyrefly]
|
|
186
|
+
project-includes = [
|
|
187
|
+
"src",
|
|
188
|
+
"tests",
|
|
189
|
+
"examples",
|
|
190
|
+
"scripts",
|
|
191
|
+
"benchmarks",
|
|
192
|
+
"refresh-engine-skills",
|
|
193
|
+
"tests/skill",
|
|
194
|
+
]
|
|
195
|
+
search-path = [
|
|
196
|
+
"src",
|
|
197
|
+
".",
|
|
198
|
+
"examples",
|
|
199
|
+
"scripts",
|
|
200
|
+
]
|
|
201
|
+
python-version = "3.12"
|