refresh-engine 0.1.0__tar.gz → 0.2.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 (156) hide show
  1. refresh_engine-0.2.0/CHANGELOG.md +180 -0
  2. refresh_engine-0.2.0/PKG-INFO +199 -0
  3. refresh_engine-0.2.0/README.md +170 -0
  4. refresh_engine-0.2.0/pyproject.toml +173 -0
  5. {refresh_engine-0.1.0 → refresh_engine-0.2.0}/src/refresh_engine/__init__.py +51 -64
  6. refresh_engine-0.2.0/src/refresh_engine/_internal/__init__.py +5 -0
  7. refresh_engine-0.2.0/src/refresh_engine/_internal/concurrency.py +58 -0
  8. refresh_engine-0.2.0/src/refresh_engine/_internal/context.py +334 -0
  9. refresh_engine-0.2.0/src/refresh_engine/_internal/coordinator.py +238 -0
  10. refresh_engine-0.2.0/src/refresh_engine/_internal/execution.py +332 -0
  11. refresh_engine-0.2.0/src/refresh_engine/_internal/observation.py +174 -0
  12. refresh_engine-0.2.0/src/refresh_engine/_internal/pipeline.py +402 -0
  13. refresh_engine-0.2.0/src/refresh_engine/_internal/planning.py +236 -0
  14. refresh_engine-0.2.0/src/refresh_engine/_internal/reentrancy.py +36 -0
  15. refresh_engine-0.2.0/src/refresh_engine/_internal/values.py +97 -0
  16. refresh_engine-0.2.0/src/refresh_engine/contracts.py +499 -0
  17. refresh_engine-0.2.0/src/refresh_engine/engine.py +253 -0
  18. refresh_engine-0.2.0/src/refresh_engine/errors.py +85 -0
  19. refresh_engine-0.2.0/src/refresh_engine/events.py +322 -0
  20. refresh_engine-0.2.0/src/refresh_engine/fingerprints.py +356 -0
  21. refresh_engine-0.2.0/src/refresh_engine/models.py +595 -0
  22. refresh_engine-0.2.0/src/refresh_engine/observability.py +315 -0
  23. refresh_engine-0.2.0/src/refresh_engine/scheduling.py +368 -0
  24. refresh_engine-0.2.0/src/refresh_engine/selectors.py +123 -0
  25. refresh_engine-0.2.0/src/refresh_engine/stores.py +62 -0
  26. refresh_engine-0.2.0/tests/conftest.py +17 -0
  27. refresh_engine-0.2.0/tests/integration/__init__.py +0 -0
  28. refresh_engine-0.2.0/tests/integration/test_coordination.py +413 -0
  29. refresh_engine-0.2.0/tests/integration/test_events.py +219 -0
  30. refresh_engine-0.2.0/tests/integration/test_failures.py +625 -0
  31. refresh_engine-0.2.0/tests/integration/test_reentrancy.py +396 -0
  32. refresh_engine-0.2.0/tests/integration/test_refresh_modes.py +436 -0
  33. refresh_engine-0.2.0/tests/property/__init__.py +0 -0
  34. refresh_engine-0.2.0/tests/property/test_fingerprint_properties.py +111 -0
  35. refresh_engine-0.2.0/tests/property/test_planning_properties.py +258 -0
  36. refresh_engine-0.2.0/tests/property/test_redactor_properties.py +75 -0
  37. refresh_engine-0.2.0/tests/property/test_selector_properties.py +107 -0
  38. refresh_engine-0.2.0/tests/property/test_state_machines.py +424 -0
  39. refresh_engine-0.2.0/tests/support.py +172 -0
  40. refresh_engine-0.2.0/tests/unit/__init__.py +0 -0
  41. refresh_engine-0.2.0/tests/unit/test_concurrency.py +55 -0
  42. refresh_engine-0.2.0/tests/unit/test_contracts.py +722 -0
  43. refresh_engine-0.2.0/tests/unit/test_coordinator.py +218 -0
  44. refresh_engine-0.2.0/tests/unit/test_engine.py +115 -0
  45. refresh_engine-0.2.0/tests/unit/test_errors.py +103 -0
  46. refresh_engine-0.2.0/tests/unit/test_events.py +615 -0
  47. refresh_engine-0.2.0/tests/unit/test_fingerprints.py +625 -0
  48. refresh_engine-0.2.0/tests/unit/test_models.py +923 -0
  49. refresh_engine-0.2.0/tests/unit/test_observability.py +720 -0
  50. refresh_engine-0.2.0/tests/unit/test_planning.py +197 -0
  51. refresh_engine-0.2.0/tests/unit/test_public_api.py +63 -0
  52. refresh_engine-0.2.0/tests/unit/test_reentrancy.py +36 -0
  53. refresh_engine-0.2.0/tests/unit/test_scheduling.py +926 -0
  54. refresh_engine-0.2.0/tests/unit/test_selectors.py +309 -0
  55. refresh_engine-0.2.0/tests/unit/test_stores.py +172 -0
  56. refresh_engine-0.2.0/tests/unit/test_values.py +239 -0
  57. refresh_engine-0.1.0/.github/dependabot.yml +0 -12
  58. refresh_engine-0.1.0/.github/workflows/ci.yml +0 -49
  59. refresh_engine-0.1.0/.github/workflows/release.yml +0 -61
  60. refresh_engine-0.1.0/.github/workflows/security.yml +0 -26
  61. refresh_engine-0.1.0/.readthedocs.yaml +0 -16
  62. refresh_engine-0.1.0/CHANGELOG.md +0 -33
  63. refresh_engine-0.1.0/CONTRIBUTING.md +0 -43
  64. refresh_engine-0.1.0/PKG-INFO +0 -95
  65. refresh_engine-0.1.0/README.md +0 -76
  66. refresh_engine-0.1.0/SECURITY.md +0 -26
  67. refresh_engine-0.1.0/benchmarks/README.md +0 -27
  68. refresh_engine-0.1.0/benchmarks/benchmark_refresh.py +0 -166
  69. refresh_engine-0.1.0/benchmarks/results/windows-python-3.12-2026-09-24.json +0 -354
  70. refresh_engine-0.1.0/docs/api/contracts.md +0 -48
  71. refresh_engine-0.1.0/docs/api/engine.md +0 -11
  72. refresh_engine-0.1.0/docs/api/index.md +0 -16
  73. refresh_engine-0.1.0/docs/api/models.md +0 -31
  74. refresh_engine-0.1.0/docs/api/services.md +0 -32
  75. refresh_engine-0.1.0/docs/api/strategies.md +0 -24
  76. refresh_engine-0.1.0/docs/architecture/decisions.md +0 -140
  77. refresh_engine-0.1.0/docs/architecture/lifecycle.md +0 -25
  78. refresh_engine-0.1.0/docs/architecture/overview.md +0 -62
  79. refresh_engine-0.1.0/docs/getting-started.md +0 -62
  80. refresh_engine-0.1.0/docs/guides/events-and-observability.md +0 -35
  81. refresh_engine-0.1.0/docs/guides/examples-and-results.md +0 -320
  82. refresh_engine-0.1.0/docs/guides/fingerprints-and-dependencies.md +0 -30
  83. refresh_engine-0.1.0/docs/guides/index.md +0 -15
  84. refresh_engine-0.1.0/docs/guides/refresh-modes.md +0 -33
  85. refresh_engine-0.1.0/docs/guides/sources-and-operations.md +0 -38
  86. refresh_engine-0.1.0/docs/guides/state-and-failure-safety.md +0 -76
  87. refresh_engine-0.1.0/docs/guides/triggers-and-scheduling.md +0 -35
  88. refresh_engine-0.1.0/docs/index.md +0 -25
  89. refresh_engine-0.1.0/examples/01_basic_and_modes.py +0 -37
  90. refresh_engine-0.1.0/examples/02_dependencies_and_fingerprints.py +0 -30
  91. refresh_engine-0.1.0/examples/03_triggers_and_selectors.py +0 -45
  92. refresh_engine-0.1.0/examples/04_async_and_external_scheduling.py +0 -37
  93. refresh_engine-0.1.0/examples/06_resilience_and_partial_discovery.py +0 -45
  94. refresh_engine-0.1.0/examples/07_events_and_observability.py +0 -42
  95. refresh_engine-0.1.0/examples/08_custom_store_and_overlap.py +0 -47
  96. refresh_engine-0.1.0/examples/09_filesystem_resources.py +0 -76
  97. refresh_engine-0.1.0/examples/10_scale_and_genericity.py +0 -51
  98. refresh_engine-0.1.0/examples/11_cancellation_and_shutdown.py +0 -46
  99. refresh_engine-0.1.0/examples/12_sqlite_state_store.py +0 -38
  100. refresh_engine-0.1.0/examples/13_abc_extensions.py +0 -50
  101. refresh_engine-0.1.0/examples/14_postgres_state_store.py +0 -57
  102. refresh_engine-0.1.0/examples/README.md +0 -37
  103. refresh_engine-0.1.0/examples/common.py +0 -46
  104. refresh_engine-0.1.0/examples/postgres_store.py +0 -148
  105. refresh_engine-0.1.0/examples/sqlite_store.py +0 -260
  106. refresh_engine-0.1.0/mkdocs.yml +0 -59
  107. refresh_engine-0.1.0/pyproject.toml +0 -83
  108. refresh_engine-0.1.0/scripts/check_docs_examples.py +0 -18
  109. refresh_engine-0.1.0/scripts/run_examples.py +0 -22
  110. refresh_engine-0.1.0/scripts/validate_distribution.py +0 -169
  111. refresh_engine-0.1.0/src/refresh_engine/api/__init__.py +0 -43
  112. refresh_engine-0.1.0/src/refresh_engine/api/abc.py +0 -176
  113. refresh_engine-0.1.0/src/refresh_engine/api/engine.py +0 -468
  114. refresh_engine-0.1.0/src/refresh_engine/api/protocols.py +0 -89
  115. refresh_engine-0.1.0/src/refresh_engine/core/models.py +0 -362
  116. refresh_engine-0.1.0/src/refresh_engine/detection/__init__.py +0 -21
  117. refresh_engine-0.1.0/src/refresh_engine/detection/detector.py +0 -54
  118. refresh_engine-0.1.0/src/refresh_engine/detection/fingerprints.py +0 -115
  119. refresh_engine-0.1.0/src/refresh_engine/errors.py +0 -57
  120. refresh_engine-0.1.0/src/refresh_engine/events/__init__.py +0 -6
  121. refresh_engine-0.1.0/src/refresh_engine/events/models.py +0 -42
  122. refresh_engine-0.1.0/src/refresh_engine/events/publisher.py +0 -77
  123. refresh_engine-0.1.0/src/refresh_engine/execution/__init__.py +0 -5
  124. refresh_engine-0.1.0/src/refresh_engine/execution/coordinator.py +0 -128
  125. refresh_engine-0.1.0/src/refresh_engine/execution/executor.py +0 -180
  126. refresh_engine-0.1.0/src/refresh_engine/observability/__init__.py +0 -9
  127. refresh_engine-0.1.0/src/refresh_engine/observability/logging.py +0 -49
  128. refresh_engine-0.1.0/src/refresh_engine/observability/metrics.py +0 -44
  129. refresh_engine-0.1.0/src/refresh_engine/planning/__init__.py +0 -6
  130. refresh_engine-0.1.0/src/refresh_engine/planning/dependency.py +0 -79
  131. refresh_engine-0.1.0/src/refresh_engine/planning/planner.py +0 -86
  132. refresh_engine-0.1.0/src/refresh_engine/scheduling/__init__.py +0 -13
  133. refresh_engine-0.1.0/src/refresh_engine/scheduling/async_scheduler.py +0 -126
  134. refresh_engine-0.1.0/src/refresh_engine/scheduling/external.py +0 -17
  135. refresh_engine-0.1.0/src/refresh_engine/stores/__init__.py +0 -11
  136. refresh_engine-0.1.0/src/refresh_engine/stores/memory.py +0 -77
  137. refresh_engine-0.1.0/src/refresh_engine/triggers/__init__.py +0 -16
  138. refresh_engine-0.1.0/src/refresh_engine/triggers/adapters.py +0 -38
  139. refresh_engine-0.1.0/src/refresh_engine/triggers/selectors.py +0 -46
  140. refresh_engine-0.1.0/tests/concurrency/test_execution.py +0 -134
  141. refresh_engine-0.1.0/tests/conftest.py +0 -1
  142. refresh_engine-0.1.0/tests/helpers.py +0 -77
  143. refresh_engine-0.1.0/tests/integration/test_incremental.py +0 -111
  144. refresh_engine-0.1.0/tests/integration/test_safety.py +0 -48
  145. refresh_engine-0.1.0/tests/scheduler/test_scheduler.py +0 -70
  146. refresh_engine-0.1.0/tests/unit/test_abstract_bases.py +0 -84
  147. refresh_engine-0.1.0/tests/unit/test_dependency.py +0 -55
  148. refresh_engine-0.1.0/tests/unit/test_events_store_selectors.py +0 -107
  149. refresh_engine-0.1.0/tests/unit/test_fingerprints.py +0 -91
  150. refresh_engine-0.1.0/tests/unit/test_public_api.py +0 -19
  151. refresh_engine-0.1.0/tests/unit/test_triggers_models.py +0 -49
  152. refresh_engine-0.1.0/uv.lock +0 -1138
  153. {refresh_engine-0.1.0 → refresh_engine-0.2.0}/.gitignore +0 -0
  154. {refresh_engine-0.1.0 → refresh_engine-0.2.0}/LICENSE +0 -0
  155. {refresh_engine-0.1.0 → refresh_engine-0.2.0}/src/refresh_engine/py.typed +0 -0
  156. {refresh_engine-0.1.0 → refresh_engine-0.2.0}/tests/__init__.py +0 -0
