refresh-engine 0.2.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.
Files changed (63) hide show
  1. {refresh_engine-0.2.0 → refresh_engine-1.0.0}/PKG-INFO +188 -180
  2. {refresh_engine-0.2.0 → refresh_engine-1.0.0}/README.md +9 -5
  3. refresh_engine-1.0.0/pyproject.toml +201 -0
  4. refresh_engine-0.2.0/pyproject.toml → refresh_engine-1.0.0/pyproject.toml.orig +35 -41
  5. {refresh_engine-0.2.0 → refresh_engine-1.0.0}/src/refresh_engine/__init__.py +150 -156
  6. {refresh_engine-0.2.0 → refresh_engine-1.0.0}/src/refresh_engine/_internal/context.py +334 -334
  7. {refresh_engine-0.2.0 → refresh_engine-1.0.0}/src/refresh_engine/_internal/execution.py +30 -9
  8. {refresh_engine-0.2.0 → refresh_engine-1.0.0}/src/refresh_engine/_internal/pipeline.py +421 -402
  9. {refresh_engine-0.2.0 → refresh_engine-1.0.0}/src/refresh_engine/contracts.py +44 -208
  10. {refresh_engine-0.2.0 → refresh_engine-1.0.0}/src/refresh_engine/engine.py +260 -253
  11. {refresh_engine-0.2.0 → refresh_engine-1.0.0}/src/refresh_engine/events.py +333 -322
  12. {refresh_engine-0.2.0 → refresh_engine-1.0.0}/src/refresh_engine/fingerprints.py +356 -356
  13. {refresh_engine-0.2.0 → refresh_engine-1.0.0}/src/refresh_engine/models.py +9 -3
  14. {refresh_engine-0.2.0 → refresh_engine-1.0.0}/src/refresh_engine/observability.py +319 -315
  15. refresh_engine-1.0.0/src/refresh_engine/otel.py +193 -0
  16. {refresh_engine-0.2.0 → refresh_engine-1.0.0}/src/refresh_engine/scheduling.py +254 -368
  17. {refresh_engine-0.2.0 → refresh_engine-1.0.0}/src/refresh_engine/selectors.py +123 -123
  18. {refresh_engine-0.2.0 → refresh_engine-1.0.0}/src/refresh_engine/stores.py +62 -62
  19. refresh_engine-1.0.0/src/refresh_engine/testing.py +104 -0
  20. refresh_engine-0.2.0/.gitignore +0 -39
  21. refresh_engine-0.2.0/CHANGELOG.md +0 -180
  22. refresh_engine-0.2.0/tests/__init__.py +0 -1
  23. refresh_engine-0.2.0/tests/conftest.py +0 -17
  24. refresh_engine-0.2.0/tests/integration/__init__.py +0 -0
  25. refresh_engine-0.2.0/tests/integration/test_coordination.py +0 -413
  26. refresh_engine-0.2.0/tests/integration/test_events.py +0 -219
  27. refresh_engine-0.2.0/tests/integration/test_failures.py +0 -625
  28. refresh_engine-0.2.0/tests/integration/test_reentrancy.py +0 -396
  29. refresh_engine-0.2.0/tests/integration/test_refresh_modes.py +0 -436
  30. refresh_engine-0.2.0/tests/property/__init__.py +0 -0
  31. refresh_engine-0.2.0/tests/property/test_fingerprint_properties.py +0 -111
  32. refresh_engine-0.2.0/tests/property/test_planning_properties.py +0 -258
  33. refresh_engine-0.2.0/tests/property/test_redactor_properties.py +0 -75
  34. refresh_engine-0.2.0/tests/property/test_selector_properties.py +0 -107
  35. refresh_engine-0.2.0/tests/property/test_state_machines.py +0 -424
  36. refresh_engine-0.2.0/tests/support.py +0 -172
  37. refresh_engine-0.2.0/tests/unit/__init__.py +0 -0
  38. refresh_engine-0.2.0/tests/unit/test_concurrency.py +0 -55
  39. refresh_engine-0.2.0/tests/unit/test_contracts.py +0 -722
  40. refresh_engine-0.2.0/tests/unit/test_coordinator.py +0 -218
  41. refresh_engine-0.2.0/tests/unit/test_engine.py +0 -115
  42. refresh_engine-0.2.0/tests/unit/test_errors.py +0 -103
  43. refresh_engine-0.2.0/tests/unit/test_events.py +0 -615
  44. refresh_engine-0.2.0/tests/unit/test_fingerprints.py +0 -625
  45. refresh_engine-0.2.0/tests/unit/test_models.py +0 -923
  46. refresh_engine-0.2.0/tests/unit/test_observability.py +0 -720
  47. refresh_engine-0.2.0/tests/unit/test_planning.py +0 -197
  48. refresh_engine-0.2.0/tests/unit/test_public_api.py +0 -63
  49. refresh_engine-0.2.0/tests/unit/test_reentrancy.py +0 -36
  50. refresh_engine-0.2.0/tests/unit/test_scheduling.py +0 -926
  51. refresh_engine-0.2.0/tests/unit/test_selectors.py +0 -309
  52. refresh_engine-0.2.0/tests/unit/test_stores.py +0 -172
  53. refresh_engine-0.2.0/tests/unit/test_values.py +0 -239
  54. {refresh_engine-0.2.0 → refresh_engine-1.0.0}/LICENSE +0 -0
  55. {refresh_engine-0.2.0 → refresh_engine-1.0.0}/src/refresh_engine/_internal/__init__.py +0 -0
  56. {refresh_engine-0.2.0 → refresh_engine-1.0.0}/src/refresh_engine/_internal/concurrency.py +0 -0
  57. {refresh_engine-0.2.0 → refresh_engine-1.0.0}/src/refresh_engine/_internal/coordinator.py +0 -0
  58. {refresh_engine-0.2.0 → refresh_engine-1.0.0}/src/refresh_engine/_internal/observation.py +0 -0
  59. {refresh_engine-0.2.0 → refresh_engine-1.0.0}/src/refresh_engine/_internal/planning.py +0 -0
  60. {refresh_engine-0.2.0 → refresh_engine-1.0.0}/src/refresh_engine/_internal/reentrancy.py +0 -0
  61. {refresh_engine-0.2.0 → refresh_engine-1.0.0}/src/refresh_engine/_internal/values.py +0 -0
  62. {refresh_engine-0.2.0 → refresh_engine-1.0.0}/src/refresh_engine/errors.py +0 -0
  63. {refresh_engine-0.2.0 → refresh_engine-1.0.0}/src/refresh_engine/py.typed +0 -0
