fleetpull 0.1.0__py3-none-any.whl
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.
- fleetpull/__init__.py +36 -0
- fleetpull/api/__init__.py +32 -0
- fleetpull/api/auth_ingress.py +216 -0
- fleetpull/api/catalog.py +139 -0
- fleetpull/api/fetch.py +229 -0
- fleetpull/api/identity.py +90 -0
- fleetpull/api/sync.py +735 -0
- fleetpull/cli.py +146 -0
- fleetpull/config/__init__.py +47 -0
- fleetpull/config/base.py +18 -0
- fleetpull/config/example.py +90 -0
- fleetpull/config/geotab.py +78 -0
- fleetpull/config/http.py +32 -0
- fleetpull/config/loading.py +195 -0
- fleetpull/config/logger.py +139 -0
- fleetpull/config/providers.py +520 -0
- fleetpull/config/rate_limit.py +50 -0
- fleetpull/config/resolution.py +156 -0
- fleetpull/config/retry.py +69 -0
- fleetpull/config/root.py +157 -0
- fleetpull/config/sections.py +146 -0
- fleetpull/endpoints/__init__.py +24 -0
- fleetpull/endpoints/geotab/__init__.py +12 -0
- fleetpull/endpoints/geotab/_requests.py +427 -0
- fleetpull/endpoints/geotab/annotation_logs.py +73 -0
- fleetpull/endpoints/geotab/audits.py +70 -0
- fleetpull/endpoints/geotab/devices.py +79 -0
- fleetpull/endpoints/geotab/driver_changes.py +73 -0
- fleetpull/endpoints/geotab/duty_status_logs.py +75 -0
- fleetpull/endpoints/geotab/dvir_logs.py +76 -0
- fleetpull/endpoints/geotab/exception_events.py +119 -0
- fleetpull/endpoints/geotab/fault_data.py +72 -0
- fleetpull/endpoints/geotab/fill_ups.py +79 -0
- fleetpull/endpoints/geotab/fuel_and_energy_used.py +74 -0
- fleetpull/endpoints/geotab/fuel_tax_details.py +75 -0
- fleetpull/endpoints/geotab/log_records.py +74 -0
- fleetpull/endpoints/geotab/media_files.py +74 -0
- fleetpull/endpoints/geotab/shipment_logs.py +71 -0
- fleetpull/endpoints/geotab/status_data.py +71 -0
- fleetpull/endpoints/geotab/text_messages.py +77 -0
- fleetpull/endpoints/geotab/trips.py +102 -0
- fleetpull/endpoints/geotab/users.py +81 -0
- fleetpull/endpoints/motive/__init__.py +12 -0
- fleetpull/endpoints/motive/_spec_builders.py +94 -0
- fleetpull/endpoints/motive/driver_idle_rollups.py +113 -0
- fleetpull/endpoints/motive/driving_periods.py +85 -0
- fleetpull/endpoints/motive/groups.py +61 -0
- fleetpull/endpoints/motive/idle_events.py +94 -0
- fleetpull/endpoints/motive/users.py +64 -0
- fleetpull/endpoints/motive/vehicle_locations.py +170 -0
- fleetpull/endpoints/motive/vehicle_utilizations.py +122 -0
- fleetpull/endpoints/motive/vehicles.py +106 -0
- fleetpull/endpoints/registry.py +280 -0
- fleetpull/endpoints/samsara/__init__.py +12 -0
- fleetpull/endpoints/samsara/_spec_builders.py +202 -0
- fleetpull/endpoints/samsara/addresses.py +77 -0
- fleetpull/endpoints/samsara/asset_locations.py +217 -0
- fleetpull/endpoints/samsara/driver_fuel_energy_reports.py +124 -0
- fleetpull/endpoints/samsara/driver_vehicle_assignments.py +211 -0
- fleetpull/endpoints/samsara/drivers.py +169 -0
- fleetpull/endpoints/samsara/engine_states.py +114 -0
- fleetpull/endpoints/samsara/gps_readings.py +113 -0
- fleetpull/endpoints/samsara/idling_events.py +195 -0
- fleetpull/endpoints/samsara/odometer_readings.py +116 -0
- fleetpull/endpoints/samsara/trips.py +217 -0
- fleetpull/endpoints/samsara/vehicle_fuel_energy_reports.py +139 -0
- fleetpull/endpoints/samsara/vehicles.py +121 -0
- fleetpull/endpoints/shared/__init__.py +57 -0
- fleetpull/endpoints/shared/base.py +529 -0
- fleetpull/endpoints/shared/request_shape.py +227 -0
- fleetpull/endpoints/shared/resume.py +74 -0
- fleetpull/endpoints/shared/spec_builders.py +59 -0
- fleetpull/endpoints/shared/sync_mode.py +159 -0
- fleetpull/endpoints/shared/url_paths.py +149 -0
- fleetpull/exceptions.py +320 -0
- fleetpull/incremental/__init__.py +27 -0
- fleetpull/incremental/cursor.py +66 -0
- fleetpull/incremental/resolution.py +152 -0
- fleetpull/incremental/seed.py +53 -0
- fleetpull/incremental/window.py +94 -0
- fleetpull/logger/__init__.py +5 -0
- fleetpull/logger/setup.py +145 -0
- fleetpull/model_contract/__init__.py +6 -0
- fleetpull/model_contract/coercions.py +33 -0
- fleetpull/model_contract/response.py +45 -0
- fleetpull/models/__init__.py +7 -0
- fleetpull/models/geotab/__init__.py +166 -0
- fleetpull/models/geotab/annotation_log.py +109 -0
- fleetpull/models/geotab/audit.py +56 -0
- fleetpull/models/geotab/device.py +217 -0
- fleetpull/models/geotab/driver_change.py +102 -0
- fleetpull/models/geotab/duty_status_log.py +200 -0
- fleetpull/models/geotab/dvir_log.py +184 -0
- fleetpull/models/geotab/exception_event.py +135 -0
- fleetpull/models/geotab/fault_data.py +168 -0
- fleetpull/models/geotab/fill_up.py +189 -0
- fleetpull/models/geotab/fuel_and_energy_used.py +81 -0
- fleetpull/models/geotab/fuel_tax_detail.py +150 -0
- fleetpull/models/geotab/log_record.py +68 -0
- fleetpull/models/geotab/media_file.py +133 -0
- fleetpull/models/geotab/shared.py +221 -0
- fleetpull/models/geotab/shipment_log.py +105 -0
- fleetpull/models/geotab/status_data.py +108 -0
- fleetpull/models/geotab/text_message.py +125 -0
- fleetpull/models/geotab/trip.py +162 -0
- fleetpull/models/geotab/user.py +187 -0
- fleetpull/models/motive/__init__.py +43 -0
- fleetpull/models/motive/driver_idle_rollup.py +109 -0
- fleetpull/models/motive/driving_period.py +82 -0
- fleetpull/models/motive/group.py +49 -0
- fleetpull/models/motive/idle_event.py +77 -0
- fleetpull/models/motive/shared.py +192 -0
- fleetpull/models/motive/user.py +217 -0
- fleetpull/models/motive/vehicle.py +162 -0
- fleetpull/models/motive/vehicle_location.py +127 -0
- fleetpull/models/motive/vehicle_utilization.py +128 -0
- fleetpull/models/samsara/__init__.py +108 -0
- fleetpull/models/samsara/address.py +149 -0
- fleetpull/models/samsara/asset_location.py +131 -0
- fleetpull/models/samsara/driver.py +232 -0
- fleetpull/models/samsara/driver_fuel_energy_report.py +153 -0
- fleetpull/models/samsara/driver_vehicle_assignment.py +168 -0
- fleetpull/models/samsara/engine_state.py +92 -0
- fleetpull/models/samsara/gps_reading.py +146 -0
- fleetpull/models/samsara/idling_event.py +174 -0
- fleetpull/models/samsara/odometer_reading.py +90 -0
- fleetpull/models/samsara/trip.py +199 -0
- fleetpull/models/samsara/vehicle.py +167 -0
- fleetpull/models/samsara/vehicle_fuel_energy_report.py +191 -0
- fleetpull/network/__init__.py +9 -0
- fleetpull/network/auth/__init__.py +15 -0
- fleetpull/network/auth/authenticate.py +271 -0
- fleetpull/network/auth/manager.py +223 -0
- fleetpull/network/auth/models.py +57 -0
- fleetpull/network/auth/strategies.py +169 -0
- fleetpull/network/classifiers/__init__.py +11 -0
- fleetpull/network/classifiers/geotab.py +145 -0
- fleetpull/network/classifiers/motive.py +79 -0
- fleetpull/network/classifiers/samsara.py +80 -0
- fleetpull/network/client/__init__.py +25 -0
- fleetpull/network/client/page.py +32 -0
- fleetpull/network/client/profile.py +27 -0
- fleetpull/network/client/registry.py +103 -0
- fleetpull/network/client/registry_base.py +150 -0
- fleetpull/network/client/runtime.py +44 -0
- fleetpull/network/client/transport.py +381 -0
- fleetpull/network/contract/__init__.py +50 -0
- fleetpull/network/contract/auth.py +48 -0
- fleetpull/network/contract/classifier.py +189 -0
- fleetpull/network/contract/envelope_fetcher.py +33 -0
- fleetpull/network/contract/envelopes.py +189 -0
- fleetpull/network/contract/outcome.py +43 -0
- fleetpull/network/contract/page_decoder.py +101 -0
- fleetpull/network/contract/request.py +97 -0
- fleetpull/network/decoders/__init__.py +40 -0
- fleetpull/network/decoders/_window_stamp.py +78 -0
- fleetpull/network/decoders/geotab.py +309 -0
- fleetpull/network/decoders/motive.py +204 -0
- fleetpull/network/decoders/motive_reports.py +114 -0
- fleetpull/network/decoders/samsara.py +343 -0
- fleetpull/network/decoders/samsara_reports.py +119 -0
- fleetpull/network/decoders/single_page.py +55 -0
- fleetpull/network/limits/__init__.py +13 -0
- fleetpull/network/limits/bucket_math.py +58 -0
- fleetpull/network/limits/limiter.py +153 -0
- fleetpull/network/limits/registry.py +96 -0
- fleetpull/network/posture/__init__.py +6 -0
- fleetpull/network/posture/client_options.py +96 -0
- fleetpull/network/retry/__init__.py +13 -0
- fleetpull/network/retry/decision.py +155 -0
- fleetpull/network/tls/__init__.py +5 -0
- fleetpull/network/tls/truststore_context.py +35 -0
- fleetpull/orchestrator/__init__.py +30 -0
- fleetpull/orchestrator/backfill.py +127 -0
- fleetpull/orchestrator/batch.py +155 -0
- fleetpull/orchestrator/bisection.py +271 -0
- fleetpull/orchestrator/drivers.py +350 -0
- fleetpull/orchestrator/entry.py +347 -0
- fleetpull/orchestrator/executors.py +128 -0
- fleetpull/orchestrator/fanout.py +163 -0
- fleetpull/orchestrator/feed_drive.py +266 -0
- fleetpull/orchestrator/metadata_projection.py +195 -0
- fleetpull/orchestrator/outcome.py +53 -0
- fleetpull/orchestrator/recording.py +85 -0
- fleetpull/orchestrator/resume.py +143 -0
- fleetpull/orchestrator/roster_harvest.py +108 -0
- fleetpull/orchestrator/roster_refresh.py +363 -0
- fleetpull/orchestrator/runner.py +262 -0
- fleetpull/orchestrator/shape_resolution.py +221 -0
- fleetpull/orchestrator/spine.py +166 -0
- fleetpull/orchestrator/streaming.py +153 -0
- fleetpull/orchestrator/unit_loop.py +273 -0
- fleetpull/orchestrator/watermark_drive.py +455 -0
- fleetpull/paths/__init__.py +13 -0
- fleetpull/paths/datasets.py +38 -0
- fleetpull/paths/partitions.py +41 -0
- fleetpull/paths/resolution.py +98 -0
- fleetpull/polars_typing/__init__.py +20 -0
- fleetpull/py.typed +0 -0
- fleetpull/records/__init__.py +19 -0
- fleetpull/records/convert.py +53 -0
- fleetpull/records/dataframe.py +63 -0
- fleetpull/records/event_time.py +70 -0
- fleetpull/records/fields.py +190 -0
- fleetpull/records/flatten.py +54 -0
- fleetpull/records/roster_members.py +74 -0
- fleetpull/records/schema.py +73 -0
- fleetpull/records/validation.py +62 -0
- fleetpull/resources/__init__.py +10 -0
- fleetpull/resources/config.example.yaml +222 -0
- fleetpull/roster/__init__.py +16 -0
- fleetpull/roster/definition.py +46 -0
- fleetpull/roster/key.py +40 -0
- fleetpull/roster/registry.py +93 -0
- fleetpull/state/__init__.py +36 -0
- fleetpull/state/cursors.py +417 -0
- fleetpull/state/database.py +463 -0
- fleetpull/state/migrations.py +430 -0
- fleetpull/state/reconcile.py +127 -0
- fleetpull/state/rosters.py +159 -0
- fleetpull/state/run_ledger.py +572 -0
- fleetpull/state/work_units.py +648 -0
- fleetpull/storage/__init__.py +30 -0
- fleetpull/storage/append.py +187 -0
- fleetpull/storage/atomic.py +161 -0
- fleetpull/storage/files.py +176 -0
- fleetpull/storage/frames.py +98 -0
- fleetpull/storage/metadata.py +161 -0
- fleetpull/storage/partitioned.py +205 -0
- fleetpull/storage/pruning.py +169 -0
- fleetpull/storage/read.py +35 -0
- fleetpull/storage/result.py +34 -0
- fleetpull/storage/single_file.py +130 -0
- fleetpull/storage/splitting.py +88 -0
- fleetpull/storage/staging.py +178 -0
- fleetpull/storage/writers.py +145 -0
- fleetpull/timing/__init__.py +21 -0
- fleetpull/timing/canon.py +92 -0
- fleetpull/timing/clock.py +192 -0
- fleetpull/timing/codec.py +122 -0
- fleetpull/timing/sleeper.py +65 -0
- fleetpull/vocabulary/__init__.py +16 -0
- fleetpull/vocabulary/json_types.py +20 -0
- fleetpull/vocabulary/provider.py +29 -0
- fleetpull/vocabulary/quota_scope.py +47 -0
- fleetpull/vocabulary/response_category.py +32 -0
- fleetpull-0.1.0.dist-info/METADATA +248 -0
- fleetpull-0.1.0.dist-info/RECORD +252 -0
- fleetpull-0.1.0.dist-info/WHEEL +5 -0
- fleetpull-0.1.0.dist-info/entry_points.txt +2 -0
- fleetpull-0.1.0.dist-info/licenses/LICENSE +202 -0
- fleetpull-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# src/fleetpull/orchestrator/runner.py
|
|
2
|
+
"""The run executor: run one endpoint to completion, once per (endpoint, run).
|
|
3
|
+
|
|
4
|
+
``EndpointRunner`` owns one endpoint's run and dispatches on its ``sync_mode``.
|
|
5
|
+
The snapshot arm lives here: fetch once, full-replace, record the run. The
|
|
6
|
+
watermark arm is the ``WatermarkDrive`` (``orchestrator/watermark_drive.py``:
|
|
7
|
+
the plan-and-drive unit loop with the prefix-advance watermark rule) and the
|
|
8
|
+
feed arm is the ``FeedDrive`` (``orchestrator/feed_drive.py``: the per-page
|
|
9
|
+
parquet-before-token drive) -- both constructed in ``__init__`` from the same
|
|
10
|
+
``RunnerSpine`` (``orchestrator/spine.py``): the four collaborators plus the
|
|
11
|
+
runner-owned seams, the ONE writer-factory call site (``_writer_for``) and
|
|
12
|
+
the post-commit ``metadata.json`` projection
|
|
13
|
+
(``orchestrator/metadata_projection.py``). Every run-opening arm wraps its
|
|
14
|
+
protected block in the shared ``recorded_run`` spine
|
|
15
|
+
(``orchestrator/recording.py``), so a failure records the run failed without
|
|
16
|
+
masking the original error. Request cardinality and batch granularity are
|
|
17
|
+
the driver's; the runner is blind to both.
|
|
18
|
+
|
|
19
|
+
``run`` takes an optional ``BatchObserver``: a generic hook handed each
|
|
20
|
+
post-validation frame as the run streams. The runner knows nothing about what
|
|
21
|
+
an observer does with the frames (the caller boundary uses it to tap feeder
|
|
22
|
+
runs for roster reconciliation, but that knowledge lives entirely there) -- an
|
|
23
|
+
observer exception fails the run like any other batch-processing failure.
|
|
24
|
+
|
|
25
|
+
The runner depends on narrow Protocols rather than the concrete state and
|
|
26
|
+
network classes -- ``ClientSource``, ``RunRecorder``, ``CursorAccess``, and
|
|
27
|
+
``UnitQueue``, bundled as ``RunStateAccess`` (all on the spine module). It
|
|
28
|
+
opens no clients and reads no credentials -- the already-open client source
|
|
29
|
+
hands it the provider's client.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
import logging
|
|
33
|
+
|
|
34
|
+
from fleetpull.config import FleetpullConfig
|
|
35
|
+
from fleetpull.endpoints.shared import (
|
|
36
|
+
EndpointDefinition,
|
|
37
|
+
FeedMode,
|
|
38
|
+
SnapshotMode,
|
|
39
|
+
WatermarkMode,
|
|
40
|
+
)
|
|
41
|
+
from fleetpull.incremental import DateWindow
|
|
42
|
+
from fleetpull.model_contract import ResponseModel
|
|
43
|
+
from fleetpull.orchestrator.drivers import RequestDriver
|
|
44
|
+
from fleetpull.orchestrator.feed_drive import FeedDrive
|
|
45
|
+
from fleetpull.orchestrator.metadata_projection import (
|
|
46
|
+
MetadataProjection,
|
|
47
|
+
sync_mode_label,
|
|
48
|
+
)
|
|
49
|
+
from fleetpull.orchestrator.outcome import Executed, RunOutcome
|
|
50
|
+
from fleetpull.orchestrator.recording import recorded_run
|
|
51
|
+
from fleetpull.orchestrator.spine import ClientSource, RunnerSpine, RunStateAccess
|
|
52
|
+
from fleetpull.orchestrator.streaming import (
|
|
53
|
+
BatchObserver,
|
|
54
|
+
drain_batches,
|
|
55
|
+
observe_batches,
|
|
56
|
+
stream_processed_batches,
|
|
57
|
+
)
|
|
58
|
+
from fleetpull.orchestrator.watermark_drive import WatermarkDrive
|
|
59
|
+
from fleetpull.storage import DatasetWriter, select_writer
|
|
60
|
+
from fleetpull.timing import Clock
|
|
61
|
+
|
|
62
|
+
__all__: list[str] = ['EndpointRunner']
|
|
63
|
+
|
|
64
|
+
logger = logging.getLogger(__name__)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class EndpointRunner:
|
|
68
|
+
"""Runs one endpoint to completion, dispatching on its sync mode.
|
|
69
|
+
|
|
70
|
+
Constructed once with its four collaborators (client source, the bundled
|
|
71
|
+
state surfaces, clock, the root config); ``run`` takes the endpoint and
|
|
72
|
+
its request driver, so one instance runs every endpoint. The snapshot
|
|
73
|
+
arm lives on the class; the watermark and feed arms are the two drive
|
|
74
|
+
classes built in ``__init__`` from the shared spine.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
client_source: ClientSource,
|
|
80
|
+
state: RunStateAccess,
|
|
81
|
+
clock: Clock,
|
|
82
|
+
config: FleetpullConfig,
|
|
83
|
+
) -> None:
|
|
84
|
+
"""
|
|
85
|
+
Args:
|
|
86
|
+
client_source: Hands out an open per-provider client (the registry).
|
|
87
|
+
state: The state-database surfaces -- the ledger, the cursor
|
|
88
|
+
store, and the work-unit queue.
|
|
89
|
+
clock: Supplies the run instant (trailing edge, future-event guard).
|
|
90
|
+
config: The root config -- the container its composition root
|
|
91
|
+
already holds. The runner reads the ``sync`` section (the
|
|
92
|
+
cold-start anchor, the unit width, and the unit worker
|
|
93
|
+
count, handed to the drives on the spine) plus two storage
|
|
94
|
+
values: ``storage.dataset_root`` (where the writers land)
|
|
95
|
+
and ``storage.drop_exact_duplicates`` (the writers'
|
|
96
|
+
exact-dedup switch).
|
|
97
|
+
"""
|
|
98
|
+
self._dataset_root = config.storage.dataset_root
|
|
99
|
+
self._drop_duplicates = config.storage.drop_exact_duplicates
|
|
100
|
+
self._spine = RunnerSpine(
|
|
101
|
+
clients=client_source,
|
|
102
|
+
state=state,
|
|
103
|
+
clock=clock,
|
|
104
|
+
sync=config.sync,
|
|
105
|
+
make_writer=self._writer_for,
|
|
106
|
+
projection=MetadataProjection(
|
|
107
|
+
state.cursors, clock, config.storage.dataset_root
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
self._watermark_drive = WatermarkDrive(self._spine)
|
|
111
|
+
self._feed_drive = FeedDrive(self._spine)
|
|
112
|
+
|
|
113
|
+
def run(
|
|
114
|
+
self,
|
|
115
|
+
definition: EndpointDefinition[ResponseModel],
|
|
116
|
+
driver: RequestDriver,
|
|
117
|
+
observer: BatchObserver | None = None,
|
|
118
|
+
) -> RunOutcome:
|
|
119
|
+
"""Run one endpoint to completion and report the outcome.
|
|
120
|
+
|
|
121
|
+
Narrates at INFO: one start line at entry (provider, endpoint, sync
|
|
122
|
+
mode) and, for an ``Executed`` outcome, one completion line with the
|
|
123
|
+
merged counts and the endpoint's elapsed seconds (a monotonic delta
|
|
124
|
+
captured at entry). A ``CaughtUp`` outcome narrates through the
|
|
125
|
+
watermark arm's own 'caught up' line instead.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
definition: The endpoint to run.
|
|
129
|
+
driver: The request driver supplying the run's record batches.
|
|
130
|
+
observer: An optional generic hook handed each post-validation
|
|
131
|
+
frame as the run streams; the runner is blind to what it does.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
The run outcome -- ``Executed``, or ``CaughtUp`` when a windowed
|
|
135
|
+
run had nothing to drive.
|
|
136
|
+
|
|
137
|
+
Raises:
|
|
138
|
+
FleetpullError: A fetch, validation, or write failure -- the run is
|
|
139
|
+
recorded failed and the error propagates.
|
|
140
|
+
"""
|
|
141
|
+
endpoint_started = self._spine.clock.monotonic_seconds()
|
|
142
|
+
logger.info(
|
|
143
|
+
'endpoint started: provider=%s endpoint=%s mode=%s',
|
|
144
|
+
definition.provider.value,
|
|
145
|
+
definition.name,
|
|
146
|
+
sync_mode_label(definition.sync_mode),
|
|
147
|
+
)
|
|
148
|
+
match definition.sync_mode:
|
|
149
|
+
case SnapshotMode():
|
|
150
|
+
outcome: RunOutcome = self._run_snapshot(definition, driver, observer)
|
|
151
|
+
case WatermarkMode() as mode:
|
|
152
|
+
outcome = self._watermark_drive.run(definition, driver, mode, observer)
|
|
153
|
+
case FeedMode():
|
|
154
|
+
outcome = self._feed_drive.run(definition, driver, observer)
|
|
155
|
+
if isinstance(outcome, Executed):
|
|
156
|
+
self._log_endpoint_complete(definition, outcome, endpoint_started)
|
|
157
|
+
return outcome
|
|
158
|
+
|
|
159
|
+
def _writer_for(
|
|
160
|
+
self,
|
|
161
|
+
definition: EndpointDefinition[ResponseModel],
|
|
162
|
+
window: DateWindow | None = None,
|
|
163
|
+
) -> DatasetWriter:
|
|
164
|
+
"""Construct the endpoint's writer -- the ONE ``select_writer`` call site.
|
|
165
|
+
|
|
166
|
+
Every arm's writer routes through here (the drives receive it as the
|
|
167
|
+
spine's ``make_writer``), with the dataset root and the exact-dedup
|
|
168
|
+
switch bound once at construction.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
definition: The endpoint being run.
|
|
172
|
+
window: The run's resolved resume window, for the incremental
|
|
173
|
+
cells; ``None`` for the snapshot and feed cells.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
The endpoint's ``DatasetWriter`` for this run.
|
|
177
|
+
"""
|
|
178
|
+
return select_writer(
|
|
179
|
+
definition,
|
|
180
|
+
self._dataset_root,
|
|
181
|
+
window=window,
|
|
182
|
+
drop_duplicates=self._drop_duplicates,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def _run_snapshot(
|
|
186
|
+
self,
|
|
187
|
+
definition: EndpointDefinition[ResponseModel],
|
|
188
|
+
driver: RequestDriver,
|
|
189
|
+
observer: BatchObserver | None,
|
|
190
|
+
) -> RunOutcome:
|
|
191
|
+
"""Run the snapshot arm: full fetch, full-replace write, record the run.
|
|
192
|
+
|
|
193
|
+
Resolves the provider's client, opens a snapshot run, drives the batches,
|
|
194
|
+
writes each, and completes the run with the fetched-row count. A snapshot has
|
|
195
|
+
no resume value and no cursor, so it passes ``resume=None`` and advances no
|
|
196
|
+
watermark. The client is resolved before the run is opened, so an
|
|
197
|
+
unconfigured provider opens no dangling run. ``complete_run`` runs inside the
|
|
198
|
+
protected block: a failure to record completion marks the run failed rather
|
|
199
|
+
than leaving a zombie ``running`` row. The ``metadata.json`` projection
|
|
200
|
+
writes after the protected block -- the run is committed by then, and a
|
|
201
|
+
post-commit projection failure must never mark a succeeded run failed.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
definition: The snapshot endpoint to run.
|
|
205
|
+
driver: The shape-resolved request driver -- a
|
|
206
|
+
``SingleRequestDriver`` for a single-fetch snapshot, a
|
|
207
|
+
``FanOutRequestDriver`` for a ``ParamSweep`` one.
|
|
208
|
+
observer: The optional per-frame hook, handed each post-validation
|
|
209
|
+
frame as the run streams.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
``Executed`` with the fetched-row count and the write report.
|
|
213
|
+
|
|
214
|
+
Raises:
|
|
215
|
+
FleetpullError: A fetch, validation, write, or completion failure -- the
|
|
216
|
+
run is recorded failed and the original error re-raised.
|
|
217
|
+
"""
|
|
218
|
+
recorder = self._spine.state.recorder
|
|
219
|
+
client = self._spine.clients.client_for(definition.provider)
|
|
220
|
+
run_id = recorder.start_snapshot_run(definition.provider, definition.name)
|
|
221
|
+
with recorded_run(recorder, run_id):
|
|
222
|
+
writer = self._writer_for(definition)
|
|
223
|
+
batches = stream_processed_batches(
|
|
224
|
+
definition, driver, client, resume=None, context=None
|
|
225
|
+
)
|
|
226
|
+
records_fetched, _ = drain_batches(
|
|
227
|
+
observe_batches(batches, observer), writer
|
|
228
|
+
)
|
|
229
|
+
write = writer.finalize()
|
|
230
|
+
recorder.complete_run(run_id, row_count=records_fetched)
|
|
231
|
+
outcome = Executed(records_fetched=records_fetched, write=write)
|
|
232
|
+
self._spine.projection.project(definition, outcome, window=None)
|
|
233
|
+
return outcome
|
|
234
|
+
|
|
235
|
+
def _log_endpoint_complete(
|
|
236
|
+
self,
|
|
237
|
+
definition: EndpointDefinition[ResponseModel],
|
|
238
|
+
outcome: Executed,
|
|
239
|
+
endpoint_started: float,
|
|
240
|
+
) -> None:
|
|
241
|
+
"""Narrate a committed endpoint's counts and elapsed time at INFO.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
definition: The endpoint that just ran.
|
|
245
|
+
outcome: The run's (merged) ``Executed`` outcome.
|
|
246
|
+
endpoint_started: The ``monotonic_seconds`` reading captured at
|
|
247
|
+
``run()`` entry, so the elapsed value covers the whole
|
|
248
|
+
endpoint (both arms, metadata projection included).
|
|
249
|
+
"""
|
|
250
|
+
logger.info(
|
|
251
|
+
'endpoint complete: provider=%s endpoint=%s records_fetched=%d '
|
|
252
|
+
'rows_written=%d duplicates_dropped=%d files_written=%d '
|
|
253
|
+
'deleted_partitions=%d elapsed_seconds=%.1f',
|
|
254
|
+
definition.provider.value,
|
|
255
|
+
definition.name,
|
|
256
|
+
outcome.records_fetched,
|
|
257
|
+
outcome.write.rows_written,
|
|
258
|
+
outcome.write.duplicates_dropped,
|
|
259
|
+
outcome.write.files_written,
|
|
260
|
+
len(outcome.write.deleted_partitions),
|
|
261
|
+
self._spine.clock.monotonic_seconds() - endpoint_started,
|
|
262
|
+
)
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# src/fleetpull/orchestrator/shape_resolution.py
|
|
2
|
+
"""The shared shape-to-driver seam: one ``RequestShape`` match, one driver out.
|
|
3
|
+
|
|
4
|
+
``resolve_request_driver`` is the single place a declared ``request_shape``
|
|
5
|
+
becomes a ``RequestDriver`` -- both composition roots call it (the
|
|
6
|
+
orchestration entry for sync, ``fetch`` for the in-memory verb), so a new
|
|
7
|
+
cardinality pattern is a new union member plus its arm here, never a new
|
|
8
|
+
field or a new branch anywhere else. The seam owns only the dispatch:
|
|
9
|
+
supplying roster members for the roster-backed shapes (``RosterFanOut`` /
|
|
10
|
+
``BatchedRosterFanOut``) -- registry lookup, refresh policy, store read,
|
|
11
|
+
the empty-roster guard -- stays with the caller, which feeds them in
|
|
12
|
+
through the ``RosterMemberSource`` callable. A stateless caller
|
|
13
|
+
(``fetch``) passes ``roster_members=None`` and every stateless shape
|
|
14
|
+
resolves; a roster-backed shape then fails loudly, because a roster is
|
|
15
|
+
durable operational state the stateless composition deliberately lacks.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from collections.abc import Callable, Sequence
|
|
19
|
+
from typing import Protocol
|
|
20
|
+
|
|
21
|
+
from fleetpull.endpoints.shared import (
|
|
22
|
+
BatchedRosterFanOut,
|
|
23
|
+
BisectedWindowFetch,
|
|
24
|
+
EndpointDefinition,
|
|
25
|
+
ParamSweep,
|
|
26
|
+
RosterFanOut,
|
|
27
|
+
SingleFetch,
|
|
28
|
+
)
|
|
29
|
+
from fleetpull.exceptions import ConfigurationError
|
|
30
|
+
from fleetpull.model_contract import ResponseModel
|
|
31
|
+
from fleetpull.orchestrator.bisection import BisectingWindowDriver
|
|
32
|
+
from fleetpull.orchestrator.drivers import (
|
|
33
|
+
FanOutRequestDriver,
|
|
34
|
+
RequestDriver,
|
|
35
|
+
SingleRequestDriver,
|
|
36
|
+
)
|
|
37
|
+
from fleetpull.orchestrator.fanout import FetchPool
|
|
38
|
+
from fleetpull.roster import RosterKey
|
|
39
|
+
from fleetpull.vocabulary import Provider
|
|
40
|
+
|
|
41
|
+
__all__: list[str] = [
|
|
42
|
+
'FetchPoolSource',
|
|
43
|
+
'RosterMemberSource',
|
|
44
|
+
'resolve_request_driver',
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class FetchPoolSource(Protocol):
|
|
49
|
+
"""The pool-lookup surface the resolution needs (``FetchPoolRegistry``'s shape)."""
|
|
50
|
+
|
|
51
|
+
def pool_for(self, provider: Provider) -> FetchPool:
|
|
52
|
+
"""Return the fetch pool for a provider."""
|
|
53
|
+
...
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# The caller's roster half of a roster-backed resolution: handed the
|
|
57
|
+
# roster's key, it returns the refreshed membership (or raises the caller's
|
|
58
|
+
# own roster failure). Both roster-backed shapes name their roster with the
|
|
59
|
+
# same ``RosterKey``, so one key-shaped source serves them both -- one
|
|
60
|
+
# roster machinery path, no second seam, no synthetic shape. None marks a
|
|
61
|
+
# stateless caller with no roster state at all.
|
|
62
|
+
type RosterMemberSource = Callable[[RosterKey], Sequence[str]]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def resolve_request_driver(
|
|
66
|
+
definition: EndpointDefinition[ResponseModel],
|
|
67
|
+
*,
|
|
68
|
+
fetch_pools: FetchPoolSource,
|
|
69
|
+
roster_members: RosterMemberSource | None,
|
|
70
|
+
) -> RequestDriver:
|
|
71
|
+
"""Resolve a definition's declared request shape into a request driver.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
definition: The endpoint whose ``request_shape`` routes.
|
|
75
|
+
fetch_pools: Supplies the provider's fetch pool for the fanned shapes
|
|
76
|
+
(``RosterFanOut`` / ``BatchedRosterFanOut`` / ``ParamSweep``);
|
|
77
|
+
never consulted for the single-chain shapes.
|
|
78
|
+
roster_members: The caller's roster membership source, invoked only
|
|
79
|
+
for the roster-backed shapes; ``None`` for a stateless caller
|
|
80
|
+
with no roster state.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
The ``SingleRequestDriver`` for ``SingleFetch``; the
|
|
84
|
+
``BisectingWindowDriver`` for ``BisectedWindowFetch``; the
|
|
85
|
+
``FanOutRequestDriver`` over the roster's members for
|
|
86
|
+
``RosterFanOut``, over sorted comma-joined member batches for
|
|
87
|
+
``BatchedRosterFanOut``, or over the declared values
|
|
88
|
+
(``member_key`` = ``param``) for ``ParamSweep`` -- the driver is
|
|
89
|
+
member-agnostic, so every fanned shape shares it.
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
ConfigurationError: The shape is roster-backed (``RosterFanOut`` /
|
|
93
|
+
``BatchedRosterFanOut``) and no roster source is available --
|
|
94
|
+
the stateless-caller case.
|
|
95
|
+
FleetpullError: Whatever the roster source raises resolving a
|
|
96
|
+
roster-backed shape (an unregistered or empty roster, a
|
|
97
|
+
cold-start refresh failure), propagated unswallowed.
|
|
98
|
+
|
|
99
|
+
Side Effects:
|
|
100
|
+
On the roster-backed paths: whatever the supplied source performs
|
|
101
|
+
(a feeder listing and a store write when stale).
|
|
102
|
+
"""
|
|
103
|
+
match definition.request_shape:
|
|
104
|
+
case SingleFetch():
|
|
105
|
+
return SingleRequestDriver()
|
|
106
|
+
case BisectedWindowFetch() as shape:
|
|
107
|
+
return BisectingWindowDriver(shape=shape)
|
|
108
|
+
case ParamSweep() as sweep:
|
|
109
|
+
return FanOutRequestDriver(
|
|
110
|
+
members=sweep.values,
|
|
111
|
+
member_key=sweep.param,
|
|
112
|
+
fetch_pool=fetch_pools.pool_for(definition.provider),
|
|
113
|
+
)
|
|
114
|
+
case RosterFanOut() as fan_out:
|
|
115
|
+
return FanOutRequestDriver(
|
|
116
|
+
members=_require_roster_members(
|
|
117
|
+
definition, roster_members, fan_out.roster
|
|
118
|
+
),
|
|
119
|
+
member_key=fan_out.member_key,
|
|
120
|
+
fetch_pool=fetch_pools.pool_for(definition.provider),
|
|
121
|
+
)
|
|
122
|
+
case BatchedRosterFanOut() as batched:
|
|
123
|
+
# The batched shape is transport packing over the plain roster
|
|
124
|
+
# fan-out, so membership resolves through the identical source
|
|
125
|
+
# call -- the same roster key -- and only then chunks into
|
|
126
|
+
# comma-joined batch values. The driver stays member-agnostic:
|
|
127
|
+
# each batch is simply one member string.
|
|
128
|
+
members = _require_roster_members(
|
|
129
|
+
definition, roster_members, batched.roster
|
|
130
|
+
)
|
|
131
|
+
return FanOutRequestDriver(
|
|
132
|
+
members=_comma_joined_batches(members, batched.batch_size),
|
|
133
|
+
member_key=batched.member_key,
|
|
134
|
+
fetch_pool=fetch_pools.pool_for(definition.provider),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _require_roster_members(
|
|
139
|
+
definition: EndpointDefinition[ResponseModel],
|
|
140
|
+
roster_members: RosterMemberSource | None,
|
|
141
|
+
roster: RosterKey,
|
|
142
|
+
) -> Sequence[str]:
|
|
143
|
+
"""Resolve a roster-backed shape's membership, refusing a stateless caller.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
definition: The endpoint being resolved, for the error context.
|
|
147
|
+
roster_members: The caller's roster membership source, or ``None``
|
|
148
|
+
for a stateless caller with no roster state.
|
|
149
|
+
roster: The roster the shape names -- a ``RosterFanOut``'s or a
|
|
150
|
+
``BatchedRosterFanOut``'s ``roster`` key alike.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
The refreshed membership, exactly as the source supplies it.
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
ConfigurationError: No roster source is available -- the
|
|
157
|
+
stateless-caller (in-memory ``fetch``) case.
|
|
158
|
+
FleetpullError: Whatever the source raises (an unregistered or
|
|
159
|
+
empty roster, a cold-start refresh failure), propagated
|
|
160
|
+
unswallowed.
|
|
161
|
+
|
|
162
|
+
Side Effects:
|
|
163
|
+
Whatever the supplied source performs (a feeder listing and a
|
|
164
|
+
store write when stale).
|
|
165
|
+
"""
|
|
166
|
+
if roster_members is None:
|
|
167
|
+
raise ConfigurationError(
|
|
168
|
+
'no roster source for a roster fan-out endpoint',
|
|
169
|
+
provider=definition.provider.value,
|
|
170
|
+
endpoint=definition.name,
|
|
171
|
+
detail=(
|
|
172
|
+
f'the {roster.name!r} roster fan-out needs '
|
|
173
|
+
f'durable roster state, which this stateless '
|
|
174
|
+
f'composition (the in-memory fetch verb) deliberately '
|
|
175
|
+
f'lacks; run it through the config-driven sync path'
|
|
176
|
+
),
|
|
177
|
+
)
|
|
178
|
+
return roster_members(roster)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _comma_joined_batches(members: Sequence[str], batch_size: int) -> tuple[str, ...]:
|
|
182
|
+
"""Chunk members into sorted, comma-joined batch values, one per chain.
|
|
183
|
+
|
|
184
|
+
Deterministic by construction: members sort before chunking, so
|
|
185
|
+
identical rosters always produce identical batches regardless of the
|
|
186
|
+
source's ordering. Pure -- no state, no side effects; the batch is
|
|
187
|
+
transport packing only (records self-identify), so nothing here maps
|
|
188
|
+
a member back to its batch.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
members: The roster's member values, in any order. A member
|
|
192
|
+
containing the join delimiter is rejected loudly: a comma
|
|
193
|
+
inside one member would silently widen the batch on the wire
|
|
194
|
+
(more ids than members -- past the API cap, or addressing an
|
|
195
|
+
unintended asset), and no provider's roster ids carry commas.
|
|
196
|
+
batch_size: The maximum members per batch; at least 1, enforced
|
|
197
|
+
by ``BatchedRosterFanOut`` at declaration.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
The comma-joined batch strings, in sorted-member order -- fewer
|
|
201
|
+
members than one batch yields a single batch.
|
|
202
|
+
|
|
203
|
+
Raises:
|
|
204
|
+
ConfigurationError: A member value contains a comma -- corrupt
|
|
205
|
+
roster data surfaced loudly, never packed onto the wire.
|
|
206
|
+
"""
|
|
207
|
+
comma_carriers = [member for member in members if ',' in member]
|
|
208
|
+
if comma_carriers:
|
|
209
|
+
raise ConfigurationError(
|
|
210
|
+
'roster member contains the batch join delimiter',
|
|
211
|
+
detail=(
|
|
212
|
+
f'{len(comma_carriers)} member value(s) contain a comma '
|
|
213
|
+
f'(first: {comma_carriers[0]!r}); a comma-joined batch '
|
|
214
|
+
f'would carry more ids than members'
|
|
215
|
+
),
|
|
216
|
+
)
|
|
217
|
+
ordered = sorted(members)
|
|
218
|
+
return tuple(
|
|
219
|
+
','.join(ordered[start : start + batch_size])
|
|
220
|
+
for start in range(0, len(ordered), batch_size)
|
|
221
|
+
)
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# src/fleetpull/orchestrator/spine.py
|
|
2
|
+
"""The run executor's shared spine: its narrow protocols and drive bundle.
|
|
3
|
+
|
|
4
|
+
The narrow Protocols the run executor and its drive arms depend on instead of
|
|
5
|
+
the concrete state and network classes -- ``ClientSource`` (the registry's
|
|
6
|
+
``client_for``; the roster refresh coordinator shares this one declaration),
|
|
7
|
+
``RunRecorder`` (the ledger's lifecycle methods), and ``CursorAccess`` (the
|
|
8
|
+
cursor store's read and its two kind-guarded writes) -- with the three
|
|
9
|
+
state-database surfaces bundled as ``RunStateAccess`` and the whole drive kit
|
|
10
|
+
bundled as ``RunnerSpine``: the collaborators plus the runner-owned seams
|
|
11
|
+
(the one writer-factory call site, the metadata projection) every arm runs
|
|
12
|
+
through. They live here, below the runner and the drive modules alike, so the
|
|
13
|
+
runner can construct the drives without an import cycle.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
from typing import Protocol
|
|
19
|
+
|
|
20
|
+
from fleetpull.config import SyncConfig
|
|
21
|
+
from fleetpull.endpoints.shared import EndpointDefinition
|
|
22
|
+
from fleetpull.incremental import DateWindow, IncrementalCursor
|
|
23
|
+
from fleetpull.model_contract import ResponseModel
|
|
24
|
+
from fleetpull.network.client import TransportClient
|
|
25
|
+
from fleetpull.orchestrator.metadata_projection import MetadataProjection
|
|
26
|
+
from fleetpull.orchestrator.unit_loop import UnitQueue
|
|
27
|
+
from fleetpull.storage import DatasetWriter
|
|
28
|
+
from fleetpull.timing import Clock
|
|
29
|
+
from fleetpull.vocabulary import Provider
|
|
30
|
+
|
|
31
|
+
__all__: list[str] = [
|
|
32
|
+
'ClientSource',
|
|
33
|
+
'CursorAccess',
|
|
34
|
+
'RunRecorder',
|
|
35
|
+
'RunStateAccess',
|
|
36
|
+
'RunnerSpine',
|
|
37
|
+
'WriterFactory',
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ClientSource(Protocol):
|
|
42
|
+
"""The client-lookup surface the executor needs (a subset of the registry)."""
|
|
43
|
+
|
|
44
|
+
def client_for(self, provider: Provider) -> TransportClient:
|
|
45
|
+
"""Return the open transport client for a provider."""
|
|
46
|
+
...
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class RunRecorder(Protocol):
|
|
50
|
+
"""The run-recording surface the executor needs (a subset of RunLedger)."""
|
|
51
|
+
|
|
52
|
+
def start_snapshot_run(self, provider: Provider, endpoint: str) -> int:
|
|
53
|
+
"""Open a snapshot run and return its id."""
|
|
54
|
+
...
|
|
55
|
+
|
|
56
|
+
def complete_run(
|
|
57
|
+
self, run_id: int, *, row_count: int, to_version: str | None = None
|
|
58
|
+
) -> None:
|
|
59
|
+
"""Close a run as succeeded with its row count (and a feed run's token)."""
|
|
60
|
+
...
|
|
61
|
+
|
|
62
|
+
def fail_run(self, run_id: int, *, error_detail: str) -> None:
|
|
63
|
+
"""Close a run as failed with an error detail."""
|
|
64
|
+
...
|
|
65
|
+
|
|
66
|
+
def start_window_run(
|
|
67
|
+
self, provider: Provider, endpoint: str, *, window: DateWindow
|
|
68
|
+
) -> int:
|
|
69
|
+
"""Open a watermark run for a window and return its id."""
|
|
70
|
+
...
|
|
71
|
+
|
|
72
|
+
def start_feed_run(
|
|
73
|
+
self, provider: Provider, endpoint: str, *, from_version: str
|
|
74
|
+
) -> int:
|
|
75
|
+
"""Open a feed run resuming from a token (or seed label) and return its id."""
|
|
76
|
+
...
|
|
77
|
+
|
|
78
|
+
def coverage_frontier(self, provider: Provider, endpoint: str) -> datetime | None:
|
|
79
|
+
"""Return the furthest window end a succeeded run has covered, if any."""
|
|
80
|
+
...
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class CursorAccess(Protocol):
|
|
84
|
+
"""The cursor surface the incremental arms need (a subset of CursorStore)."""
|
|
85
|
+
|
|
86
|
+
def get_cursor(self, provider: Provider, endpoint: str) -> IncrementalCursor | None:
|
|
87
|
+
"""Return the persisted cursor for a (provider, endpoint), or None."""
|
|
88
|
+
...
|
|
89
|
+
|
|
90
|
+
def advance_watermark_forward(
|
|
91
|
+
self, provider: Provider, endpoint: str, observed: datetime
|
|
92
|
+
) -> bool:
|
|
93
|
+
"""Atomically advance the date watermark iff strictly forward."""
|
|
94
|
+
...
|
|
95
|
+
|
|
96
|
+
def commit_feed_token(
|
|
97
|
+
self, provider: Provider, endpoint: str, to_version: str
|
|
98
|
+
) -> None:
|
|
99
|
+
"""Commit the feed cursor, kind-guarded last-write-wins."""
|
|
100
|
+
...
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass(frozen=True, slots=True)
|
|
104
|
+
class RunStateAccess:
|
|
105
|
+
"""The three state-database surfaces one endpoint run commits through.
|
|
106
|
+
|
|
107
|
+
They always travel together -- the composition root builds all three
|
|
108
|
+
over the one state database and the runner's per-unit crash order
|
|
109
|
+
sequences them (parquet -> ledger completion -> unit done-mark ->
|
|
110
|
+
watermark prefix commit) -- so they ride as one collaborator (the
|
|
111
|
+
bundle rule).
|
|
112
|
+
|
|
113
|
+
Attributes:
|
|
114
|
+
recorder: The run ledger's lifecycle surface.
|
|
115
|
+
cursors: The cursor store's read and kind-guarded-write surface.
|
|
116
|
+
units: The work-unit claim queue.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
recorder: RunRecorder
|
|
120
|
+
cursors: CursorAccess
|
|
121
|
+
units: UnitQueue
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class WriterFactory(Protocol):
|
|
125
|
+
"""The runner's ONE writer-construction seam, as the drives receive it.
|
|
126
|
+
|
|
127
|
+
A bound view of the runner's ``select_writer`` call site (dataset root
|
|
128
|
+
and dedup switch already closed over), so every arm constructs its
|
|
129
|
+
writer through one place and the routing face is called exactly once
|
|
130
|
+
in the orchestration layer.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
def __call__(
|
|
134
|
+
self,
|
|
135
|
+
definition: EndpointDefinition[ResponseModel],
|
|
136
|
+
window: DateWindow | None = None,
|
|
137
|
+
) -> DatasetWriter:
|
|
138
|
+
"""Construct the endpoint's writer for this run (and window, if any)."""
|
|
139
|
+
...
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass(frozen=True, slots=True)
|
|
143
|
+
class RunnerSpine:
|
|
144
|
+
"""The whole drive kit: collaborators plus the runner-owned seams.
|
|
145
|
+
|
|
146
|
+
What the snapshot arm and both drive classes (the watermark and feed
|
|
147
|
+
drives) run through, bundled once (the bundle rule) so the drives
|
|
148
|
+
construct from a single parameter in ``EndpointRunner.__init__``.
|
|
149
|
+
|
|
150
|
+
Attributes:
|
|
151
|
+
clients: Hands out an open per-provider client (the registry).
|
|
152
|
+
state: The state-database surfaces -- ledger, cursor store, unit
|
|
153
|
+
queue.
|
|
154
|
+
clock: Supplies the run instant (trailing edge, future-event guard).
|
|
155
|
+
sync: The sync section -- the cold-start anchor, the unit width,
|
|
156
|
+
and the unit worker count.
|
|
157
|
+
make_writer: The runner's one writer-factory call site, bound.
|
|
158
|
+
projection: The post-commit ``metadata.json`` projection.
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
clients: ClientSource
|
|
162
|
+
state: RunStateAccess
|
|
163
|
+
clock: Clock
|
|
164
|
+
sync: SyncConfig
|
|
165
|
+
make_writer: WriterFactory
|
|
166
|
+
projection: MetadataProjection
|