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.
Files changed (127) hide show
  1. refresh_engine-1.0.0/PKG-INFO +207 -0
  2. refresh_engine-1.0.0/README.md +174 -0
  3. refresh_engine-1.0.0/pyproject.toml +201 -0
  4. refresh_engine-1.0.0/pyproject.toml.orig +167 -0
  5. {refresh_engine-0.1.0 → refresh_engine-1.0.0}/src/refresh_engine/__init__.py +150 -169
  6. refresh_engine-1.0.0/src/refresh_engine/_internal/__init__.py +5 -0
  7. refresh_engine-1.0.0/src/refresh_engine/_internal/concurrency.py +58 -0
  8. refresh_engine-1.0.0/src/refresh_engine/_internal/context.py +334 -0
  9. refresh_engine-1.0.0/src/refresh_engine/_internal/coordinator.py +238 -0
  10. refresh_engine-1.0.0/src/refresh_engine/_internal/execution.py +353 -0
  11. refresh_engine-1.0.0/src/refresh_engine/_internal/observation.py +174 -0
  12. refresh_engine-1.0.0/src/refresh_engine/_internal/pipeline.py +421 -0
  13. refresh_engine-1.0.0/src/refresh_engine/_internal/planning.py +236 -0
  14. refresh_engine-1.0.0/src/refresh_engine/_internal/reentrancy.py +36 -0
  15. refresh_engine-1.0.0/src/refresh_engine/_internal/values.py +97 -0
  16. refresh_engine-1.0.0/src/refresh_engine/contracts.py +335 -0
  17. refresh_engine-1.0.0/src/refresh_engine/engine.py +260 -0
  18. refresh_engine-1.0.0/src/refresh_engine/errors.py +85 -0
  19. refresh_engine-1.0.0/src/refresh_engine/events.py +333 -0
  20. refresh_engine-1.0.0/src/refresh_engine/fingerprints.py +356 -0
  21. refresh_engine-1.0.0/src/refresh_engine/models.py +601 -0
  22. refresh_engine-1.0.0/src/refresh_engine/observability.py +319 -0
  23. refresh_engine-1.0.0/src/refresh_engine/otel.py +193 -0
  24. refresh_engine-1.0.0/src/refresh_engine/scheduling.py +254 -0
  25. refresh_engine-1.0.0/src/refresh_engine/selectors.py +123 -0
  26. refresh_engine-1.0.0/src/refresh_engine/stores.py +62 -0
  27. refresh_engine-1.0.0/src/refresh_engine/testing.py +104 -0
  28. refresh_engine-0.1.0/.github/dependabot.yml +0 -12
  29. refresh_engine-0.1.0/.github/workflows/ci.yml +0 -49
  30. refresh_engine-0.1.0/.github/workflows/release.yml +0 -61
  31. refresh_engine-0.1.0/.github/workflows/security.yml +0 -26
  32. refresh_engine-0.1.0/.gitignore +0 -39
  33. refresh_engine-0.1.0/.readthedocs.yaml +0 -16
  34. refresh_engine-0.1.0/CHANGELOG.md +0 -33
  35. refresh_engine-0.1.0/CONTRIBUTING.md +0 -43
  36. refresh_engine-0.1.0/PKG-INFO +0 -95
  37. refresh_engine-0.1.0/README.md +0 -76
  38. refresh_engine-0.1.0/SECURITY.md +0 -26
  39. refresh_engine-0.1.0/benchmarks/README.md +0 -27
  40. refresh_engine-0.1.0/benchmarks/benchmark_refresh.py +0 -166
  41. refresh_engine-0.1.0/benchmarks/results/windows-python-3.12-2026-09-24.json +0 -354
  42. refresh_engine-0.1.0/docs/api/contracts.md +0 -48
  43. refresh_engine-0.1.0/docs/api/engine.md +0 -11
  44. refresh_engine-0.1.0/docs/api/index.md +0 -16
  45. refresh_engine-0.1.0/docs/api/models.md +0 -31
  46. refresh_engine-0.1.0/docs/api/services.md +0 -32
  47. refresh_engine-0.1.0/docs/api/strategies.md +0 -24
  48. refresh_engine-0.1.0/docs/architecture/decisions.md +0 -140
  49. refresh_engine-0.1.0/docs/architecture/lifecycle.md +0 -25
  50. refresh_engine-0.1.0/docs/architecture/overview.md +0 -62
  51. refresh_engine-0.1.0/docs/getting-started.md +0 -62
  52. refresh_engine-0.1.0/docs/guides/events-and-observability.md +0 -35
  53. refresh_engine-0.1.0/docs/guides/examples-and-results.md +0 -320
  54. refresh_engine-0.1.0/docs/guides/fingerprints-and-dependencies.md +0 -30
  55. refresh_engine-0.1.0/docs/guides/index.md +0 -15
  56. refresh_engine-0.1.0/docs/guides/refresh-modes.md +0 -33
  57. refresh_engine-0.1.0/docs/guides/sources-and-operations.md +0 -38
  58. refresh_engine-0.1.0/docs/guides/state-and-failure-safety.md +0 -76
  59. refresh_engine-0.1.0/docs/guides/triggers-and-scheduling.md +0 -35
  60. refresh_engine-0.1.0/docs/index.md +0 -25
  61. refresh_engine-0.1.0/examples/01_basic_and_modes.py +0 -37
  62. refresh_engine-0.1.0/examples/02_dependencies_and_fingerprints.py +0 -30
  63. refresh_engine-0.1.0/examples/03_triggers_and_selectors.py +0 -45
  64. refresh_engine-0.1.0/examples/04_async_and_external_scheduling.py +0 -37
  65. refresh_engine-0.1.0/examples/06_resilience_and_partial_discovery.py +0 -45
  66. refresh_engine-0.1.0/examples/07_events_and_observability.py +0 -42
  67. refresh_engine-0.1.0/examples/08_custom_store_and_overlap.py +0 -47
  68. refresh_engine-0.1.0/examples/09_filesystem_resources.py +0 -76
  69. refresh_engine-0.1.0/examples/10_scale_and_genericity.py +0 -51
  70. refresh_engine-0.1.0/examples/11_cancellation_and_shutdown.py +0 -46
  71. refresh_engine-0.1.0/examples/12_sqlite_state_store.py +0 -38
  72. refresh_engine-0.1.0/examples/13_abc_extensions.py +0 -50
  73. refresh_engine-0.1.0/examples/14_postgres_state_store.py +0 -57
  74. refresh_engine-0.1.0/examples/README.md +0 -37
  75. refresh_engine-0.1.0/examples/common.py +0 -46
  76. refresh_engine-0.1.0/examples/postgres_store.py +0 -148
  77. refresh_engine-0.1.0/examples/sqlite_store.py +0 -260
  78. refresh_engine-0.1.0/mkdocs.yml +0 -59
  79. refresh_engine-0.1.0/pyproject.toml +0 -83
  80. refresh_engine-0.1.0/scripts/check_docs_examples.py +0 -18
  81. refresh_engine-0.1.0/scripts/run_examples.py +0 -22
  82. refresh_engine-0.1.0/scripts/validate_distribution.py +0 -169
  83. refresh_engine-0.1.0/src/refresh_engine/api/__init__.py +0 -43
  84. refresh_engine-0.1.0/src/refresh_engine/api/abc.py +0 -176
  85. refresh_engine-0.1.0/src/refresh_engine/api/engine.py +0 -468
  86. refresh_engine-0.1.0/src/refresh_engine/api/protocols.py +0 -89
  87. refresh_engine-0.1.0/src/refresh_engine/core/models.py +0 -362
  88. refresh_engine-0.1.0/src/refresh_engine/detection/__init__.py +0 -21
  89. refresh_engine-0.1.0/src/refresh_engine/detection/detector.py +0 -54
  90. refresh_engine-0.1.0/src/refresh_engine/detection/fingerprints.py +0 -115
  91. refresh_engine-0.1.0/src/refresh_engine/errors.py +0 -57
  92. refresh_engine-0.1.0/src/refresh_engine/events/__init__.py +0 -6
  93. refresh_engine-0.1.0/src/refresh_engine/events/models.py +0 -42
  94. refresh_engine-0.1.0/src/refresh_engine/events/publisher.py +0 -77
  95. refresh_engine-0.1.0/src/refresh_engine/execution/__init__.py +0 -5
  96. refresh_engine-0.1.0/src/refresh_engine/execution/coordinator.py +0 -128
  97. refresh_engine-0.1.0/src/refresh_engine/execution/executor.py +0 -180
  98. refresh_engine-0.1.0/src/refresh_engine/observability/__init__.py +0 -9
  99. refresh_engine-0.1.0/src/refresh_engine/observability/logging.py +0 -49
  100. refresh_engine-0.1.0/src/refresh_engine/observability/metrics.py +0 -44
  101. refresh_engine-0.1.0/src/refresh_engine/planning/__init__.py +0 -6
  102. refresh_engine-0.1.0/src/refresh_engine/planning/dependency.py +0 -79
  103. refresh_engine-0.1.0/src/refresh_engine/planning/planner.py +0 -86
  104. refresh_engine-0.1.0/src/refresh_engine/scheduling/__init__.py +0 -13
  105. refresh_engine-0.1.0/src/refresh_engine/scheduling/async_scheduler.py +0 -126
  106. refresh_engine-0.1.0/src/refresh_engine/scheduling/external.py +0 -17
  107. refresh_engine-0.1.0/src/refresh_engine/stores/__init__.py +0 -11
  108. refresh_engine-0.1.0/src/refresh_engine/stores/memory.py +0 -77
  109. refresh_engine-0.1.0/src/refresh_engine/triggers/__init__.py +0 -16
  110. refresh_engine-0.1.0/src/refresh_engine/triggers/adapters.py +0 -38
  111. refresh_engine-0.1.0/src/refresh_engine/triggers/selectors.py +0 -46
  112. refresh_engine-0.1.0/tests/__init__.py +0 -1
  113. refresh_engine-0.1.0/tests/concurrency/test_execution.py +0 -134
  114. refresh_engine-0.1.0/tests/conftest.py +0 -1
  115. refresh_engine-0.1.0/tests/helpers.py +0 -77
  116. refresh_engine-0.1.0/tests/integration/test_incremental.py +0 -111
  117. refresh_engine-0.1.0/tests/integration/test_safety.py +0 -48
  118. refresh_engine-0.1.0/tests/scheduler/test_scheduler.py +0 -70
  119. refresh_engine-0.1.0/tests/unit/test_abstract_bases.py +0 -84
  120. refresh_engine-0.1.0/tests/unit/test_dependency.py +0 -55
  121. refresh_engine-0.1.0/tests/unit/test_events_store_selectors.py +0 -107
  122. refresh_engine-0.1.0/tests/unit/test_fingerprints.py +0 -91
  123. refresh_engine-0.1.0/tests/unit/test_public_api.py +0 -19
  124. refresh_engine-0.1.0/tests/unit/test_triggers_models.py +0 -49
  125. refresh_engine-0.1.0/uv.lock +0 -1138
  126. {refresh_engine-0.1.0 → refresh_engine-1.0.0}/LICENSE +0 -0
  127. {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
+ [![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).
@@ -0,0 +1,174 @@
1
+ # refresh-engine
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/refresh-engine)](https://pypi.org/project/refresh-engine/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/refresh-engine)](https://pypi.org/project/refresh-engine/)
5
+ [![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)
6
+ [![Documentation](https://readthedocs.org/projects/refresh-engine/badge/?version=latest)](https://refresh-engine.readthedocs.io/en/latest/)
7
+ [![License](https://img.shields.io/pypi/l/refresh-engine)](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"