@@ -1,16 +1,11 @@
1
- Metadata-Version: 2.5
1
+ Metadata-Version: 2.4
2
2
  Name: refresh-engine
3
- Version: 0.2.0
3
+ Version: 1.0.0
4
4
  Summary: Asyncio-first, domain-neutral change detection and dependency-aware refresh orchestration for Python.
5
- Project-URL: Homepage, https://github.com/smuniharish/refresh-engine
6
- Project-URL: Documentation, https://refresh-engine.readthedocs.io
7
- Project-URL: Repository, https://github.com/smuniharish/refresh-engine
8
- Project-URL: Issues, https://github.com/smuniharish/refresh-engine/issues
9
- Project-URL: Changelog, https://refresh-engine.readthedocs.io/en/latest/changelog/
5
+ Keywords: asyncio,change-detection,dependency-graph,fingerprint,incremental,orchestration,refresh,scheduler,synchronization
10
6
  Author: S. Muni Harish
11
7
  License-Expression: Apache-2.0
12
8
  License-File: LICENSE
13
- Keywords: asyncio,change-detection,dependency-graph,fingerprint,incremental,orchestration,refresh,scheduler,synchronization
14
9
  Classifier: Development Status :: 5 - Production/Stable
15
10
  Classifier: Framework :: AsyncIO
