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,211 @@
|
|
|
1
|
+
# src/fleetpull/endpoints/samsara/driver_vehicle_assignments.py
|
|
2
|
+
"""The Samsara driver_vehicle_assignments binding: the fleet-wide
|
|
3
|
+
windowed cursor walk with a fixed ``filterBy`` param -- the
|
|
4
|
+
idling_events species carrying the trips overlap-anchoring decisions.
|
|
5
|
+
|
|
6
|
+
``GET /fleet/driver-vehicle-assignments`` is a modern-envelope surface
|
|
7
|
+
(``data`` + ``pagination {endCursor, hasNextPage}`` -- the standard
|
|
8
|
+
cursor contract). Assignments are fleet-wide with per-record driver AND
|
|
9
|
+
vehicle attribution, so there is NO fan-out -- the default
|
|
10
|
+
``SingleFetch`` shape, declared by declaring nothing.
|
|
11
|
+
|
|
12
|
+
``filterBy`` is REQUIRED and API-enforced to a two-value vocabulary:
|
|
13
|
+
omitting it is HTTP 400, and ``filterBy=bogus`` is HTTP 400 naming the
|
|
14
|
+
vocabulary (``value of filterBy must be one of "drivers", "vehicles"
|
|
15
|
+
but got value "bogus"``) -- loud, never silent-empty. The two sweeps
|
|
16
|
+
are ONE DATASET: full 24-hour walks under ``filterBy=vehicles`` and
|
|
17
|
+
``filterBy=drivers`` returned IDENTICAL row sets (216 = 216, proven
|
|
18
|
+
equal as tuple sets; captured 2026-07-20), so the axis is a traversal
|
|
19
|
+
choice, not a data partition -- one endpoint, ``filterBy=vehicles``
|
|
20
|
+
baked into every request as a FIXED param (the stats triple's ``types``
|
|
21
|
+
builder idiom), no sweep, no second endpoint (DESIGN section 8).
|
|
22
|
+
|
|
23
|
+
The server pages at a FIXED 50 records and the ``limit`` param is
|
|
24
|
+
PROVEN IGNORED: limit=1, 5, 100, 512, 513 -- and no limit at all --
|
|
25
|
+
each returned a 50-record first page with ``hasNextPage: true``, and
|
|
26
|
+
513 was NOT rejected (no enforced tier on this surface, unlike every
|
|
27
|
+
probed sibling). The declared ``results_limit=50`` is
|
|
28
|
+
documentation-by-declaration of the server's OWN observed page size,
|
|
29
|
+
not a working knob.
|
|
30
|
+
|
|
31
|
+
Retrieval is OVERLAP-anchored, probe-proven on adjacent day windows:
|
|
32
|
+
two neighboring 24-hour windows shared 5 midnight-spanning assignments
|
|
33
|
+
(identical tuples in both), and the later window carried 5 rows whose
|
|
34
|
+
``startTime`` precedes the window start plus 2 whose ``endTime`` is
|
|
35
|
+
at/after the window end. Per DESIGN section 4 (the trips decisions,
|
|
36
|
+
mirrored): overlap retrieval supersets start-anchored ownership, so
|
|
37
|
+
``event_time_column='start_time'``, the runner's post-fetch window
|
|
38
|
+
filter assigns each assignment to the single chunk owning its start,
|
|
39
|
+
no wire pad is needed, and wholesale partition replacement handles
|
|
40
|
+
midnight-spanning intervals exactly as it does for trips.
|
|
41
|
+
|
|
42
|
+
No range cap was probed on this surface; the default 7-day chunk
|
|
43
|
+
width is live-proven (a 7-day unit fetched 6,897 records clean,
|
|
44
|
+
2026-07-21).
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
from collections.abc import Mapping
|
|
48
|
+
from dataclasses import dataclass
|
|
49
|
+
from datetime import timedelta
|
|
50
|
+
from typing import Final
|
|
51
|
+
|
|
52
|
+
from fleetpull.config import SamsaraConfig
|
|
53
|
+
from fleetpull.endpoints.shared import (
|
|
54
|
+
EndpointDefinition,
|
|
55
|
+
ResumeValue,
|
|
56
|
+
StorageKind,
|
|
57
|
+
WatermarkMode,
|
|
58
|
+
require_date_window,
|
|
59
|
+
)
|
|
60
|
+
from fleetpull.models.samsara import DriverVehicleAssignment
|
|
61
|
+
from fleetpull.network.contract import HttpMethod, RequestSpec
|
|
62
|
+
from fleetpull.network.decoders import SamsaraCursorPageDecoder
|
|
63
|
+
from fleetpull.timing import to_iso8601
|
|
64
|
+
from fleetpull.vocabulary import Provider, QuotaScope
|
|
65
|
+
|
|
66
|
+
__all__: list[str] = [
|
|
67
|
+
'SamsaraDriverVehicleAssignmentsSpecBuilder',
|
|
68
|
+
'build_endpoint',
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
_ASSIGNMENTS_PATH: Final[str] = '/fleet/driver-vehicle-assignments'
|
|
72
|
+
_RECORDS_KEY: Final[str] = 'data'
|
|
73
|
+
|
|
74
|
+
# The per-page record count. 50 is the server's OWN observed page size,
|
|
75
|
+
# and the `limit` param is PROVEN IGNORED on this surface: limit=1, 5,
|
|
76
|
+
# 100, 512, 513 -- and no limit at all -- each returned a 50-record
|
|
77
|
+
# first page with hasNextPage: true; 513 was NOT rejected (no enforced
|
|
78
|
+
# tier, captured 2026-07-20). Declaring 50 documents the server's
|
|
79
|
+
# paging; it is not a working knob.
|
|
80
|
+
_RESULTS_LIMIT: Final[int] = 50
|
|
81
|
+
|
|
82
|
+
# The traversal-axis selector, REQUIRED and API-enforced on input to a
|
|
83
|
+
# two-value vocabulary (missing -> HTTP 400; filterBy=bogus -> HTTP 400
|
|
84
|
+
# naming 'drivers'/'vehicles'; captured 2026-07-20). Both sweeps return
|
|
85
|
+
# the IDENTICAL row set, so one value is baked into every request --
|
|
86
|
+
# the axis is traversal, not partition (DESIGN section 8).
|
|
87
|
+
_FILTER_BY_PARAM: Final[str] = 'filterBy'
|
|
88
|
+
_FILTER_BY_VALUE: Final[str] = 'vehicles'
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True, slots=True)
|
|
92
|
+
class SamsaraDriverVehicleAssignmentsSpecBuilder:
|
|
93
|
+
"""Build the fleet-wide, date-windowed first request for assignments.
|
|
94
|
+
|
|
95
|
+
The ``SpecBuilder`` for the driver_vehicle_assignments single
|
|
96
|
+
chain: a fixed ``GET base_url + path`` carrying the resume window
|
|
97
|
+
as RFC3339 ``startTime``/``endTime`` (the timing codec's
|
|
98
|
+
``to_iso8601``) plus the FIXED ``filterBy=vehicles`` selector baked
|
|
99
|
+
into every request (the stats triple's ``types`` idiom -- module
|
|
100
|
+
docstring for the one-dataset proof). The decoder owns pagination:
|
|
101
|
+
its ``first_request`` merges ``limit`` onto this spec and its
|
|
102
|
+
``after`` advance merges onto the sent spec, so the window and the
|
|
103
|
+
selector persist across the whole cursor walk.
|
|
104
|
+
|
|
105
|
+
The canonical half-open ``[start, end)`` window maps to the wire as
|
|
106
|
+
``startTime = start`` and ``endTime = end``. Retrieval is
|
|
107
|
+
OVERLAP-anchored (module docstring), so a window also returns
|
|
108
|
+
assignments merely straddling its bounds -- deliberately unpadded:
|
|
109
|
+
overlap retrieval supersets start-anchored ownership, and the
|
|
110
|
+
runner's post-fetch window filter keeps only assignments whose
|
|
111
|
+
start lies in ``[start, end)``, so each assignment lands in exactly
|
|
112
|
+
the one chunk owning its start and boundary straddlers never
|
|
113
|
+
persist twice.
|
|
114
|
+
|
|
115
|
+
Attributes:
|
|
116
|
+
base_url: Root of the Samsara API, trailing-slash-normalized by
|
|
117
|
+
the provider config so the leading-slash path joins
|
|
118
|
+
directly.
|
|
119
|
+
path: The endpoint's leading-slash request path
|
|
120
|
+
(``'/fleet/driver-vehicle-assignments'``).
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
base_url: str
|
|
124
|
+
path: str
|
|
125
|
+
|
|
126
|
+
def build_spec(
|
|
127
|
+
self, resume: ResumeValue, member_values: Mapping[str, str]
|
|
128
|
+
) -> RequestSpec:
|
|
129
|
+
"""Build the fleet-wide, date-windowed, fixed-filter GET.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
resume: The run's resume window. Must be a ``DateWindow`` --
|
|
133
|
+
a watermark endpoint always resumes from one; any other
|
|
134
|
+
value is a wiring bug.
|
|
135
|
+
member_values: Accepted to satisfy the protocol; unused -- a
|
|
136
|
+
fleet-wide single chain binds no member.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
A credential-less ``GET`` for ``base_url + path`` carrying
|
|
140
|
+
``filterBy=vehicles`` and the window's bounds as RFC3339
|
|
141
|
+
``startTime``/``endTime``. Auth headers are layered on by
|
|
142
|
+
the client's ``ProviderProfile``; pagination parameters are
|
|
143
|
+
injected by the page decoder's ``first_request``.
|
|
144
|
+
|
|
145
|
+
Raises:
|
|
146
|
+
TypeError: ``resume`` is not a ``DateWindow``.
|
|
147
|
+
ValueError: A window bound is not canonical UTC.
|
|
148
|
+
|
|
149
|
+
Side Effects:
|
|
150
|
+
None.
|
|
151
|
+
"""
|
|
152
|
+
resume_window = require_date_window(resume, type(self).__name__)
|
|
153
|
+
params = {
|
|
154
|
+
_FILTER_BY_PARAM: _FILTER_BY_VALUE,
|
|
155
|
+
'startTime': to_iso8601(resume_window.start),
|
|
156
|
+
'endTime': to_iso8601(resume_window.end),
|
|
157
|
+
}
|
|
158
|
+
return RequestSpec(
|
|
159
|
+
method=HttpMethod.GET,
|
|
160
|
+
url=f'{self.base_url}{self.path}',
|
|
161
|
+
params=params,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def build_endpoint(
|
|
166
|
+
config: SamsaraConfig,
|
|
167
|
+
) -> EndpointDefinition[DriverVehicleAssignment]:
|
|
168
|
+
"""Build the Samsara driver_vehicle_assignments watermark binding.
|
|
169
|
+
|
|
170
|
+
Fleet-wide driver-vehicle assignment intervals fetched
|
|
171
|
+
incrementally: the run resumes from a ``DateWindow`` (watermark
|
|
172
|
+
with the provider's late-arrival lookback from config), the fetched
|
|
173
|
+
assignments are written to ``date=YYYY-MM-DD`` partitions on
|
|
174
|
+
``start_time``, and each refetched partition is replaced --
|
|
175
|
+
absorbing the overlap-retrieved midnight straddlers exactly as
|
|
176
|
+
trips does (module docstring). Records arrive as a top-level list
|
|
177
|
+
under ``data``, walked by explicit cursor pages (``limit`` on page
|
|
178
|
+
one, ``after`` merged thereafter, the window and ``filterBy``
|
|
179
|
+
persisting throughout), terminal on ``hasNextPage: false``. No
|
|
180
|
+
request shape is declared -- the endpoint is a fleet-wide
|
|
181
|
+
``SingleFetch``, the default.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
config: The validated Samsara configuration; supplies the base
|
|
185
|
+
URL the spec-builder joins to the assignments path and the
|
|
186
|
+
lookback and cutoff the watermark mode carries.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
The frozen driver_vehicle_assignments ``EndpointDefinition``.
|
|
190
|
+
Construction validates the ``WatermarkMode`` /
|
|
191
|
+
``DATE_PARTITIONED`` / ``event_time_column`` triple against the
|
|
192
|
+
response model.
|
|
193
|
+
"""
|
|
194
|
+
return EndpointDefinition(
|
|
195
|
+
provider=Provider.SAMSARA,
|
|
196
|
+
name='driver_vehicle_assignments',
|
|
197
|
+
spec_builder=SamsaraDriverVehicleAssignmentsSpecBuilder(
|
|
198
|
+
base_url=config.base_url, path=_ASSIGNMENTS_PATH
|
|
199
|
+
),
|
|
200
|
+
page_decoder=SamsaraCursorPageDecoder(
|
|
201
|
+
records_key=_RECORDS_KEY, results_limit=_RESULTS_LIMIT
|
|
202
|
+
),
|
|
203
|
+
response_model=DriverVehicleAssignment,
|
|
204
|
+
quota_scope=QuotaScope.SAMSARA,
|
|
205
|
+
storage_kind=StorageKind.DATE_PARTITIONED,
|
|
206
|
+
sync_mode=WatermarkMode(
|
|
207
|
+
lookback=timedelta(days=config.lookback_days),
|
|
208
|
+
cutoff=timedelta(days=config.cutoff_days),
|
|
209
|
+
),
|
|
210
|
+
event_time_column='start_time',
|
|
211
|
+
)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# src/fleetpull/endpoints/samsara/drivers.py
|
|
2
|
+
"""The Samsara drivers binding: the two-sweep activation-status snapshot,
|
|
3
|
+
the first ``ParamSweep`` consumer.
|
|
4
|
+
|
|
5
|
+
The default ``/fleet/drivers`` listing IS the active set exactly -- the
|
|
6
|
+
2026-07-20 probe matched it record-for-record against
|
|
7
|
+
``driverActivationStatus=active`` (460 ids, identical) -- while the
|
|
8
|
+
deactivated sweep returned 372 fully disjoint records INVISIBLE to the
|
|
9
|
+
default listing: 45% of the 832-driver population. The one complete
|
|
10
|
+
driver dataset is therefore the union of both sweeps, so this binding
|
|
11
|
+
declares ``request_shape=ParamSweep`` over the two statuses; the shared
|
|
12
|
+
shape-resolution seam fans one request chain per value through the
|
|
13
|
+
member-agnostic ``FanOutRequestDriver``, and the
|
|
14
|
+
``driver_activation_status`` column carries the split in the one stored
|
|
15
|
+
dataset.
|
|
16
|
+
|
|
17
|
+
No completeness check is declared, on two proofs. Continuation is
|
|
18
|
+
explicit per page (the vehicles cursor contract, proven per-type on
|
|
19
|
+
drivers: a limit=5 walk of 92 pages returned 460/460 unique ascending
|
|
20
|
+
ids with no boundary overlap or loss), and the sweep vocabulary is
|
|
21
|
+
API-enforced: ``driverActivationStatus`` is a strict closed enum whose
|
|
22
|
+
every probed variant -- case changes, comma-joins, repeated keys, bogus
|
|
23
|
+
values -- returned HTTP 400 naming the two admissible values, loudly,
|
|
24
|
+
never a silent empty listing. A typo'd sweep value can therefore never
|
|
25
|
+
masquerade as an empty partition.
|
|
26
|
+
|
|
27
|
+
The existing ``SamsaraCursorPageDecoder`` needed NO change for the
|
|
28
|
+
sweep: its advance merges ``after`` onto the SENT spec, so a
|
|
29
|
+
first-request query parameter persists across the whole walk -- proven
|
|
30
|
+
live by a limit=50 deactivated walk (8 pages, 50x7+22, 372/372 unique,
|
|
31
|
+
every record deactivated, a fresh cursor per page, the standard
|
|
32
|
+
terminal). The spec builder below exists because the shared
|
|
33
|
+
``StaticGetSpecBuilder`` deliberately ignores ``member_values`` (a
|
|
34
|
+
single-chain snapshot binds no member); a sweep chain binds one query
|
|
35
|
+
parameter, so this endpoint's builder merges the member binding
|
|
36
|
+
verbatim as query parameters (the Motive vehicle_locations
|
|
37
|
+
leaf-builder precedent, which renders its member into the URL path
|
|
38
|
+
instead).
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from collections.abc import Mapping
|
|
42
|
+
from dataclasses import dataclass
|
|
43
|
+
from typing import Final
|
|
44
|
+
|
|
45
|
+
from fleetpull.config import SamsaraConfig
|
|
46
|
+
from fleetpull.endpoints.shared import (
|
|
47
|
+
EndpointDefinition,
|
|
48
|
+
ParamSweep,
|
|
49
|
+
ResumeValue,
|
|
50
|
+
SnapshotMode,
|
|
51
|
+
StorageKind,
|
|
52
|
+
)
|
|
53
|
+
from fleetpull.models.samsara import Driver
|
|
54
|
+
from fleetpull.network.contract import HttpMethod, RequestSpec
|
|
55
|
+
from fleetpull.network.decoders import SamsaraCursorPageDecoder
|
|
56
|
+
from fleetpull.vocabulary import Provider, QuotaScope
|
|
57
|
+
|
|
58
|
+
__all__: list[str] = [
|
|
59
|
+
'SamsaraDriversSpecBuilder',
|
|
60
|
+
'build_endpoint',
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
_DRIVERS_PATH: Final[str] = '/fleet/drivers'
|
|
64
|
+
_RECORDS_KEY: Final[str] = 'data'
|
|
65
|
+
|
|
66
|
+
# The per-page record count. 512 is Samsara's documented list-endpoint
|
|
67
|
+
# maximum, accepted on /fleet/drivers (captured 2026-07-20); the cursor
|
|
68
|
+
# mechanics behind it were proven per-type by a limit=5 walk -- 92
|
|
69
|
+
# pages, 460/460 unique ascending ids, no boundary overlap or loss.
|
|
70
|
+
_RESULTS_LIMIT: Final[int] = 512
|
|
71
|
+
|
|
72
|
+
# The sweep's member key IS the wire query parameter, verbatim: the spec
|
|
73
|
+
# builder merges member_values into params unchanged, so declaring the
|
|
74
|
+
# exact wire token here leaves no translation seam to drift. The value
|
|
75
|
+
# set is API-closed -- any other value is a loud HTTP 400 (captured
|
|
76
|
+
# 2026-07-20), never a silent empty listing.
|
|
77
|
+
_ACTIVATION_STATUS_PARAM: Final[str] = 'driverActivationStatus'
|
|
78
|
+
_ACTIVATION_STATUS_VALUES: Final[tuple[str, ...]] = ('active', 'deactivated')
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True, slots=True)
|
|
82
|
+
class SamsaraDriversSpecBuilder:
|
|
83
|
+
"""Build the per-sweep first request for drivers.
|
|
84
|
+
|
|
85
|
+
The ``SpecBuilder`` for the drivers ``ParamSweep``: a fixed
|
|
86
|
+
``GET base_url + path`` carrying the chain's member binding as query
|
|
87
|
+
parameters, verbatim -- each sweep chain's ``member_values`` is
|
|
88
|
+
``{'driverActivationStatus': <value>}``, and the decoder's ``after``
|
|
89
|
+
advance merges onto this spec, so the status parameter persists
|
|
90
|
+
across every page of the chain's walk (module docstring). Exists
|
|
91
|
+
because the shared ``StaticGetSpecBuilder`` deliberately ignores
|
|
92
|
+
``member_values``; this endpoint's member IS a query parameter.
|
|
93
|
+
|
|
94
|
+
Attributes:
|
|
95
|
+
base_url: Root of the Samsara API, trailing-slash-normalized by
|
|
96
|
+
the provider config so the leading-slash path joins
|
|
97
|
+
directly.
|
|
98
|
+
path: The endpoint's leading-slash request path
|
|
99
|
+
(``'/fleet/drivers'``).
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
base_url: str
|
|
103
|
+
path: str
|
|
104
|
+
|
|
105
|
+
def build_spec(
|
|
106
|
+
self, resume: ResumeValue, member_values: Mapping[str, str]
|
|
107
|
+
) -> RequestSpec:
|
|
108
|
+
"""Build one sweep chain's first request.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
resume: Accepted to satisfy the protocol; unused -- a
|
|
112
|
+
snapshot resumes from nothing.
|
|
113
|
+
member_values: The chain's member binding, merged verbatim
|
|
114
|
+
as query parameters -- ``{'driverActivationStatus':
|
|
115
|
+
<value>}`` for a sweep chain.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
A credential-less ``GET`` for ``base_url + path`` carrying
|
|
119
|
+
the member binding as query parameters. Auth headers are
|
|
120
|
+
layered on by the client's ``ProviderProfile``; pagination
|
|
121
|
+
parameters are injected by the page decoder's
|
|
122
|
+
``first_request``.
|
|
123
|
+
|
|
124
|
+
Side Effects:
|
|
125
|
+
None.
|
|
126
|
+
"""
|
|
127
|
+
return RequestSpec(
|
|
128
|
+
method=HttpMethod.GET,
|
|
129
|
+
url=f'{self.base_url}{self.path}',
|
|
130
|
+
params=dict(member_values),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def build_endpoint(config: SamsaraConfig) -> EndpointDefinition[Driver]:
|
|
135
|
+
"""Build the Samsara drivers two-sweep snapshot binding.
|
|
136
|
+
|
|
137
|
+
A full-listing snapshot of the fleet's drivers, complete only as the
|
|
138
|
+
union of the two activation-status sweeps (module docstring): the
|
|
139
|
+
declared ``ParamSweep`` fans one cursor-walked chain per status, and
|
|
140
|
+
every run fully replaces the single parquet file with that union.
|
|
141
|
+
Records arrive as a top-level list under ``data``, walked by
|
|
142
|
+
explicit cursor pages (``limit`` on page one, ``after`` merged
|
|
143
|
+
thereafter, the status parameter persisting throughout), terminal on
|
|
144
|
+
``hasNextPage: false``.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
config: The validated Samsara configuration; supplies the base
|
|
148
|
+
URL the spec-builder joins to the drivers path.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
The frozen drivers ``EndpointDefinition``.
|
|
152
|
+
"""
|
|
153
|
+
return EndpointDefinition(
|
|
154
|
+
provider=Provider.SAMSARA,
|
|
155
|
+
name='drivers',
|
|
156
|
+
spec_builder=SamsaraDriversSpecBuilder(
|
|
157
|
+
base_url=config.base_url, path=_DRIVERS_PATH
|
|
158
|
+
),
|
|
159
|
+
page_decoder=SamsaraCursorPageDecoder(
|
|
160
|
+
records_key=_RECORDS_KEY, results_limit=_RESULTS_LIMIT
|
|
161
|
+
),
|
|
162
|
+
response_model=Driver,
|
|
163
|
+
quota_scope=QuotaScope.SAMSARA,
|
|
164
|
+
storage_kind=StorageKind.SINGLE,
|
|
165
|
+
sync_mode=SnapshotMode(),
|
|
166
|
+
request_shape=ParamSweep(
|
|
167
|
+
param=_ACTIVATION_STATUS_PARAM, values=_ACTIVATION_STATUS_VALUES
|
|
168
|
+
),
|
|
169
|
+
)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# src/fleetpull/endpoints/samsara/engine_states.py
|
|
2
|
+
"""The Samsara engine_states binding: the vehicle-stats windowed cursor
|
|
3
|
+
walk for ``types=engineStates`` -- one of the three endpoints the legacy
|
|
4
|
+
``/fleet/vehicles/stats/history`` surface splits into (engine_states,
|
|
5
|
+
gps_readings, odometer_readings; the three stat types carry disjoint
|
|
6
|
+
schemas, so each is its own entity and dataset -- DESIGN §8).
|
|
7
|
+
|
|
8
|
+
``GET /fleet/vehicles/stats/history`` is a modern-envelope surface
|
|
9
|
+
(``data`` + ``pagination {endCursor, hasNextPage}``), but its cursor
|
|
10
|
+
walks the VEHICLE axis within the fixed window: three consecutive live
|
|
11
|
+
pages showed zero vehicle-id overlap (captured 2026-07-20). Each
|
|
12
|
+
vehicle record nests one reading series under the requested type's key,
|
|
13
|
+
so the binding pairs the shared windowed builder (which bakes the FIXED
|
|
14
|
+
``types=engineStates`` selector into every request -- the ``types``
|
|
15
|
+
vocabulary is API-enforced on input, a loud 400 on any unknown value)
|
|
16
|
+
with ``SamsaraVehicleSeriesPageDecoder``, which unnests each vehicle's
|
|
17
|
+
series into flat per-reading records and delegates pagination to the
|
|
18
|
+
inner cursor decoder untouched.
|
|
19
|
+
|
|
20
|
+
Only carrier vehicles are returned per requested type (24-hour walk:
|
|
21
|
+
138 vehicles, 138 with data -- no empty-array padding observed), and
|
|
22
|
+
the endpoint is fleet-wide with per-record vehicle attribution
|
|
23
|
+
synthesized by the decoder, so there is NO fan-out -- the default
|
|
24
|
+
``SingleFetch`` shape, declared by declaring nothing, and no roster is
|
|
25
|
+
sourced or consumed.
|
|
26
|
+
|
|
27
|
+
The per-endpoint ``limit`` maximum is 512, probed directly on THIS
|
|
28
|
+
surface: limit=512 returned HTTP 200 and limit=513 a loud HTTP 400 --
|
|
29
|
+
the vehicles/drivers tier, NOT idling's 200 (the per-endpoint
|
|
30
|
+
limit-tier rule, honored by probing rather than assuming).
|
|
31
|
+
|
|
32
|
+
Retrieval is READING-TIME anchored on the half-open ``[startTime,
|
|
33
|
+
endTime)`` window, probe-proven: a 12:00-13:00Z window returned
|
|
34
|
+
readings spanning exactly 12:00:03.062Z..12:59:56.881Z. Consequence:
|
|
35
|
+
``event_time_column='time'``, the retrieval anchor and the routing
|
|
36
|
+
anchor coincide natively, no wire pad exists, and the runner's
|
|
37
|
+
post-fetch window filter is pure hygiene.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from datetime import timedelta
|
|
41
|
+
from typing import Final
|
|
42
|
+
|
|
43
|
+
from fleetpull.config import SamsaraConfig
|
|
44
|
+
from fleetpull.endpoints.samsara._spec_builders import (
|
|
45
|
+
RECORDS_KEY,
|
|
46
|
+
RESULTS_LIMIT,
|
|
47
|
+
STATS_HISTORY_PATH,
|
|
48
|
+
SamsaraVehicleStatsSpecBuilder,
|
|
49
|
+
)
|
|
50
|
+
from fleetpull.endpoints.shared import (
|
|
51
|
+
EndpointDefinition,
|
|
52
|
+
StorageKind,
|
|
53
|
+
WatermarkMode,
|
|
54
|
+
)
|
|
55
|
+
from fleetpull.models.samsara import EngineState
|
|
56
|
+
from fleetpull.network.decoders import SamsaraVehicleSeriesPageDecoder
|
|
57
|
+
from fleetpull.vocabulary import Provider, QuotaScope
|
|
58
|
+
|
|
59
|
+
__all__: list[str] = ['build_endpoint']
|
|
60
|
+
|
|
61
|
+
# The requested stat type -- the verbatim `types` wire value AND the
|
|
62
|
+
# per-vehicle series key the decoder unnests (one name on the wire for
|
|
63
|
+
# both roles, captured 2026-07-20).
|
|
64
|
+
_STAT_TYPE: Final[str] = 'engineStates'
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def build_endpoint(config: SamsaraConfig) -> EndpointDefinition[EngineState]:
|
|
68
|
+
"""Build the Samsara engine_states watermark binding.
|
|
69
|
+
|
|
70
|
+
Fleet-wide engine-state readings fetched incrementally: the run
|
|
71
|
+
resumes from a ``DateWindow`` (watermark with the provider's
|
|
72
|
+
late-arrival lookback from config), the fetched readings are
|
|
73
|
+
written to ``date=YYYY-MM-DD`` partitions on ``time``, and each
|
|
74
|
+
refetched partition is replaced. Vehicle records arrive as a
|
|
75
|
+
top-level list under ``data``, walked by explicit cursor pages
|
|
76
|
+
along the VEHICLE axis (``limit`` on page one, ``after`` merged
|
|
77
|
+
thereafter, the window and ``types`` parameters persisting
|
|
78
|
+
throughout), and the decoder unnests each vehicle's
|
|
79
|
+
``engineStates`` series into one flat record per reading. No
|
|
80
|
+
request shape is declared -- the endpoint is a fleet-wide
|
|
81
|
+
``SingleFetch``, the default.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
config: The validated Samsara configuration; supplies the base
|
|
85
|
+
URL the spec-builder joins to the stats-history path and
|
|
86
|
+
the lookback and cutoff the watermark mode carries.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
The frozen engine_states ``EndpointDefinition``. Construction
|
|
90
|
+
validates the ``WatermarkMode`` / ``DATE_PARTITIONED`` /
|
|
91
|
+
``event_time_column`` triple against the response model.
|
|
92
|
+
"""
|
|
93
|
+
return EndpointDefinition(
|
|
94
|
+
provider=Provider.SAMSARA,
|
|
95
|
+
name='engine_states',
|
|
96
|
+
spec_builder=SamsaraVehicleStatsSpecBuilder(
|
|
97
|
+
base_url=config.base_url,
|
|
98
|
+
path=STATS_HISTORY_PATH,
|
|
99
|
+
stat_type=_STAT_TYPE,
|
|
100
|
+
),
|
|
101
|
+
page_decoder=SamsaraVehicleSeriesPageDecoder(
|
|
102
|
+
records_key=RECORDS_KEY,
|
|
103
|
+
results_limit=RESULTS_LIMIT,
|
|
104
|
+
series_key=_STAT_TYPE,
|
|
105
|
+
),
|
|
106
|
+
response_model=EngineState,
|
|
107
|
+
quota_scope=QuotaScope.SAMSARA,
|
|
108
|
+
storage_kind=StorageKind.DATE_PARTITIONED,
|
|
109
|
+
sync_mode=WatermarkMode(
|
|
110
|
+
lookback=timedelta(days=config.lookback_days),
|
|
111
|
+
cutoff=timedelta(days=config.cutoff_days),
|
|
112
|
+
),
|
|
113
|
+
event_time_column='time',
|
|
114
|
+
)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# src/fleetpull/endpoints/samsara/gps_readings.py
|
|
2
|
+
"""The Samsara gps_readings binding: the vehicle-stats windowed cursor
|
|
3
|
+
walk for ``types=gps`` -- one of the three endpoints the legacy
|
|
4
|
+
``/fleet/vehicles/stats/history`` surface splits into (engine_states,
|
|
5
|
+
gps_readings, odometer_readings; the three stat types carry disjoint
|
|
6
|
+
schemas, so each is its own entity and dataset -- DESIGN §8).
|
|
7
|
+
|
|
8
|
+
``GET /fleet/vehicles/stats/history`` is a modern-envelope surface
|
|
9
|
+
(``data`` + ``pagination {endCursor, hasNextPage}``), but its cursor
|
|
10
|
+
walks the VEHICLE axis within the fixed window: three consecutive live
|
|
11
|
+
pages showed zero vehicle-id overlap (captured 2026-07-20). Each
|
|
12
|
+
vehicle record nests one reading series under the requested type's key,
|
|
13
|
+
so the binding pairs the shared windowed builder (which bakes the FIXED
|
|
14
|
+
``types=gps`` selector into every request -- the ``types`` vocabulary
|
|
15
|
+
is API-enforced on input, a loud 400 on any unknown value) with
|
|
16
|
+
``SamsaraVehicleSeriesPageDecoder``, which unnests each vehicle's
|
|
17
|
+
series into flat per-reading records and delegates pagination to the
|
|
18
|
+
inner cursor decoder untouched.
|
|
19
|
+
|
|
20
|
+
Only carrier vehicles are returned per requested type (the 24-hour gps
|
|
21
|
+
sample: 569 vehicles, 569 with data -- no empty-array padding
|
|
22
|
+
observed), and the endpoint is fleet-wide with per-record vehicle
|
|
23
|
+
attribution synthesized by the decoder, so there is NO fan-out -- the
|
|
24
|
+
default ``SingleFetch`` shape, declared by declaring nothing, and no
|
|
25
|
+
roster is sourced or consumed.
|
|
26
|
+
|
|
27
|
+
The per-endpoint ``limit`` maximum is 512, probed directly on THIS
|
|
28
|
+
surface: limit=512 returned HTTP 200 and limit=513 a loud HTTP 400 --
|
|
29
|
+
the vehicles/drivers tier, NOT idling's 200 (the per-endpoint
|
|
30
|
+
limit-tier rule, honored by probing rather than assuming).
|
|
31
|
+
|
|
32
|
+
Retrieval is READING-TIME anchored on the half-open ``[startTime,
|
|
33
|
+
endTime)`` window, probe-proven: a 12:00-13:00Z window returned
|
|
34
|
+
readings spanning exactly 12:00:03.062Z..12:59:56.881Z. Consequence:
|
|
35
|
+
``event_time_column='time'``, the retrieval anchor and the routing
|
|
36
|
+
anchor coincide natively, no wire pad exists, and the runner's
|
|
37
|
+
post-fetch window filter is pure hygiene.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from datetime import timedelta
|
|
41
|
+
from typing import Final
|
|
42
|
+
|
|
43
|
+
from fleetpull.config import SamsaraConfig
|
|
44
|
+
from fleetpull.endpoints.samsara._spec_builders import (
|
|
45
|
+
RECORDS_KEY,
|
|
46
|
+
RESULTS_LIMIT,
|
|
47
|
+
STATS_HISTORY_PATH,
|
|
48
|
+
SamsaraVehicleStatsSpecBuilder,
|
|
49
|
+
)
|
|
50
|
+
from fleetpull.endpoints.shared import (
|
|
51
|
+
EndpointDefinition,
|
|
52
|
+
StorageKind,
|
|
53
|
+
WatermarkMode,
|
|
54
|
+
)
|
|
55
|
+
from fleetpull.models.samsara import GpsReading
|
|
56
|
+
from fleetpull.network.decoders import SamsaraVehicleSeriesPageDecoder
|
|
57
|
+
from fleetpull.vocabulary import Provider, QuotaScope
|
|
58
|
+
|
|
59
|
+
__all__: list[str] = ['build_endpoint']
|
|
60
|
+
|
|
61
|
+
# The requested stat type -- the verbatim `types` wire value AND the
|
|
62
|
+
# per-vehicle series key the decoder unnests (one name on the wire for
|
|
63
|
+
# both roles, captured 2026-07-20).
|
|
64
|
+
_STAT_TYPE: Final[str] = 'gps'
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def build_endpoint(config: SamsaraConfig) -> EndpointDefinition[GpsReading]:
|
|
68
|
+
"""Build the Samsara gps_readings watermark binding.
|
|
69
|
+
|
|
70
|
+
Fleet-wide GPS readings fetched incrementally: the run resumes
|
|
71
|
+
from a ``DateWindow`` (watermark with the provider's late-arrival
|
|
72
|
+
lookback from config), the fetched readings are written to
|
|
73
|
+
``date=YYYY-MM-DD`` partitions on ``time``, and each refetched
|
|
74
|
+
partition is replaced. Vehicle records arrive as a top-level list
|
|
75
|
+
under ``data``, walked by explicit cursor pages along the VEHICLE
|
|
76
|
+
axis (``limit`` on page one, ``after`` merged thereafter, the
|
|
77
|
+
window and ``types`` parameters persisting throughout), and the
|
|
78
|
+
decoder unnests each vehicle's ``gps`` series into one flat record
|
|
79
|
+
per reading. No request shape is declared -- the endpoint is a
|
|
80
|
+
fleet-wide ``SingleFetch``, the default.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
config: The validated Samsara configuration; supplies the base
|
|
84
|
+
URL the spec-builder joins to the stats-history path and
|
|
85
|
+
the lookback and cutoff the watermark mode carries.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
The frozen gps_readings ``EndpointDefinition``. Construction
|
|
89
|
+
validates the ``WatermarkMode`` / ``DATE_PARTITIONED`` /
|
|
90
|
+
``event_time_column`` triple against the response model.
|
|
91
|
+
"""
|
|
92
|
+
return EndpointDefinition(
|
|
93
|
+
provider=Provider.SAMSARA,
|
|
94
|
+
name='gps_readings',
|
|
95
|
+
spec_builder=SamsaraVehicleStatsSpecBuilder(
|
|
96
|
+
base_url=config.base_url,
|
|
97
|
+
path=STATS_HISTORY_PATH,
|
|
98
|
+
stat_type=_STAT_TYPE,
|
|
99
|
+
),
|
|
100
|
+
page_decoder=SamsaraVehicleSeriesPageDecoder(
|
|
101
|
+
records_key=RECORDS_KEY,
|
|
102
|
+
results_limit=RESULTS_LIMIT,
|
|
103
|
+
series_key=_STAT_TYPE,
|
|
104
|
+
),
|
|
105
|
+
response_model=GpsReading,
|
|
106
|
+
quota_scope=QuotaScope.SAMSARA,
|
|
107
|
+
storage_kind=StorageKind.DATE_PARTITIONED,
|
|
108
|
+
sync_mode=WatermarkMode(
|
|
109
|
+
lookback=timedelta(days=config.lookback_days),
|
|
110
|
+
cutoff=timedelta(days=config.cutoff_days),
|
|
111
|
+
),
|
|
112
|
+
event_time_column='time',
|
|
113
|
+
)
|