@@ -0,0 +1,180 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project adheres to
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.2.0] - 2026-10-05
10
+
11
+ This release reworks the engine for correctness, speed, and a smaller, clearer API.
12
+ It contains breaking changes; read the [upgrade notes](#upgrading-from-010) first.
13
+
14
+ ### Added
15
+
16
+ - Support for Python 3.13 and 3.14.
17
+ - Selector-based targeted refreshes: `refresh(selector=...)` and
18
+ `RefreshRequest.selector`. Selectors compose with `&`, `|`, and `~`, and
19
+ `TagSelector` reads tags from a configurable metadata `key`.
20
+ - `MetricsEventHandler`, which turns lifecycle events into counters and observations
21
+ on any `MetricsSink`.
22
+ - `ExternalSchedulerAdapter.submit_threadsafe()` and `run_threadsafe()` for
23
+ schedulers that run outside the event loop.
24
+ - `RefreshEngine` and `AsyncScheduler` are asynchronous context managers, and
25
+ `RefreshEngine.closed` reports whether the engine accepts requests.
26
+ - The source, operation, fingerprint strategy, and state store of a refresh run in
27
+ the context of the code that requested it, so context variables such as tracing
28
+ spans reach them. Event handlers run in a fresh context.
29
+ - Protection against waiting for oneself: `RefreshEngine.close(wait=False)` and
30
+ `AsyncScheduler.stop(wait=False)` shut down without waiting, for code that a
31
+ refresh or scheduled run waits for. Awaited directly by the engine's own source,
32
+ operation, fingerprint strategy, or state store, `refresh()` and `submit()` raise
33
+ `RuntimeError` and `close()` returns without waiting. Awaited directly by a
34
+ handler, `EventPublisher.drain()` returns immediately.
35
+ - `canonical_digest()` for writing fingerprint strategies. Fingerprints now support
36
+ `bytearray`, `memoryview`, `UUID`, `Decimal`, `time`, `timedelta`, and enumerations
37
+ with any supported value.
38
+ - `StateTransactionABC` stages changes and passes them to a single abstract method,
39
+ `apply_changes()`, which makes durable stores much shorter to write.
40
+ - Automatic retries: an unchanged resource whose planned refresh did not complete,
41
+ such as a dependent skipped after a failure, is retried by the next refresh in any
42
+ mode.
43
+ - Dependency cycles fail only the resources in the cycle, each with the cycle it
44
+ belongs to, and skip their dependents; everything else is refreshed.
45
+ - `RefreshPhase`, `RefreshResult.request`, `RefreshResult.dependency_changed_count`,
46
+ `RefreshEvent.correlation_id`, and `RefreshEvent.error`.
47
+ - Event kinds `SNAPSHOT_STARTED`, `SNAPSHOT_COMPLETED`, `RESOURCE_SNAPSHOT_FAILED`,
48
+ `RESOURCE_REFRESH_SKIPPED`, `REFRESH_SKIPPED`, and `SCHEDULER_RUN_FAILED`, and
49
+ richer attributes on every event, including the request metadata on
50
+ `REFRESH_STARTED`.
51
+ - `EngineClosedError`, and `SnapshotError` for snapshots that violate the source
52
+ contract.
53
+ - `DiscoveryResult` accepts synchronous iterables as well as asynchronous ones.
54
+
55
+ ### Changed
56
+
57
+ - **Breaking:** the supported Python versions are 3.12 and newer, and the runtime
58
+ dependencies are `structlog>=26.1.0` and `tenacity>=9.1.4`. APScheduler and NetworkX
59
+ are no longer required.
60
+ - **Breaking:** every public name is imported from `refresh_engine`. The
61
+ `api`, `core`, `detection`, `events`, `execution`, `observability`, `planning`,
62
+ `scheduling`, `stores`, and `triggers` subpackages were replaced.
63
+ - **Breaking:** the fingerprint encoding is new, so the first refresh after
64
+ upgrading reports every resource as modified and refreshes it once.
65
+ `CompositeHash` no longer fingerprints dependencies; dependency changes are
66
+ reported as `DEPENDENCY_CHANGED`. `VersionTokenFingerprint` and `ETagFingerprint`
67
+ use the token itself as the fingerprint value.
68
+ - **Breaking:** operations always receive a `Resource`, including for deletions,
69
+ and protocol parameters are positional-only.
70
+ - **Breaking:** `EventPublisher.publish()` is synchronous and never blocks.
71
+ Synchronous handlers run on the event loop, and coroutine handlers are awaited.
72
+ `EventPublisherABC` gains `has_subscribers`.
73
+ - **Breaking:** `StructlogEventHandler` logs the event kind as the event name, uses
74
+ per-kind log levels, adds `occurred_at`, omits empty IDs, and passes failures as
75
+ `exc_info`. `default_redactor` matches credential-like keys in any naming style
76
+ and at any nesting depth.
77
+ - **Breaking:** `TimeoutConfig.resource_refresh` is renamed to `operation`.
78
+ - **Breaking:** `RefreshConfig.cancel_active_on_shutdown` is replaced by
79
+ `RefreshEngine.close(cancel=...)`, and `AsyncScheduler(cancel_active_on_stop=...)`
80
+ by `AsyncScheduler.stop(cancel=...)`.
81
+ - **Breaking:** model fields after the identifier are keyword-only.
82
+ `Resource.dependencies` and `ResourceSnapshot.dependencies` default to `None`,
83
+ meaning "not declared". Invalid arguments raise `ConfigurationError`, a
84
+ `ValueError`.
85
+ - **Breaking:** overlapping requests are coalesced only when one request covers the
86
+ other, so incremental and targeted requests run separately. Requests superseded
87
+ under `CANCEL_PREVIOUS`, or cancelled by `close(cancel=True)`, now return a
88
+ `CANCELLED` result instead of raising `asyncio.CancelledError`.
89
+ - **Breaking:** statuses follow precise rules: `PARTIAL` when some but not all
90
+ in-scope resources failed, `FAILED` when the refresh could not complete, the
91
+ fail-fast policy stopped it, or everything failed. `RefreshResult.failed_count` is
92
+ a property equal to `len(errors)`.
93
+ - **Breaking:** an overall timeout returns a `FAILED` result instead of raising
94
+ `TimeoutError`, and the best-effort policy still commits completed work.
95
+ - **Breaking:** `ExternalSchedulerAdapter` is created with `submit` and an optional
96
+ keyword-only `loop`.
97
+ - Targeted refreshes observe only the targeted resources.
98
+ - `AsyncScheduler` runs on a fixed interval grid, skips missed slots, never overlaps
99
+ runs, and keeps running after a callback fails.
100
+
101
+ ### Removed
102
+
103
+ - The pipeline internals `ChangeDetector`, `RefreshPlanner`, `ImpactAnalyzer`,
104
+ `DependencyGraph`, `Change`, `ChangeSet`, `ChangeType`, and `ImpactSet`.
105
+ - `EventTrigger` and `QueryTrigger`; create a `RefreshRequest` or call `refresh()`
106
+ directly.
107
+ - `AsyncSchedulerABC`.
108
+ - The exceptions `ChangeDetectionError`, `PlanningError`, `ExecutionError`,
109
+ `CancellationError`, and `DependencyError`, which were never raised.
110
+ - `FailurePolicy.RETRY_THEN_CONTINUE`, which behaved exactly like `BEST_EFFORT`;
111
+ configure retries with `RetryConfig`.
112
+ - `RefreshRequest.priority`, `RefreshRequest.deduplication_key`,
113
+ `RefreshRequest.merge()`, `Resource.source`, `RefreshResult.skipped()`, and
114
+ `canonical_bytes()`.
115
+
116
+ ### Fixed
117
+
118
+ - Dependencies declared only by snapshots were dropped after a cheap-indicator
119
+ match, which caused a needless refresh and lost the dependency.
120
+ - A dependency-aware refresh after an incomplete discovery called operations
121
+ without a resource for undiscovered dependents.
122
+ - A failed snapshot of an impacted resource failed the whole refresh and left tasks
123
+ running.
124
+ - A failing scheduler callback stopped the schedule permanently and was re-raised
125
+ by `stop()`, and scheduler wake-ups ran on a worker thread.
126
+ - A refresh whose only change failed reported `PARTIAL`, while a single snapshot
127
+ failure among unchanged resources reported `FAILED`; failed snapshots were counted
128
+ as unchanged.
129
+ - `CANCEL_PREVIOUS` raised `asyncio.CancelledError` in callers that were not
130
+ cancelled.
131
+ - A failed commit discarded the execution results.
132
+ - `FAIL_FAST` kept running the operations of the current level after a failure, and
133
+ committed state after snapshot failures.
134
+ - Dependents of a failed operation ran against stale input.
135
+ - Every unchanged resource was written to the state store on every refresh.
136
+ - A custom store, fingerprint strategy, or event publisher that evaluated as false
137
+ was replaced with the default.
138
+ - Fingerprinting raised `TypeError` for enumerations with non-JSON values, hashed
139
+ `bytearray` values element by element, and ignored dataclass types.
140
+ - `TagSelector` split a string tag into characters.
141
+ - `StructlogEventHandler` failed for an event attribute named `event`.
142
+ - A queued request whose callers had all cancelled still ran, and the publisher's
143
+ error list grew without bound.
144
+ - Lifecycle events did not carry the request's correlation ID.
145
+
146
+ ### Performance
147
+
148
+ - A full refresh of 100,000 resources takes 3.1 s instead of 9.5 s and peaks at
149
+ 114 MB instead of 513 MB. Incremental refreshes are about twice as fast.
150
+ - Events are not built when nobody subscribes, and the publisher delivers them from
151
+ one background task instead of one task and one thread hop per event.
152
+ - Unchanged resources are no longer written to the state store, and only snapshots
153
+ of resources that will receive an operation stay in memory.
154
+
155
+ ### Upgrading from 0.1.0
156
+
157
+ 1. Import every name from `refresh_engine`.
158
+ 2. Update operations to the positional signature
159
+ `(resource, snapshot, action, request, /)`; `resource` is never `None`.
160
+ 3. Replace `EventTrigger` and `QueryTrigger` with `RefreshRequest` or `refresh()`,
161
+ and `FailurePolicy.RETRY_THEN_CONTINUE` with `BEST_EFFORT` plus a `RetryConfig`.
162
+ 4. Rename `TimeoutConfig(resource_refresh=...)` to `TimeoutConfig(operation=...)`,
163
+ and move shutdown cancellation to `close(cancel=True)` and `stop(cancel=True)`.
164
+ 5. Pass model fields after the identifier by keyword, for example
165
+ `Resource("a", metadata={...})`.
166
+ 6. Port custom state transactions to `StateTransactionABC.apply_changes()`, or keep
167
+ implementing the `StateTransaction` protocol.
168
+ 7. Expect the first refresh after the upgrade to refresh every resource once.
169
+
170
+ ## [0.1.0] - 2026-09-24
171
+
172
+ ### Added
173
+
174
+ - Initial release: asyncio-first discovery, fingerprinting, change detection,
175
+ dependency-aware planning, bounded execution with retries and timeouts,
176
+ transactional state, lifecycle events, structured logging, and scheduling.
177
+
178
+ [Unreleased]: https://github.com/smuniharish/refresh-engine/compare/v0.2.0...HEAD
179
+ [0.2.0]: https://pypi.org/project/refresh-engine/0.2.0/
180
+ [0.1.0]: https://pypi.org/project/refresh-engine/0.1.0/
@@ -0,0 +1,199 @@
1
+ Metadata-Version: 2.5
2
+ Name: refresh-engine
3
+ Version: 0.2.0
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/
10
+ Author: S. Muni Harish
11
+ License-Expression: Apache-2.0
12
+ License-File: LICENSE
13
+ Keywords: asyncio,change-detection,dependency-graph,fingerprint,incremental,orchestration,refresh,scheduler,synchronization
14
+ Classifier: Development Status :: 5 - Production/Stable
15
+ Classifier: Framework :: AsyncIO
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.12
26
+ Requires-Dist: structlog>=26.1.0
27
+ Requires-Dist: tenacity>=9.1.4
28
+ Description-Content-Type: text/markdown
29
+
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).
@@ -0,0 +1,170 @@
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, a
37
+ thread-safe bridge for external schedulers, and policies that coalesce, queue,
38
+ skip, or supersede overlapping requests.
39
+ - **Observability.** Typed lifecycle events with correlation IDs, structured
40
+ logging through [structlog](https://www.structlog.org) with secret redaction,
41
+ and metrics.
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
+ ## Quick start
54
+
55
+ Implement a source that lists and reads resources, and an operation that applies
56
+ each change. The engine does the rest:
57
+
58
+ <!-- test -->
59
+ ```python
60
+ import asyncio
61
+
62
+ from refresh_engine import (
63
+ DiscoveryResult,
64
+ PlanAction,
65
+ RefreshEngine,
66
+ RefreshRequest,
67
+ Resource,
68
+ ResourceSnapshot,
69
+ )
70
+
71
+ CATALOG = {"apples": 120, "pears": 80}
72
+ SEARCH_INDEX: dict[str, int] = {}
73
+
74
+
75
+ class InventorySource:
76
+ async def discover(self) -> DiscoveryResult:
77
+ return DiscoveryResult(Resource(item) for item in sorted(CATALOG))
78
+
79
+ async def snapshot(self, resource: Resource, /) -> ResourceSnapshot:
80
+ return ResourceSnapshot(
81
+ resource.resource_id, content=CATALOG[resource.resource_id]
82
+ )
83
+
84
+
85
+ async def update_index(
86
+ resource: Resource,
87
+ snapshot: ResourceSnapshot | None,
88
+ action: PlanAction,
89
+ request: RefreshRequest,
90
+ /,
91
+ ) -> None:
92
+ if action is PlanAction.DELETE:
93
+ SEARCH_INDEX.pop(resource.resource_id, None)
94
+ elif snapshot is not None:
95
+ SEARCH_INDEX[resource.resource_id] = snapshot.content
96
+
97
+
98
+ async def main() -> None:
99
+ async with RefreshEngine(InventorySource(), update_index) as engine:
100
+ first = await engine.refresh()
101
+ print(f"{first.status}: refreshed {first.refreshed_count}", SEARCH_INDEX)
102
+
103
+ CATALOG["pears"] = 75
104
+ del CATALOG["apples"]
105
+ second = await engine.refresh()
106
+ print(
107
+ f"{second.status}: modified {second.modified_count},"
108
+ f" deleted {second.deleted_count}",
109
+ SEARCH_INDEX,
110
+ )
111
+
112
+
113
+ asyncio.run(main())
114
+ ```
115
+
116
+ ```text
117
+ success: refreshed 2 {'apples': 120, 'pears': 80}
118
+ success: modified 1, deleted 1 {'pears': 75}
119
+ ```
120
+
121
+ The second refresh touches only the modified and the deleted resource. Continue
122
+ with the [getting started guide](https://refresh-engine.readthedocs.io/en/latest/getting-started/)
123
+ to add durable state, dependencies, scheduling, and logging.
124
+
125
+ ## Performance
126
+
127
+ The engine's overhead is small next to real I/O: with in-memory sources, a full
128
+ refresh of 100,000 resources takes about 3 seconds, and an incremental refresh
129
+ that finds nothing changed takes about half a second. See the
130
+ [benchmarks](https://github.com/smuniharish/refresh-engine/blob/HEAD/benchmarks/README.md)
131
+ for the method and details.
132
+
133
+ ## Documentation
134
+
135
+ The [documentation](https://refresh-engine.readthedocs.io/en/latest/) covers:
136
+
137
+ - [Getting started](https://refresh-engine.readthedocs.io/en/latest/getting-started/)
138
+ and [concepts](https://refresh-engine.readthedocs.io/en/latest/concepts/)
139
+ - [Guides](https://refresh-engine.readthedocs.io/en/latest/guides/) for every
140
+ feature, including a
141
+ [production checklist](https://refresh-engine.readthedocs.io/en/latest/guides/production/)
142
+ - [Runnable examples](https://refresh-engine.readthedocs.io/en/latest/examples/),
143
+ including SQLite and PostgreSQL state stores
144
+ - The [API reference](https://refresh-engine.readthedocs.io/en/latest/api/)
145
+ - The [architecture](https://refresh-engine.readthedocs.io/en/latest/architecture/overview/)
146
+ and its guarantees
147
+
148
+ ## Agent Skills
149
+
150
+ Coding agents such as Claude Code, Codex, Cursor, and GitHub Copilot can use the
151
+ refresh-engine [Agent Skill](https://refresh-engine.readthedocs.io/en/latest/agent-skills/)
152
+ to integrate the library correctly:
153
+
154
+ ```bash
155
+ npx skills add https://github.com/smuniharish/refresh-engine/tree/master/refresh-engine-skills/skills/refresh-engine
156
+ ```
157
+
158
+ ## Contributing
159
+
160
+ Contributions are welcome. Read the
161
+ [contributing guide](https://github.com/smuniharish/refresh-engine/blob/HEAD/CONTRIBUTING.md),
162
+ and report vulnerabilities as described in the
163
+ [security policy](https://github.com/smuniharish/refresh-engine/blob/HEAD/SECURITY.md).
164
+ Notable changes are listed in the
165
+ [changelog](https://github.com/smuniharish/refresh-engine/blob/HEAD/CHANGELOG.md).
166
+
167
+ ## License
168
+
169
+ refresh-engine is licensed under the
170
+ [Apache License 2.0](https://github.com/smuniharish/refresh-engine/blob/HEAD/LICENSE).