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,113 @@
|
|
|
1
|
+
# src/fleetpull/endpoints/motive/driver_idle_rollups.py
|
|
2
|
+
"""The Motive driver_idle_rollups binding: the driver arm of the
|
|
3
|
+
window-grain utilization rollup pair (probe-settled 2026-07-21, DESIGN
|
|
4
|
+
section 8). The legacy hub's ``driver_utilization``, shipped under the
|
|
5
|
+
WIRE'S OWN envelope vocabulary -- the wrapper is
|
|
6
|
+
``driver_idle_rollups``/``driver_idle_rollup``, a different vocabulary
|
|
7
|
+
from its ``/v2/driver_utilization`` path, and the endpoint mirrors the
|
|
8
|
+
wire (model ``DriverIdleRollup``; the legacy-name mapping is recorded
|
|
9
|
+
in ``ENDPOINTS.md``).
|
|
10
|
+
|
|
11
|
+
The vehicle arm's binding with the path, wrapper keys, and population
|
|
12
|
+
swapped: the standard Motive wrapped-list envelope and offset
|
|
13
|
+
pagination at the configured page size, but rows are the DRIVERS WITH
|
|
14
|
+
ACTIVITY in the window (13 on a quiet single day, 653 across six days
|
|
15
|
+
-- per-driver-per-window grain, unlike the vehicle arm's
|
|
16
|
+
whole-fleet-every-window population), each attributed to the shared
|
|
17
|
+
8-key ``UserSummary`` ``driver`` ref -- or to NULL on the unattributed
|
|
18
|
+
rollup bucket row. Fleet-wide with per-record attribution, so no
|
|
19
|
+
fan-out and no roster -- the default ``SingleFetch`` shape, declared by
|
|
20
|
+
declaring nothing.
|
|
21
|
+
|
|
22
|
+
The pair's window-grain facts are shared (vehicle_utilizations carries
|
|
23
|
+
the full statement): the rollup grain is the request window, so
|
|
24
|
+
``fixed_unit_days=1`` (the fuel-energy machinery's second consumer; the
|
|
25
|
+
family precedent's do-not-sum posture rides the model docstring), the
|
|
26
|
+
decoder-stamped ``window_start`` routes each day's rollup to its own
|
|
27
|
+
partition, and the shared ``MotiveFleetDateRangeSpecBuilder`` maps the
|
|
28
|
+
unit ``DateWindow`` to the INCLUSIVE ``start_date``/``end_date`` label
|
|
29
|
+
pair -- interpreted in COMPANY-LOCAL days (the account's
|
|
30
|
+
``/v1/companies`` zone at a UTC-5 offset), mirrored verbatim, never
|
|
31
|
+
converted.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from datetime import timedelta
|
|
35
|
+
from typing import Final
|
|
36
|
+
|
|
37
|
+
from fleetpull.config import MotiveConfig
|
|
38
|
+
from fleetpull.endpoints.motive._spec_builders import MotiveFleetDateRangeSpecBuilder
|
|
39
|
+
from fleetpull.endpoints.shared import (
|
|
40
|
+
EndpointDefinition,
|
|
41
|
+
StorageKind,
|
|
42
|
+
WatermarkMode,
|
|
43
|
+
)
|
|
44
|
+
from fleetpull.models.motive import DriverIdleRollup
|
|
45
|
+
from fleetpull.network.decoders import MotiveWindowReportPageDecoder
|
|
46
|
+
from fleetpull.vocabulary import Provider, QuotaScope
|
|
47
|
+
|
|
48
|
+
__all__: list[str] = ['build_endpoint']
|
|
49
|
+
|
|
50
|
+
_DRIVER_IDLE_ROLLUPS_PATH: Final[str] = '/v2/driver_utilization'
|
|
51
|
+
|
|
52
|
+
# The wire's OWN envelope vocabulary -- NOT the path's: the wrapper keys
|
|
53
|
+
# are driver_idle_rollups/driver_idle_rollup (captured 2026-07-21), and
|
|
54
|
+
# the endpoint name follows the wire.
|
|
55
|
+
_DRIVER_IDLE_ROLLUPS_LIST_KEY: Final[str] = 'driver_idle_rollups'
|
|
56
|
+
_DRIVER_IDLE_ROLLUPS_ITEM_KEY: Final[str] = 'driver_idle_rollup'
|
|
57
|
+
|
|
58
|
+
# The fixed work-unit width, in whole days -- the pair's window-grain
|
|
59
|
+
# proof (module docstring; captured 2026-07-21): the unit width is part
|
|
60
|
+
# of the row's meaning, pinned here rather than left to
|
|
61
|
+
# sync.backfill_chunk_days (the fuel-energy machinery, DESIGN section 5).
|
|
62
|
+
_FIXED_UNIT_DAYS: Final[int] = 1
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def build_endpoint(config: MotiveConfig) -> EndpointDefinition[DriverIdleRollup]:
|
|
66
|
+
"""Build the Motive driver_idle_rollups watermark binding.
|
|
67
|
+
|
|
68
|
+
Per-driver idle rollups fetched incrementally at the fixed 1-day
|
|
69
|
+
unit width: the run resumes from a ``DateWindow`` (watermark with
|
|
70
|
+
the provider's late-arrival lookback from config), the planner tiles
|
|
71
|
+
it into exactly-one-day units (the declared ``fixed_unit_days`` --
|
|
72
|
+
module docstring for the window-grain statement), each unit's rows
|
|
73
|
+
are stamped with its window by the decoder and written to the
|
|
74
|
+
``date=YYYY-MM-DD`` partition on ``window_start``, and each
|
|
75
|
+
refetched partition is replaced. Records arrive wrapped
|
|
76
|
+
(``{"driver_idle_rollups": [{"driver_idle_rollup": {...}}]}``)
|
|
77
|
+
under page-numbered pagination at the page size the config requests,
|
|
78
|
+
the ``start_date``/``end_date`` labels persisting across the walk.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
config: The validated Motive configuration; supplies the base
|
|
82
|
+
URL the spec-builder joins to the utilization path, the page
|
|
83
|
+
size the decoder requests, and the lookback and cutoff the
|
|
84
|
+
watermark mode carries.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
The frozen driver_idle_rollups ``EndpointDefinition``.
|
|
88
|
+
Construction validates the ``WatermarkMode`` /
|
|
89
|
+
``DATE_PARTITIONED`` / ``event_time_column`` triple against the
|
|
90
|
+
response model.
|
|
91
|
+
"""
|
|
92
|
+
return EndpointDefinition(
|
|
93
|
+
provider=Provider.MOTIVE,
|
|
94
|
+
name='driver_idle_rollups',
|
|
95
|
+
spec_builder=MotiveFleetDateRangeSpecBuilder(
|
|
96
|
+
base_url=config.base_url,
|
|
97
|
+
path=_DRIVER_IDLE_ROLLUPS_PATH,
|
|
98
|
+
),
|
|
99
|
+
page_decoder=MotiveWindowReportPageDecoder(
|
|
100
|
+
list_key=_DRIVER_IDLE_ROLLUPS_LIST_KEY,
|
|
101
|
+
item_key=_DRIVER_IDLE_ROLLUPS_ITEM_KEY,
|
|
102
|
+
per_page=config.records_per_page,
|
|
103
|
+
),
|
|
104
|
+
response_model=DriverIdleRollup,
|
|
105
|
+
quota_scope=QuotaScope.MOTIVE,
|
|
106
|
+
storage_kind=StorageKind.DATE_PARTITIONED,
|
|
107
|
+
sync_mode=WatermarkMode(
|
|
108
|
+
lookback=timedelta(days=config.lookback_days),
|
|
109
|
+
cutoff=timedelta(days=config.cutoff_days),
|
|
110
|
+
fixed_unit_days=_FIXED_UNIT_DAYS,
|
|
111
|
+
),
|
|
112
|
+
event_time_column='window_start',
|
|
113
|
+
)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# src/fleetpull/endpoints/motive/driving_periods.py
|
|
2
|
+
"""The Motive driving_periods watermark binding.
|
|
3
|
+
|
|
4
|
+
A fleet-wide driving-event endpoint: one date-range request covers every
|
|
5
|
+
vehicle, offset-paginated through the shared wrapped-list decoder. Window
|
|
6
|
+
matching is START-anchored on UTC days (DESIGN §8, captured 2026-07-15):
|
|
7
|
+
retrieval anchor and partition-routing anchor coincide natively, so the
|
|
8
|
+
builder maps the resume window to the wire dates with no pad.
|
|
9
|
+
|
|
10
|
+
The provider enforces a loud 30-day range cap — HTTP 400, ``"Date range
|
|
11
|
+
cannot be greater than 30 days"`` — counting the date delta. Steady-state
|
|
12
|
+
windows sit far inside it; a backfill configured with chunks wider than
|
|
13
|
+
30 days fails loudly on the first request rather than losing data
|
|
14
|
+
silently, and the fix is a smaller chunk width.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from datetime import timedelta
|
|
18
|
+
from typing import Final
|
|
19
|
+
|
|
20
|
+
from fleetpull.config import MotiveConfig
|
|
21
|
+
from fleetpull.endpoints.motive._spec_builders import MotiveFleetDateRangeSpecBuilder
|
|
22
|
+
from fleetpull.endpoints.shared import (
|
|
23
|
+
EndpointDefinition,
|
|
24
|
+
StorageKind,
|
|
25
|
+
WatermarkMode,
|
|
26
|
+
)
|
|
27
|
+
from fleetpull.models.motive import DrivingPeriod
|
|
28
|
+
from fleetpull.network.decoders import MotiveWrappedListPageDecoder
|
|
29
|
+
from fleetpull.vocabulary import Provider, QuotaScope
|
|
30
|
+
|
|
31
|
+
__all__: list[str] = ['build_endpoint']
|
|
32
|
+
|
|
33
|
+
_DRIVING_PERIODS_PATH: Final[str] = '/v1/driving_periods'
|
|
34
|
+
_DRIVING_PERIODS_LIST_KEY: Final[str] = 'driving_periods'
|
|
35
|
+
_DRIVING_PERIODS_ITEM_KEY: Final[str] = 'driving_period'
|
|
36
|
+
|
|
37
|
+
# The page size sent on every request. Captured honored at 100
|
|
38
|
+
# (2026-07-15); larger values are unprobed. A strong candidate for a
|
|
39
|
+
# user config knob — promotion is one new MotiveConfig field passed
|
|
40
|
+
# where this constant is passed.
|
|
41
|
+
_PER_PAGE: Final[int] = 100
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def build_endpoint(config: MotiveConfig) -> EndpointDefinition[DrivingPeriod]:
|
|
45
|
+
"""Build the Motive driving_periods watermark binding.
|
|
46
|
+
|
|
47
|
+
Fleet-wide driving spans fetched incrementally: the run resumes from
|
|
48
|
+
a ``DateWindow`` (watermark with the provider's late-arrival lookback
|
|
49
|
+
from config — which also refetches yesterday's in-progress records
|
|
50
|
+
once they complete), the fetched whole days are written to
|
|
51
|
+
``date=YYYY-MM-DD`` partitions on ``start_time``, and each refetched
|
|
52
|
+
partition is replaced. Records arrive wrapped and offset-paginated
|
|
53
|
+
(``{"driving_periods": [{"driving_period": {...}}], "pagination":
|
|
54
|
+
{...}}``).
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
config: The validated Motive configuration; supplies the base URL
|
|
58
|
+
and the lookback and cutoff the watermark mode carries.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
The frozen driving_periods ``EndpointDefinition``. Construction
|
|
62
|
+
validates the ``WatermarkMode`` / ``DATE_PARTITIONED`` /
|
|
63
|
+
``event_time_column`` triple against the response model.
|
|
64
|
+
"""
|
|
65
|
+
return EndpointDefinition(
|
|
66
|
+
provider=Provider.MOTIVE,
|
|
67
|
+
name='driving_periods',
|
|
68
|
+
spec_builder=MotiveFleetDateRangeSpecBuilder(
|
|
69
|
+
base_url=config.base_url,
|
|
70
|
+
path=_DRIVING_PERIODS_PATH,
|
|
71
|
+
),
|
|
72
|
+
page_decoder=MotiveWrappedListPageDecoder(
|
|
73
|
+
list_key=_DRIVING_PERIODS_LIST_KEY,
|
|
74
|
+
item_key=_DRIVING_PERIODS_ITEM_KEY,
|
|
75
|
+
per_page=_PER_PAGE,
|
|
76
|
+
),
|
|
77
|
+
response_model=DrivingPeriod,
|
|
78
|
+
quota_scope=QuotaScope.MOTIVE,
|
|
79
|
+
storage_kind=StorageKind.DATE_PARTITIONED,
|
|
80
|
+
sync_mode=WatermarkMode(
|
|
81
|
+
lookback=timedelta(days=config.lookback_days),
|
|
82
|
+
cutoff=timedelta(days=config.cutoff_days),
|
|
83
|
+
),
|
|
84
|
+
event_time_column='start_time',
|
|
85
|
+
)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# src/fleetpull/endpoints/motive/groups.py
|
|
2
|
+
"""The Motive groups binding: a factory composing the groups snapshot
|
|
3
|
+
EndpointDefinition from resolved Motive configuration.
|
|
4
|
+
|
|
5
|
+
The vehicles template verbatim, bound to ``/v1/groups`` (probed
|
|
6
|
+
2026-07-21): a full-population wrapped-list snapshot on the shared
|
|
7
|
+
static-GET builder and the existing Motive offset-pagination decoder, at
|
|
8
|
+
the configured page size (``per_page`` 50 and 100 both honored live). No
|
|
9
|
+
roster is sourced or consumed.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import Final
|
|
13
|
+
|
|
14
|
+
from fleetpull.config import MotiveConfig
|
|
15
|
+
from fleetpull.endpoints.shared import (
|
|
16
|
+
EndpointDefinition,
|
|
17
|
+
SnapshotMode,
|
|
18
|
+
StaticGetSpecBuilder,
|
|
19
|
+
StorageKind,
|
|
20
|
+
)
|
|
21
|
+
from fleetpull.models.motive import Group
|
|
22
|
+
from fleetpull.network.decoders import MotiveWrappedListPageDecoder
|
|
23
|
+
from fleetpull.vocabulary import Provider, QuotaScope
|
|
24
|
+
|
|
25
|
+
__all__: list[str] = ['build_endpoint']
|
|
26
|
+
|
|
27
|
+
_GROUPS_PATH: Final[str] = '/v1/groups'
|
|
28
|
+
_GROUPS_LIST_KEY: Final[str] = 'groups'
|
|
29
|
+
_GROUPS_ITEM_KEY: Final[str] = 'group'
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def build_endpoint(config: MotiveConfig) -> EndpointDefinition[Group]:
|
|
33
|
+
"""Build the Motive groups snapshot binding.
|
|
34
|
+
|
|
35
|
+
A full-dataset snapshot of the company's group tree: no resume, a
|
|
36
|
+
single parquet file, full replacement each run. Records arrive
|
|
37
|
+
wrapped (``{"groups": [{"group": {...}}]}``) under page-numbered
|
|
38
|
+
pagination, at the page size the config requests.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
config: The validated Motive configuration; supplies the base URL
|
|
42
|
+
the spec-builder joins to the groups path and the page size
|
|
43
|
+
the decoder requests.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
The frozen groups ``EndpointDefinition``.
|
|
47
|
+
"""
|
|
48
|
+
return EndpointDefinition(
|
|
49
|
+
provider=Provider.MOTIVE,
|
|
50
|
+
name='groups',
|
|
51
|
+
spec_builder=StaticGetSpecBuilder(base_url=config.base_url, path=_GROUPS_PATH),
|
|
52
|
+
page_decoder=MotiveWrappedListPageDecoder(
|
|
53
|
+
list_key=_GROUPS_LIST_KEY,
|
|
54
|
+
item_key=_GROUPS_ITEM_KEY,
|
|
55
|
+
per_page=config.records_per_page,
|
|
56
|
+
),
|
|
57
|
+
response_model=Group,
|
|
58
|
+
quota_scope=QuotaScope.MOTIVE,
|
|
59
|
+
storage_kind=StorageKind.SINGLE,
|
|
60
|
+
sync_mode=SnapshotMode(),
|
|
61
|
+
)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# src/fleetpull/endpoints/motive/idle_events.py
|
|
2
|
+
"""The Motive idle_events watermark binding.
|
|
3
|
+
|
|
4
|
+
A fleet-wide idle-event endpoint: one date-range request covers every
|
|
5
|
+
vehicle, offset-paginated through the shared wrapped-list decoder. Unlike
|
|
6
|
+
its driving_periods sibling, window matching here is OVERLAP-anchored on
|
|
7
|
+
**company-local** day boundaries, not UTC (DESIGN §8, captured
|
|
8
|
+
2026-07-15, prediction-confirmed): the wire dates are interpreted in the
|
|
9
|
+
account's configured timezone. The binding therefore pads the wire window
|
|
10
|
+
one day on each side — covering any account timezone — and the true UTC
|
|
11
|
+
window does the trimming: the post-fetch window filter keeps only records
|
|
12
|
+
whose ``start_time`` falls in the resume window, and the writer's
|
|
13
|
+
partition tripwire enforces it. Every record lands in exactly one chunk;
|
|
14
|
+
the pad widens only what is fetched, never what is written.
|
|
15
|
+
|
|
16
|
+
The 30-day range cap is NOT enforced here (a 35-day window was honored
|
|
17
|
+
end-to-end in capture) — but chunking stays bounded with the sibling's
|
|
18
|
+
cap anyway, keeping wide-window latency (observed 12-18 s) inside the
|
|
19
|
+
configured HTTP read timeout.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from datetime import timedelta
|
|
23
|
+
from typing import Final
|
|
24
|
+
|
|
25
|
+
from fleetpull.config import MotiveConfig
|
|
26
|
+
from fleetpull.endpoints.motive._spec_builders import MotiveFleetDateRangeSpecBuilder
|
|
27
|
+
from fleetpull.endpoints.shared import (
|
|
28
|
+
EndpointDefinition,
|
|
29
|
+
StorageKind,
|
|
30
|
+
WatermarkMode,
|
|
31
|
+
)
|
|
32
|
+
from fleetpull.models.motive import IdleEvent
|
|
33
|
+
from fleetpull.network.decoders import MotiveWrappedListPageDecoder
|
|
34
|
+
from fleetpull.vocabulary import Provider, QuotaScope
|
|
35
|
+
|
|
36
|
+
__all__: list[str] = ['build_endpoint']
|
|
37
|
+
|
|
38
|
+
_IDLE_EVENTS_PATH: Final[str] = '/v1/idle_events'
|
|
39
|
+
_IDLE_EVENTS_LIST_KEY: Final[str] = 'idle_events'
|
|
40
|
+
_IDLE_EVENTS_ITEM_KEY: Final[str] = 'idle_event'
|
|
41
|
+
|
|
42
|
+
# The page size sent on every request. Captured honored at 100
|
|
43
|
+
# (2026-07-15); larger values are unprobed. A strong candidate for a
|
|
44
|
+
# user config knob — promotion is one new MotiveConfig field passed
|
|
45
|
+
# where this constant is passed.
|
|
46
|
+
_PER_PAGE: Final[int] = 100
|
|
47
|
+
|
|
48
|
+
# One whole day each side: the timezone-agnostic cover for the
|
|
49
|
+
# company-local overlap matching described in the module docstring.
|
|
50
|
+
_WINDOW_PAD_DAYS: Final[int] = 1
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def build_endpoint(config: MotiveConfig) -> EndpointDefinition[IdleEvent]:
|
|
54
|
+
"""Build the Motive idle_events watermark binding.
|
|
55
|
+
|
|
56
|
+
Fleet-wide idle intervals fetched incrementally: the run resumes from
|
|
57
|
+
a ``DateWindow`` (watermark with the provider's late-arrival lookback
|
|
58
|
+
from config), the wire window is padded per the module docstring, the
|
|
59
|
+
kept records are written to ``date=YYYY-MM-DD`` partitions on
|
|
60
|
+
``start_time``, and each refetched partition is replaced. Records
|
|
61
|
+
arrive wrapped and offset-paginated (``{"idle_events":
|
|
62
|
+
[{"idle_event": {...}}], "pagination": {...}}``).
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
config: The validated Motive configuration; supplies the base URL
|
|
66
|
+
and the lookback and cutoff the watermark mode carries.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
The frozen idle_events ``EndpointDefinition``. Construction
|
|
70
|
+
validates the ``WatermarkMode`` / ``DATE_PARTITIONED`` /
|
|
71
|
+
``event_time_column`` triple against the response model.
|
|
72
|
+
"""
|
|
73
|
+
return EndpointDefinition(
|
|
74
|
+
provider=Provider.MOTIVE,
|
|
75
|
+
name='idle_events',
|
|
76
|
+
spec_builder=MotiveFleetDateRangeSpecBuilder(
|
|
77
|
+
base_url=config.base_url,
|
|
78
|
+
path=_IDLE_EVENTS_PATH,
|
|
79
|
+
window_pad_days=_WINDOW_PAD_DAYS,
|
|
80
|
+
),
|
|
81
|
+
page_decoder=MotiveWrappedListPageDecoder(
|
|
82
|
+
list_key=_IDLE_EVENTS_LIST_KEY,
|
|
83
|
+
item_key=_IDLE_EVENTS_ITEM_KEY,
|
|
84
|
+
per_page=_PER_PAGE,
|
|
85
|
+
),
|
|
86
|
+
response_model=IdleEvent,
|
|
87
|
+
quota_scope=QuotaScope.MOTIVE,
|
|
88
|
+
storage_kind=StorageKind.DATE_PARTITIONED,
|
|
89
|
+
sync_mode=WatermarkMode(
|
|
90
|
+
lookback=timedelta(days=config.lookback_days),
|
|
91
|
+
cutoff=timedelta(days=config.cutoff_days),
|
|
92
|
+
),
|
|
93
|
+
event_time_column='start_time',
|
|
94
|
+
)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# src/fleetpull/endpoints/motive/users.py
|
|
2
|
+
"""The Motive users binding: a factory composing the users snapshot
|
|
3
|
+
EndpointDefinition from resolved Motive configuration.
|
|
4
|
+
|
|
5
|
+
The vehicles template verbatim, bound to ``/v1/users`` (probed
|
|
6
|
+
2026-07-21): a full-population wrapped-list snapshot on the shared
|
|
7
|
+
static-GET builder and the existing Motive offset-pagination decoder, at
|
|
8
|
+
the configured page size (``per_page`` 50 and 100 both honored live).
|
|
9
|
+
Only the pagination parameters are sent — the unfiltered listing is the
|
|
10
|
+
complete population, one dataset despite the role-partitioned record
|
|
11
|
+
shape: the ``role`` column carries the split (DESIGN section 8). No
|
|
12
|
+
roster is sourced or consumed.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from typing import Final
|
|
16
|
+
|
|
17
|
+
from fleetpull.config import MotiveConfig
|
|
18
|
+
from fleetpull.endpoints.shared import (
|
|
19
|
+
EndpointDefinition,
|
|
20
|
+
SnapshotMode,
|
|
21
|
+
StaticGetSpecBuilder,
|
|
22
|
+
StorageKind,
|
|
23
|
+
)
|
|
24
|
+
from fleetpull.models.motive import User
|
|
25
|
+
from fleetpull.network.decoders import MotiveWrappedListPageDecoder
|
|
26
|
+
from fleetpull.vocabulary import Provider, QuotaScope
|
|
27
|
+
|
|
28
|
+
__all__: list[str] = ['build_endpoint']
|
|
29
|
+
|
|
30
|
+
_USERS_PATH: Final[str] = '/v1/users'
|
|
31
|
+
_USERS_LIST_KEY: Final[str] = 'users'
|
|
32
|
+
_USERS_ITEM_KEY: Final[str] = 'user'
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def build_endpoint(config: MotiveConfig) -> EndpointDefinition[User]:
|
|
36
|
+
"""Build the Motive users snapshot binding.
|
|
37
|
+
|
|
38
|
+
A full-dataset snapshot of every account — drivers, admins, and
|
|
39
|
+
fleet users: no resume, a single parquet file, full replacement each
|
|
40
|
+
run. Records arrive wrapped (``{"users": [{"user": {...}}]}``) under
|
|
41
|
+
page-numbered pagination, at the page size the config requests.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
config: The validated Motive configuration; supplies the base URL
|
|
45
|
+
the spec-builder joins to the users path and the page size
|
|
46
|
+
the decoder requests.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
The frozen users ``EndpointDefinition``.
|
|
50
|
+
"""
|
|
51
|
+
return EndpointDefinition(
|
|
52
|
+
provider=Provider.MOTIVE,
|
|
53
|
+
name='users',
|
|
54
|
+
spec_builder=StaticGetSpecBuilder(base_url=config.base_url, path=_USERS_PATH),
|
|
55
|
+
page_decoder=MotiveWrappedListPageDecoder(
|
|
56
|
+
list_key=_USERS_LIST_KEY,
|
|
57
|
+
item_key=_USERS_ITEM_KEY,
|
|
58
|
+
per_page=config.records_per_page,
|
|
59
|
+
),
|
|
60
|
+
response_model=User,
|
|
61
|
+
quota_scope=QuotaScope.MOTIVE,
|
|
62
|
+
storage_kind=StorageKind.SINGLE,
|
|
63
|
+
sync_mode=SnapshotMode(),
|
|
64
|
+
)
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# src/fleetpull/endpoints/motive/vehicle_locations.py
|
|
2
|
+
"""The Motive vehicle_locations watermark spec-builder and roster fan-out shape.
|
|
3
|
+
|
|
4
|
+
A per-vehicle breadcrumb endpoint. The binding declares its request shape
|
|
5
|
+
(``request_shape=RosterFanOut(...)``) naming the ``vehicle_ids`` roster by its
|
|
6
|
+
opaque ``RosterKey`` -- this module knows nothing about the roster's feeder.
|
|
7
|
+
The orchestration entry (``orchestrator/entry.py``) resolves that key through
|
|
8
|
+
the ``RosterRegistry`` to its ``RosterDefinition``, refreshes the stored
|
|
9
|
+
membership via the ``RosterRefreshCoordinator`` when stale, reads the members
|
|
10
|
+
from the ``RosterStore``, and feeds the shape-resolution seam, which builds
|
|
11
|
+
the ``FanOutRequestDriver`` -- that driver then calls ``build_spec`` once per
|
|
12
|
+
member, passing that vehicle's id in ``member_values`` and the run's resume
|
|
13
|
+
``DateWindow`` as ``resume``. The builder renders the per-vehicle path and
|
|
14
|
+
maps the window to Motive's ``start_date`` / ``end_date`` query parameters.
|
|
15
|
+
|
|
16
|
+
Motive's ``start_date`` / ``end_date`` are inclusive on both ends and day-granular,
|
|
17
|
+
anchored on ``located_at`` -- the endpoint returns every breadcrumb whose date falls
|
|
18
|
+
in ``[start_date, end_date]`` (confirmed against the predecessor's production
|
|
19
|
+
fetcher, whose comment records that Motive "returns full days"). The date-partitioned
|
|
20
|
+
watermark path wants exactly those whole days -- each ``date=`` partition is replaced
|
|
21
|
+
wholesale -- so unlike the predecessor (which post-filtered to an exact datetime
|
|
22
|
+
range for its single-file output), this builder does not trim: it fetches the whole
|
|
23
|
+
days and lets the writer replace whole partitions.
|
|
24
|
+
|
|
25
|
+
The mapping aligns with the storage layer's covered dates (``window_dates``):
|
|
26
|
+
``start_date`` is the UTC date of ``window.start`` and ``end_date`` is the window's
|
|
27
|
+
``last_covered_date`` (the one-microsecond derivation is stated once, on
|
|
28
|
+
``DateWindow``). So what
|
|
29
|
+
this builder fetches equals what the writer replaces and prunes: the same date set,
|
|
30
|
+
no edge double-count, idempotent on refetch.
|
|
31
|
+
|
|
32
|
+
This is the dedicated builder for vehicle_locations specifically -- a date-range
|
|
33
|
+
fetch plus a per-vehicle path fan-out. The fleet-wide Motive date-range endpoints
|
|
34
|
+
(driving_periods, idle_events) share ``MotiveFleetDateRangeSpecBuilder``
|
|
35
|
+
(``endpoints/motive/_spec_builders.py``); this one stays separate because it renders
|
|
36
|
+
the per-vehicle path.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from collections.abc import Mapping
|
|
40
|
+
from dataclasses import dataclass
|
|
41
|
+
from datetime import timedelta
|
|
42
|
+
from typing import Final
|
|
43
|
+
|
|
44
|
+
from fleetpull.config import MotiveConfig
|
|
45
|
+
from fleetpull.endpoints.shared import (
|
|
46
|
+
EndpointDefinition,
|
|
47
|
+
ResumeValue,
|
|
48
|
+
RosterFanOut,
|
|
49
|
+
StorageKind,
|
|
50
|
+
WatermarkMode,
|
|
51
|
+
render_url_path_template,
|
|
52
|
+
require_date_window,
|
|
53
|
+
)
|
|
54
|
+
from fleetpull.models.motive import VehicleLocation
|
|
55
|
+
from fleetpull.network.contract import HttpMethod, RequestSpec
|
|
56
|
+
from fleetpull.network.decoders import MotiveWrappedSinglePageDecoder
|
|
57
|
+
from fleetpull.roster import RosterKey
|
|
58
|
+
from fleetpull.timing import to_utc_date_string
|
|
59
|
+
from fleetpull.vocabulary import Provider, QuotaScope
|
|
60
|
+
|
|
61
|
+
__all__: list[str] = [
|
|
62
|
+
'MotiveVehicleLocationsSpecBuilder',
|
|
63
|
+
'build_endpoint',
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
_VEHICLE_LOCATIONS_PATH: Final[str] = '/v3/vehicle_locations/{vehicle_id}'
|
|
67
|
+
_VEHICLE_LOCATIONS_LIST_KEY: Final[str] = 'vehicle_locations'
|
|
68
|
+
_VEHICLE_LOCATIONS_ITEM_KEY: Final[str] = 'vehicle_location'
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True, slots=True)
|
|
72
|
+
class MotiveVehicleLocationsSpecBuilder:
|
|
73
|
+
"""Build the per-vehicle, date-windowed first request for vehicle_locations.
|
|
74
|
+
|
|
75
|
+
The ``SpecBuilder`` for the Motive vehicle_locations watermark endpoint:
|
|
76
|
+
renders the per-vehicle path and injects the resume window as Motive's
|
|
77
|
+
``start_date`` / ``end_date`` parameters. The endpoint is not paginated, so this
|
|
78
|
+
first request is the only request; the page decoder returns no successor.
|
|
79
|
+
|
|
80
|
+
Attributes:
|
|
81
|
+
base_url: Root of the Motive API, trailing-slash-normalized by the provider
|
|
82
|
+
config so the leading-slash path joins directly.
|
|
83
|
+
path_template: The per-vehicle request path with the ``{vehicle_id}``
|
|
84
|
+
placeholder (``'/v3/vehicle_locations/{vehicle_id}'``).
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
base_url: str
|
|
88
|
+
path_template: str
|
|
89
|
+
|
|
90
|
+
def build_spec(
|
|
91
|
+
self, resume: ResumeValue, member_values: Mapping[str, str]
|
|
92
|
+
) -> RequestSpec:
|
|
93
|
+
"""Build the per-vehicle, date-windowed GET.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
resume: The run's resume window. Must be a ``DateWindow`` -- a watermark
|
|
97
|
+
endpoint always resumes from one; any other value is a wiring bug.
|
|
98
|
+
member_values: The fan-out member binding; must carry ``vehicle_id``
|
|
99
|
+
(the strict template renderer rejects a missing or extra key).
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
A credential-less ``GET`` for the per-vehicle URL, carrying
|
|
103
|
+
``start_date`` / ``end_date`` for the window's covered dates. Auth
|
|
104
|
+
headers are layered on by the client's ``ProviderProfile``.
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
TypeError: ``resume`` is not a ``DateWindow``.
|
|
108
|
+
UrlPathTemplateError: ``member_values`` does not match the template's
|
|
109
|
+
placeholders, or a value is empty.
|
|
110
|
+
|
|
111
|
+
Side Effects:
|
|
112
|
+
None.
|
|
113
|
+
"""
|
|
114
|
+
resume_window = require_date_window(resume, type(self).__name__)
|
|
115
|
+
rendered_path = render_url_path_template(self.path_template, member_values)
|
|
116
|
+
url = f'{self.base_url}{rendered_path}'
|
|
117
|
+
start_date = to_utc_date_string(resume_window.start)
|
|
118
|
+
end_date = resume_window.last_covered_date.isoformat()
|
|
119
|
+
params = {'start_date': start_date, 'end_date': end_date}
|
|
120
|
+
return RequestSpec(method=HttpMethod.GET, url=url, params=params)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def build_endpoint(config: MotiveConfig) -> EndpointDefinition[VehicleLocation]:
|
|
124
|
+
"""Build the Motive vehicle_locations watermark binding.
|
|
125
|
+
|
|
126
|
+
A per-vehicle breadcrumb history fetched incrementally: the run resumes from a
|
|
127
|
+
``DateWindow`` (watermark with the provider's late-arrival lookback from config),
|
|
128
|
+
the fetched whole days are written to ``date=YYYY-MM-DD`` partitions, and each
|
|
129
|
+
refetched partition is replaced. Records arrive wrapped and unpaginated
|
|
130
|
+
(``{"vehicle_locations": [{"vehicle_location": {...}}]}``). The
|
|
131
|
+
``request_shape`` declaration (``RosterFanOut``) names the ``vehicle_ids``
|
|
132
|
+
roster; the orchestration entry resolves it to members and fans one request
|
|
133
|
+
chain per vehicle, passing each ``vehicle_id`` to the spec-builder's
|
|
134
|
+
``member_values`` — this binding only declares the strategies and the
|
|
135
|
+
roster key, never the roster's feeder.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
config: The validated Motive configuration; supplies the base URL the
|
|
139
|
+
spec-builder joins to the per-vehicle path and the lookback and
|
|
140
|
+
cutoff the watermark mode carry.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
The frozen vehicle_locations ``EndpointDefinition``. Construction validates
|
|
144
|
+
the ``WatermarkMode`` / ``DATE_PARTITIONED`` / ``event_time_column`` triple
|
|
145
|
+
against the response model.
|
|
146
|
+
"""
|
|
147
|
+
return EndpointDefinition(
|
|
148
|
+
provider=Provider.MOTIVE,
|
|
149
|
+
name='vehicle_locations',
|
|
150
|
+
spec_builder=MotiveVehicleLocationsSpecBuilder(
|
|
151
|
+
base_url=config.base_url,
|
|
152
|
+
path_template=_VEHICLE_LOCATIONS_PATH,
|
|
153
|
+
),
|
|
154
|
+
page_decoder=MotiveWrappedSinglePageDecoder(
|
|
155
|
+
list_key=_VEHICLE_LOCATIONS_LIST_KEY,
|
|
156
|
+
item_key=_VEHICLE_LOCATIONS_ITEM_KEY,
|
|
157
|
+
),
|
|
158
|
+
response_model=VehicleLocation,
|
|
159
|
+
quota_scope=QuotaScope.MOTIVE,
|
|
160
|
+
storage_kind=StorageKind.DATE_PARTITIONED,
|
|
161
|
+
sync_mode=WatermarkMode(
|
|
162
|
+
lookback=timedelta(days=config.lookback_days),
|
|
163
|
+
cutoff=timedelta(days=config.cutoff_days),
|
|
164
|
+
),
|
|
165
|
+
event_time_column='located_at',
|
|
166
|
+
request_shape=RosterFanOut(
|
|
167
|
+
roster=RosterKey(Provider.MOTIVE, 'vehicle_ids'),
|
|
168
|
+
member_key='vehicle_id',
|
|
169
|
+
),
|
|
170
|
+
)
|