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.
- refresh_engine-0.2.0/CHANGELOG.md +180 -0
- refresh_engine-0.2.0/PKG-INFO +199 -0
- refresh_engine-0.2.0/README.md +170 -0
- refresh_engine-0.2.0/pyproject.toml +173 -0
- {refresh_engine-0.1.0 → refresh_engine-0.2.0}/src/refresh_engine/__init__.py +51 -64
- refresh_engine-0.2.0/src/refresh_engine/_internal/__init__.py +5 -0
- refresh_engine-0.2.0/src/refresh_engine/_internal/concurrency.py +58 -0
- refresh_engine-0.2.0/src/refresh_engine/_internal/context.py +334 -0
- refresh_engine-0.2.0/src/refresh_engine/_internal/coordinator.py +238 -0
- refresh_engine-0.2.0/src/refresh_engine/_internal/execution.py +332 -0
- refresh_engine-0.2.0/src/refresh_engine/_internal/observation.py +174 -0
- refresh_engine-0.2.0/src/refresh_engine/_internal/pipeline.py +402 -0
- refresh_engine-0.2.0/src/refresh_engine/_internal/planning.py +236 -0
- refresh_engine-0.2.0/src/refresh_engine/_internal/reentrancy.py +36 -0
- refresh_engine-0.2.0/src/refresh_engine/_internal/values.py +97 -0
- refresh_engine-0.2.0/src/refresh_engine/contracts.py +499 -0
- refresh_engine-0.2.0/src/refresh_engine/engine.py +253 -0
- refresh_engine-0.2.0/src/refresh_engine/errors.py +85 -0
- refresh_engine-0.2.0/src/refresh_engine/events.py +322 -0
- refresh_engine-0.2.0/src/refresh_engine/fingerprints.py +356 -0
- refresh_engine-0.2.0/src/refresh_engine/models.py +595 -0
- refresh_engine-0.2.0/src/refresh_engine/observability.py +315 -0
- refresh_engine-0.2.0/src/refresh_engine/scheduling.py +368 -0
- refresh_engine-0.2.0/src/refresh_engine/selectors.py +123 -0
- refresh_engine-0.2.0/src/refresh_engine/stores.py +62 -0
- refresh_engine-0.2.0/tests/conftest.py +17 -0
- refresh_engine-0.2.0/tests/integration/__init__.py +0 -0
- refresh_engine-0.2.0/tests/integration/test_coordination.py +413 -0
- refresh_engine-0.2.0/tests/integration/test_events.py +219 -0
- refresh_engine-0.2.0/tests/integration/test_failures.py +625 -0
- refresh_engine-0.2.0/tests/integration/test_reentrancy.py +396 -0
- refresh_engine-0.2.0/tests/integration/test_refresh_modes.py +436 -0
- refresh_engine-0.2.0/tests/property/__init__.py +0 -0
- refresh_engine-0.2.0/tests/property/test_fingerprint_properties.py +111 -0
- refresh_engine-0.2.0/tests/property/test_planning_properties.py +258 -0
- refresh_engine-0.2.0/tests/property/test_redactor_properties.py +75 -0
- refresh_engine-0.2.0/tests/property/test_selector_properties.py +107 -0
- refresh_engine-0.2.0/tests/property/test_state_machines.py +424 -0
- refresh_engine-0.2.0/tests/support.py +172 -0
- refresh_engine-0.2.0/tests/unit/__init__.py +0 -0
- refresh_engine-0.2.0/tests/unit/test_concurrency.py +55 -0
- refresh_engine-0.2.0/tests/unit/test_contracts.py +722 -0
- refresh_engine-0.2.0/tests/unit/test_coordinator.py +218 -0
- refresh_engine-0.2.0/tests/unit/test_engine.py +115 -0
- refresh_engine-0.2.0/tests/unit/test_errors.py +103 -0
- refresh_engine-0.2.0/tests/unit/test_events.py +615 -0
- refresh_engine-0.2.0/tests/unit/test_fingerprints.py +625 -0
- refresh_engine-0.2.0/tests/unit/test_models.py +923 -0
- refresh_engine-0.2.0/tests/unit/test_observability.py +720 -0
- refresh_engine-0.2.0/tests/unit/test_planning.py +197 -0
- refresh_engine-0.2.0/tests/unit/test_public_api.py +63 -0
- refresh_engine-0.2.0/tests/unit/test_reentrancy.py +36 -0
- refresh_engine-0.2.0/tests/unit/test_scheduling.py +926 -0
- refresh_engine-0.2.0/tests/unit/test_selectors.py +309 -0
- refresh_engine-0.2.0/tests/unit/test_stores.py +172 -0
- refresh_engine-0.2.0/tests/unit/test_values.py +239 -0
- refresh_engine-0.1.0/.github/dependabot.yml +0 -12
- refresh_engine-0.1.0/.github/workflows/ci.yml +0 -49
- refresh_engine-0.1.0/.github/workflows/release.yml +0 -61
- refresh_engine-0.1.0/.github/workflows/security.yml +0 -26
- refresh_engine-0.1.0/.readthedocs.yaml +0 -16
- refresh_engine-0.1.0/CHANGELOG.md +0 -33
- refresh_engine-0.1.0/CONTRIBUTING.md +0 -43
- refresh_engine-0.1.0/PKG-INFO +0 -95
- refresh_engine-0.1.0/README.md +0 -76
- refresh_engine-0.1.0/SECURITY.md +0 -26
- refresh_engine-0.1.0/benchmarks/README.md +0 -27
- refresh_engine-0.1.0/benchmarks/benchmark_refresh.py +0 -166
- refresh_engine-0.1.0/benchmarks/results/windows-python-3.12-2026-09-24.json +0 -354
- refresh_engine-0.1.0/docs/api/contracts.md +0 -48
- refresh_engine-0.1.0/docs/api/engine.md +0 -11
- refresh_engine-0.1.0/docs/api/index.md +0 -16
- refresh_engine-0.1.0/docs/api/models.md +0 -31
- refresh_engine-0.1.0/docs/api/services.md +0 -32
- refresh_engine-0.1.0/docs/api/strategies.md +0 -24
- refresh_engine-0.1.0/docs/architecture/decisions.md +0 -140
- refresh_engine-0.1.0/docs/architecture/lifecycle.md +0 -25
- refresh_engine-0.1.0/docs/architecture/overview.md +0 -62
- refresh_engine-0.1.0/docs/getting-started.md +0 -62
- refresh_engine-0.1.0/docs/guides/events-and-observability.md +0 -35
- refresh_engine-0.1.0/docs/guides/examples-and-results.md +0 -320
- refresh_engine-0.1.0/docs/guides/fingerprints-and-dependencies.md +0 -30
- refresh_engine-0.1.0/docs/guides/index.md +0 -15
- refresh_engine-0.1.0/docs/guides/refresh-modes.md +0 -33
- refresh_engine-0.1.0/docs/guides/sources-and-operations.md +0 -38
- refresh_engine-0.1.0/docs/guides/state-and-failure-safety.md +0 -76
- refresh_engine-0.1.0/docs/guides/triggers-and-scheduling.md +0 -35
- refresh_engine-0.1.0/docs/index.md +0 -25
- refresh_engine-0.1.0/examples/01_basic_and_modes.py +0 -37
- refresh_engine-0.1.0/examples/02_dependencies_and_fingerprints.py +0 -30
- refresh_engine-0.1.0/examples/03_triggers_and_selectors.py +0 -45
- refresh_engine-0.1.0/examples/04_async_and_external_scheduling.py +0 -37
- refresh_engine-0.1.0/examples/06_resilience_and_partial_discovery.py +0 -45
- refresh_engine-0.1.0/examples/07_events_and_observability.py +0 -42
- refresh_engine-0.1.0/examples/08_custom_store_and_overlap.py +0 -47
- refresh_engine-0.1.0/examples/09_filesystem_resources.py +0 -76
- refresh_engine-0.1.0/examples/10_scale_and_genericity.py +0 -51
- refresh_engine-0.1.0/examples/11_cancellation_and_shutdown.py +0 -46
- refresh_engine-0.1.0/examples/12_sqlite_state_store.py +0 -38
- refresh_engine-0.1.0/examples/13_abc_extensions.py +0 -50
- refresh_engine-0.1.0/examples/14_postgres_state_store.py +0 -57
- refresh_engine-0.1.0/examples/README.md +0 -37
- refresh_engine-0.1.0/examples/common.py +0 -46
- refresh_engine-0.1.0/examples/postgres_store.py +0 -148
- refresh_engine-0.1.0/examples/sqlite_store.py +0 -260
- refresh_engine-0.1.0/mkdocs.yml +0 -59
- refresh_engine-0.1.0/pyproject.toml +0 -83
- refresh_engine-0.1.0/scripts/check_docs_examples.py +0 -18
- refresh_engine-0.1.0/scripts/run_examples.py +0 -22
- refresh_engine-0.1.0/scripts/validate_distribution.py +0 -169
- refresh_engine-0.1.0/src/refresh_engine/api/__init__.py +0 -43
- refresh_engine-0.1.0/src/refresh_engine/api/abc.py +0 -176
- refresh_engine-0.1.0/src/refresh_engine/api/engine.py +0 -468
- refresh_engine-0.1.0/src/refresh_engine/api/protocols.py +0 -89
- refresh_engine-0.1.0/src/refresh_engine/core/models.py +0 -362
- refresh_engine-0.1.0/src/refresh_engine/detection/__init__.py +0 -21
- refresh_engine-0.1.0/src/refresh_engine/detection/detector.py +0 -54
- refresh_engine-0.1.0/src/refresh_engine/detection/fingerprints.py +0 -115
- refresh_engine-0.1.0/src/refresh_engine/errors.py +0 -57
- refresh_engine-0.1.0/src/refresh_engine/events/__init__.py +0 -6
- refresh_engine-0.1.0/src/refresh_engine/events/models.py +0 -42
- refresh_engine-0.1.0/src/refresh_engine/events/publisher.py +0 -77
- refresh_engine-0.1.0/src/refresh_engine/execution/__init__.py +0 -5
- refresh_engine-0.1.0/src/refresh_engine/execution/coordinator.py +0 -128
- refresh_engine-0.1.0/src/refresh_engine/execution/executor.py +0 -180
- refresh_engine-0.1.0/src/refresh_engine/observability/__init__.py +0 -9
- refresh_engine-0.1.0/src/refresh_engine/observability/logging.py +0 -49
- refresh_engine-0.1.0/src/refresh_engine/observability/metrics.py +0 -44
- refresh_engine-0.1.0/src/refresh_engine/planning/__init__.py +0 -6
- refresh_engine-0.1.0/src/refresh_engine/planning/dependency.py +0 -79
- refresh_engine-0.1.0/src/refresh_engine/planning/planner.py +0 -86
- refresh_engine-0.1.0/src/refresh_engine/scheduling/__init__.py +0 -13
- refresh_engine-0.1.0/src/refresh_engine/scheduling/async_scheduler.py +0 -126
- refresh_engine-0.1.0/src/refresh_engine/scheduling/external.py +0 -17
- refresh_engine-0.1.0/src/refresh_engine/stores/__init__.py +0 -11
- refresh_engine-0.1.0/src/refresh_engine/stores/memory.py +0 -77
- refresh_engine-0.1.0/src/refresh_engine/triggers/__init__.py +0 -16
- refresh_engine-0.1.0/src/refresh_engine/triggers/adapters.py +0 -38
- refresh_engine-0.1.0/src/refresh_engine/triggers/selectors.py +0 -46
- refresh_engine-0.1.0/tests/concurrency/test_execution.py +0 -134
- refresh_engine-0.1.0/tests/conftest.py +0 -1
- refresh_engine-0.1.0/tests/helpers.py +0 -77
- refresh_engine-0.1.0/tests/integration/test_incremental.py +0 -111
- refresh_engine-0.1.0/tests/integration/test_safety.py +0 -48
- refresh_engine-0.1.0/tests/scheduler/test_scheduler.py +0 -70
- refresh_engine-0.1.0/tests/unit/test_abstract_bases.py +0 -84
- refresh_engine-0.1.0/tests/unit/test_dependency.py +0 -55
- refresh_engine-0.1.0/tests/unit/test_events_store_selectors.py +0 -107
- refresh_engine-0.1.0/tests/unit/test_fingerprints.py +0 -91
- refresh_engine-0.1.0/tests/unit/test_public_api.py +0 -19
- refresh_engine-0.1.0/tests/unit/test_triggers_models.py +0 -49
- refresh_engine-0.1.0/uv.lock +0 -1138
- {refresh_engine-0.1.0 → refresh_engine-0.2.0}/.gitignore +0 -0
- {refresh_engine-0.1.0 → refresh_engine-0.2.0}/LICENSE +0 -0
- {refresh_engine-0.1.0 → refresh_engine-0.2.0}/src/refresh_engine/py.typed +0 -0
- {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
|
+
[](https://pypi.org/project/refresh-engine/)
|
|
33
|
+
[](https://pypi.org/project/refresh-engine/)
|
|
34
|
+
[](https://github.com/smuniharish/refresh-engine/actions/workflows/ci.yml)
|
|
35
|
+
[](https://refresh-engine.readthedocs.io/en/latest/)
|
|
36
|
+
[](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
|
+
[](https://pypi.org/project/refresh-engine/)
|
|
4
|
+
[](https://pypi.org/project/refresh-engine/)
|
|
5
|
+
[](https://github.com/smuniharish/refresh-engine/actions/workflows/ci.yml)
|
|
6
|
+
[](https://refresh-engine.readthedocs.io/en/latest/)
|
|
7
|
+
[](https://github.com/smuniharish/refresh-engine/blob/HEAD/LICENSE)
|
|
8
|
+
|
|
9
|
+
**Keep anything in sync with its source by refreshing only what changed.**
|
|
10
|
+
|
|
11
|
+
refresh-engine is an asyncio-first, domain-neutral Python library for keeping
|
|
12
|
+
derived data current: search indexes, caches, embeddings, configuration,
|
|
13
|
+
registries, or anything else built from a source. You describe how to list and
|
|
14
|
+
read resources and how to apply a change; the engine detects what changed,
|
|
15
|
+
plans the work in dependency order, runs it safely, and remembers the outcome.
|
|
16
|
+
|
|
17
|
+
<picture>
|
|
18
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/smuniharish/refresh-engine/HEAD/docs/assets/diagrams/pipeline-dark.png">
|
|
19
|
+
<img alt="Discover, observe, detect changes, plan, execute, and persist" src="https://raw.githubusercontent.com/smuniharish/refresh-engine/HEAD/docs/assets/diagrams/pipeline-light.png">
|
|
20
|
+
</picture>
|
|
21
|
+
|
|
22
|
+
## Features
|
|
23
|
+
|
|
24
|
+
- **Precise change detection.** Deterministic fingerprints detect added,
|
|
25
|
+
modified, and deleted resources. Reliable cheap indicators, such as ETags or
|
|
26
|
+
version tokens, skip reading unchanged resources entirely.
|
|
27
|
+
- **Four refresh modes.** Incremental, full, dependency-aware, and targeted
|
|
28
|
+
refreshes, with targets chosen by ID or by composable selectors.
|
|
29
|
+
- **Dependency ordering.** Dependencies are refreshed before their dependents,
|
|
30
|
+
changes propagate to everything that depends on them, and cycles are isolated.
|
|
31
|
+
- **Safe by default.** Bounded concurrency, retries with jittered exponential
|
|
32
|
+
backoff, timeouts, and fail-fast or best-effort policies. A partial discovery
|
|
33
|
+
never deletes anything, and failed work is retried by the next refresh.
|
|
34
|
+
- **Durable state.** State advances only for successful resources, in one atomic
|
|
35
|
+
commit per refresh. Use the in-memory store or plug in your own database.
|
|
36
|
+
- **Scheduling and overlap control.** A drift-free interval scheduler, 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).
|