16
11
  Classifier: Intended Audience :: Developers
@@ -20,180 +15,193 @@ Classifier: Programming Language :: Python :: 3 :: Only
20
15
  Classifier: Programming Language :: Python :: 3.12
21
16
  Classifier: Programming Language :: Python :: 3.13
22
17
  Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Programming Language :: Python :: 3.15
23
19
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
20
  Classifier: Typing :: Typed
25
- Requires-Python: >=3.12
26
21
  Requires-Dist: structlog>=26.1.0
27
- Requires-Dist: tenacity>=9.1.4
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
28
32
  Description-Content-Type: text/markdown
29
33
 
30
- # refresh-engine
31
-
32
- [![PyPI](https://img.shields.io/pypi/v/refresh-engine)](https://pypi.org/project/refresh-engine/)
33
- [![Python](https://img.shields.io/pypi/pyversions/refresh-engine)](https://pypi.org/project/refresh-engine/)
34
- [![CI](https://github.com/smuniharish/refresh-engine/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/smuniharish/refresh-engine/actions/workflows/ci.yml)
35
- [![Documentation](https://readthedocs.org/projects/refresh-engine/badge/?version=latest)](https://refresh-engine.readthedocs.io/en/latest/)
36
- [![License](https://img.shields.io/pypi/l/refresh-engine)](https://github.com/smuniharish/refresh-engine/blob/HEAD/LICENSE)
37
-
38
- **Keep anything in sync with its source by refreshing only what changed.**
39
-
40
- refresh-engine is an asyncio-first, domain-neutral Python library for keeping
41
- derived data current: search indexes, caches, embeddings, configuration,
42
- registries, or anything else built from a source. You describe how to list and
43
- read resources and how to apply a change; the engine detects what changed,
44
- plans the work in dependency order, runs it safely, and remembers the outcome.
45
-
46
- <picture>
47
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/smuniharish/refresh-engine/HEAD/docs/assets/diagrams/pipeline-dark.png">
48
- <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">
49
- </picture>
50
-
51
- ## Features
52
-
53
- - **Precise change detection.** Deterministic fingerprints detect added,
54
- modified, and deleted resources. Reliable cheap indicators, such as ETags or
55
- version tokens, skip reading unchanged resources entirely.
56
- - **Four refresh modes.** Incremental, full, dependency-aware, and targeted
57
- refreshes, with targets chosen by ID or by composable selectors.
58
- - **Dependency ordering.** Dependencies are refreshed before their dependents,
59
- changes propagate to everything that depends on them, and cycles are isolated.
60
- - **Safe by default.** Bounded concurrency, retries with jittered exponential
61
- backoff, timeouts, and fail-fast or best-effort policies. A partial discovery
62
- never deletes anything, and failed work is retried by the next refresh.
63
- - **Durable state.** State advances only for successful resources, in one atomic
64
- commit per refresh. Use the in-memory store or plug in your own database.
65
- - **Scheduling and overlap control.** A drift-free interval scheduler, a
66
- thread-safe bridge for external schedulers, and policies that coalesce, queue,
67
- skip, or supersede overlapping requests.
68
- - **Observability.** Typed lifecycle events with correlation IDs, structured
69
- logging through [structlog](https://www.structlog.org) with secret redaction,
70
- and metrics.
71
- - **Lightweight and typed.** Fully type-annotated, with two runtime
72
- dependencies: structlog and [tenacity](https://tenacity.readthedocs.io).
73
-
74
- ## Installation
75
-
76
- refresh-engine requires Python 3.12 or newer.
77
-
78
- ```bash
79
- pip install refresh-engine
80
- ```
81
-
82
- ## Quick start
83
-
84
- Implement a source that lists and reads resources, and an operation that applies
85
- each change. The engine does the rest:
86
-
87
- <!-- test -->
88
- ```python
89
- import asyncio
90
-
91
- from refresh_engine import (
92
- DiscoveryResult,
93
- PlanAction,
94
- RefreshEngine,
95
- RefreshRequest,
96
- Resource,
97
- ResourceSnapshot,
98
- )
99
-
100
- CATALOG = {"apples": 120, "pears": 80}
101
- SEARCH_INDEX: dict[str, int] = {}
102
-
103
-
104
- class InventorySource:
105
- async def discover(self) -> DiscoveryResult:
106
- return DiscoveryResult(Resource(item) for item in sorted(CATALOG))
107
-
108
- async def snapshot(self, resource: Resource, /) -> ResourceSnapshot:
109
- return ResourceSnapshot(
110
- resource.resource_id, content=CATALOG[resource.resource_id]
111
- )
112
-
113
-
114
- async def update_index(
115
- resource: Resource,
116
- snapshot: ResourceSnapshot | None,
117
- action: PlanAction,
118
- request: RefreshRequest,
119
- /,
120
- ) -> None:
121
- if action is PlanAction.DELETE:
122
- SEARCH_INDEX.pop(resource.resource_id, None)
123
- elif snapshot is not None:
124
- SEARCH_INDEX[resource.resource_id] = snapshot.content
125
-
126
-
127
- async def main() -> None:
128
- async with RefreshEngine(InventorySource(), update_index) as engine:
129
- first = await engine.refresh()
130
- print(f"{first.status}: refreshed {first.refreshed_count}", SEARCH_INDEX)
131
-
132
- CATALOG["pears"] = 75
133
- del CATALOG["apples"]
134
- second = await engine.refresh()
135
- print(
136
- f"{second.status}: modified {second.modified_count},"
137
- f" deleted {second.deleted_count}",
138
- SEARCH_INDEX,
139
- )
140
-
141
-
142
- asyncio.run(main())
143
- ```
144
-
145
- ```text
146
- success: refreshed 2 {'apples': 120, 'pears': 80}
147
- success: modified 1, deleted 1 {'pears': 75}
148
- ```
149
-
150
- The second refresh touches only the modified and the deleted resource. Continue
151
- with the [getting started guide](https://refresh-engine.readthedocs.io/en/latest/getting-started/)
152
- to add durable state, dependencies, scheduling, and logging.
153
-
154
- ## Performance
155
-
156
- The engine's overhead is small next to real I/O: with in-memory sources, a full
157
- refresh of 100,000 resources takes about 3 seconds, and an incremental refresh
158
- that finds nothing changed takes about half a second. See the
159
- [benchmarks](https://github.com/smuniharish/refresh-engine/blob/HEAD/benchmarks/README.md)
160
- for the method and details.
161
-
162
- ## Documentation
163
-
164
- The [documentation](https://refresh-engine.readthedocs.io/en/latest/) covers:
165
-
166
- - [Getting started](https://refresh-engine.readthedocs.io/en/latest/getting-started/)
167
- and [concepts](https://refresh-engine.readthedocs.io/en/latest/concepts/)
168
- - [Guides](https://refresh-engine.readthedocs.io/en/latest/guides/) for every
169
- feature, including a
170
- [production checklist](https://refresh-engine.readthedocs.io/en/latest/guides/production/)
171
- - [Runnable examples](https://refresh-engine.readthedocs.io/en/latest/examples/),
172
- including SQLite and PostgreSQL state stores
173
- - The [API reference](https://refresh-engine.readthedocs.io/en/latest/api/)
174
- - The [architecture](https://refresh-engine.readthedocs.io/en/latest/architecture/overview/)
175
- and its guarantees
176
-
177
- ## Agent Skills
178
-
179
- Coding agents such as Claude Code, Codex, Cursor, and GitHub Copilot can use the
180
- refresh-engine [Agent Skill](https://refresh-engine.readthedocs.io/en/latest/agent-skills/)
181
- to integrate the library correctly:
182
-
183
- ```bash
184
- npx skills add https://github.com/smuniharish/refresh-engine/tree/master/refresh-engine-skills/skills/refresh-engine
185
- ```
186
-
187
- ## Contributing
188
-
189
- Contributions are welcome. Read the
190
- [contributing guide](https://github.com/smuniharish/refresh-engine/blob/HEAD/CONTRIBUTING.md),
191
- and report vulnerabilities as described in the
192
- [security policy](https://github.com/smuniharish/refresh-engine/blob/HEAD/SECURITY.md).
193
- Notable changes are listed in the
194
- [changelog](https://github.com/smuniharish/refresh-engine/blob/HEAD/CHANGELOG.md).
195
-
196
- ## License
197
-
198
- refresh-engine is licensed under the
199
- [Apache License 2.0](https://github.com/smuniharish/refresh-engine/blob/HEAD/LICENSE).
34
+ # refresh-engine
35
+
36
+ [![PyPI](https://img.shields.io/pypi/v/refresh-engine)](https://pypi.org/project/refresh-engine/)
37
+ [![Python](https://img.shields.io/pypi/pyversions/refresh-engine)](https://pypi.org/project/refresh-engine/)
38
+ [![CI](https://github.com/smuniharish/refresh-engine/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/smuniharish/refresh-engine/actions/workflows/ci.yml)
39
+ [![Documentation](https://readthedocs.org/projects/refresh-engine/badge/?version=latest)](https://refresh-engine.readthedocs.io/en/latest/)
40
+ [![License](https://img.shields.io/pypi/l/refresh-engine)](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).
@@ -33,12 +33,12 @@ plans the work in dependency order, runs it safely, and remembers the outcome.
33
33
  never deletes anything, and failed work is retried by the next refresh.
34
34
  - **Durable state.** State advances only for successful resources, in one atomic
35
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, a
37
- thread-safe bridge for external schedulers, and policies that coalesce, queue,
38
- skip, or supersede overlapping requests.
36
+ - **Scheduling and overlap control.** A drift-free interval scheduler and
37
+ policies that coalesce, queue, skip, or supersede overlapping requests.
39
38
  - **Observability.** Typed lifecycle events with correlation IDs, structured
40
39
  logging through [structlog](https://www.structlog.org) with secret redaction,
41
- and metrics.
40
+ metrics, and optional OpenTelemetry spans and metrics using application-owned
41
+ providers.
42
42
  - **Lightweight and typed.** Fully type-annotated, with two runtime
43
43
  dependencies: structlog and [tenacity](https://tenacity.readthedocs.io).
44
44
 
@@ -50,6 +50,9 @@ refresh-engine requires Python 3.12 or newer.
50
50
  pip install refresh-engine
51
51
  ```
52
52
 
53
+ For the optional OpenTelemetry integration, install
54
+ `refresh-engine[otel]`.
55
+
53
56
  ## Quick start
54
57
 
55
58
  Implement a source that lists and reads resources, and an operation that applies
@@ -63,6 +66,7 @@ from refresh_engine import (
63
66
  DiscoveryResult,
64
67
  PlanAction,
65
68
  RefreshEngine,
69
+ ResourceSource,
66
70
  RefreshRequest,
67
71
  Resource,
68
72
  ResourceSnapshot,
@@ -72,7 +76,7 @@ CATALOG = {"apples": 120, "pears": 80}
72
76
  SEARCH_INDEX: dict[str, int] = {}
73
77
 
74
78
 
75
- class InventorySource:
79
+ class InventorySource(ResourceSource):
76
80
  async def discover(self) -> DiscoveryResult:
77
81
  return DiscoveryResult(Resource(item) for item in sorted(CATALOG))
78
82
 
@@ -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"