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.
Files changed (252) hide show
  1. fleetpull/__init__.py +36 -0
  2. fleetpull/api/__init__.py +32 -0
  3. fleetpull/api/auth_ingress.py +216 -0
  4. fleetpull/api/catalog.py +139 -0
  5. fleetpull/api/fetch.py +229 -0
  6. fleetpull/api/identity.py +90 -0
  7. fleetpull/api/sync.py +735 -0
  8. fleetpull/cli.py +146 -0
  9. fleetpull/config/__init__.py +47 -0
  10. fleetpull/config/base.py +18 -0
  11. fleetpull/config/example.py +90 -0
  12. fleetpull/config/geotab.py +78 -0
  13. fleetpull/config/http.py +32 -0
  14. fleetpull/config/loading.py +195 -0
  15. fleetpull/config/logger.py +139 -0
  16. fleetpull/config/providers.py +520 -0
  17. fleetpull/config/rate_limit.py +50 -0
  18. fleetpull/config/resolution.py +156 -0
  19. fleetpull/config/retry.py +69 -0
  20. fleetpull/config/root.py +157 -0
  21. fleetpull/config/sections.py +146 -0
  22. fleetpull/endpoints/__init__.py +24 -0
  23. fleetpull/endpoints/geotab/__init__.py +12 -0
  24. fleetpull/endpoints/geotab/_requests.py +427 -0
  25. fleetpull/endpoints/geotab/annotation_logs.py +73 -0
  26. fleetpull/endpoints/geotab/audits.py +70 -0
  27. fleetpull/endpoints/geotab/devices.py +79 -0
  28. fleetpull/endpoints/geotab/driver_changes.py +73 -0
  29. fleetpull/endpoints/geotab/duty_status_logs.py +75 -0
  30. fleetpull/endpoints/geotab/dvir_logs.py +76 -0
  31. fleetpull/endpoints/geotab/exception_events.py +119 -0
  32. fleetpull/endpoints/geotab/fault_data.py +72 -0
  33. fleetpull/endpoints/geotab/fill_ups.py +79 -0
  34. fleetpull/endpoints/geotab/fuel_and_energy_used.py +74 -0
  35. fleetpull/endpoints/geotab/fuel_tax_details.py +75 -0
  36. fleetpull/endpoints/geotab/log_records.py +74 -0
  37. fleetpull/endpoints/geotab/media_files.py +74 -0
  38. fleetpull/endpoints/geotab/shipment_logs.py +71 -0
  39. fleetpull/endpoints/geotab/status_data.py +71 -0
  40. fleetpull/endpoints/geotab/text_messages.py +77 -0
  41. fleetpull/endpoints/geotab/trips.py +102 -0
  42. fleetpull/endpoints/geotab/users.py +81 -0
  43. fleetpull/endpoints/motive/__init__.py +12 -0
  44. fleetpull/endpoints/motive/_spec_builders.py +94 -0
  45. fleetpull/endpoints/motive/driver_idle_rollups.py +113 -0
  46. fleetpull/endpoints/motive/driving_periods.py +85 -0
  47. fleetpull/endpoints/motive/groups.py +61 -0
  48. fleetpull/endpoints/motive/idle_events.py +94 -0
  49. fleetpull/endpoints/motive/users.py +64 -0
  50. fleetpull/endpoints/motive/vehicle_locations.py +170 -0
  51. fleetpull/endpoints/motive/vehicle_utilizations.py +122 -0
  52. fleetpull/endpoints/motive/vehicles.py +106 -0
  53. fleetpull/endpoints/registry.py +280 -0
  54. fleetpull/endpoints/samsara/__init__.py +12 -0
  55. fleetpull/endpoints/samsara/_spec_builders.py +202 -0
  56. fleetpull/endpoints/samsara/addresses.py +77 -0
  57. fleetpull/endpoints/samsara/asset_locations.py +217 -0
  58. fleetpull/endpoints/samsara/driver_fuel_energy_reports.py +124 -0
  59. fleetpull/endpoints/samsara/driver_vehicle_assignments.py +211 -0
  60. fleetpull/endpoints/samsara/drivers.py +169 -0
  61. fleetpull/endpoints/samsara/engine_states.py +114 -0
  62. fleetpull/endpoints/samsara/gps_readings.py +113 -0
  63. fleetpull/endpoints/samsara/idling_events.py +195 -0
  64. fleetpull/endpoints/samsara/odometer_readings.py +116 -0
  65. fleetpull/endpoints/samsara/trips.py +217 -0
  66. fleetpull/endpoints/samsara/vehicle_fuel_energy_reports.py +139 -0
  67. fleetpull/endpoints/samsara/vehicles.py +121 -0
  68. fleetpull/endpoints/shared/__init__.py +57 -0
  69. fleetpull/endpoints/shared/base.py +529 -0
  70. fleetpull/endpoints/shared/request_shape.py +227 -0
  71. fleetpull/endpoints/shared/resume.py +74 -0
  72. fleetpull/endpoints/shared/spec_builders.py +59 -0
  73. fleetpull/endpoints/shared/sync_mode.py +159 -0
  74. fleetpull/endpoints/shared/url_paths.py +149 -0
  75. fleetpull/exceptions.py +320 -0
  76. fleetpull/incremental/__init__.py +27 -0
  77. fleetpull/incremental/cursor.py +66 -0
  78. fleetpull/incremental/resolution.py +152 -0
  79. fleetpull/incremental/seed.py +53 -0
  80. fleetpull/incremental/window.py +94 -0
  81. fleetpull/logger/__init__.py +5 -0
  82. fleetpull/logger/setup.py +145 -0
  83. fleetpull/model_contract/__init__.py +6 -0
  84. fleetpull/model_contract/coercions.py +33 -0
  85. fleetpull/model_contract/response.py +45 -0
  86. fleetpull/models/__init__.py +7 -0
  87. fleetpull/models/geotab/__init__.py +166 -0
  88. fleetpull/models/geotab/annotation_log.py +109 -0
  89. fleetpull/models/geotab/audit.py +56 -0
  90. fleetpull/models/geotab/device.py +217 -0
  91. fleetpull/models/geotab/driver_change.py +102 -0
  92. fleetpull/models/geotab/duty_status_log.py +200 -0
  93. fleetpull/models/geotab/dvir_log.py +184 -0
  94. fleetpull/models/geotab/exception_event.py +135 -0
  95. fleetpull/models/geotab/fault_data.py +168 -0
  96. fleetpull/models/geotab/fill_up.py +189 -0
  97. fleetpull/models/geotab/fuel_and_energy_used.py +81 -0
  98. fleetpull/models/geotab/fuel_tax_detail.py +150 -0
  99. fleetpull/models/geotab/log_record.py +68 -0
  100. fleetpull/models/geotab/media_file.py +133 -0
  101. fleetpull/models/geotab/shared.py +221 -0
  102. fleetpull/models/geotab/shipment_log.py +105 -0
  103. fleetpull/models/geotab/status_data.py +108 -0
  104. fleetpull/models/geotab/text_message.py +125 -0
  105. fleetpull/models/geotab/trip.py +162 -0
  106. fleetpull/models/geotab/user.py +187 -0
  107. fleetpull/models/motive/__init__.py +43 -0
  108. fleetpull/models/motive/driver_idle_rollup.py +109 -0
  109. fleetpull/models/motive/driving_period.py +82 -0
  110. fleetpull/models/motive/group.py +49 -0
  111. fleetpull/models/motive/idle_event.py +77 -0
  112. fleetpull/models/motive/shared.py +192 -0
  113. fleetpull/models/motive/user.py +217 -0
  114. fleetpull/models/motive/vehicle.py +162 -0
  115. fleetpull/models/motive/vehicle_location.py +127 -0
  116. fleetpull/models/motive/vehicle_utilization.py +128 -0
  117. fleetpull/models/samsara/__init__.py +108 -0
  118. fleetpull/models/samsara/address.py +149 -0
  119. fleetpull/models/samsara/asset_location.py +131 -0
  120. fleetpull/models/samsara/driver.py +232 -0
  121. fleetpull/models/samsara/driver_fuel_energy_report.py +153 -0
  122. fleetpull/models/samsara/driver_vehicle_assignment.py +168 -0
  123. fleetpull/models/samsara/engine_state.py +92 -0
  124. fleetpull/models/samsara/gps_reading.py +146 -0
  125. fleetpull/models/samsara/idling_event.py +174 -0
  126. fleetpull/models/samsara/odometer_reading.py +90 -0
  127. fleetpull/models/samsara/trip.py +199 -0
  128. fleetpull/models/samsara/vehicle.py +167 -0
  129. fleetpull/models/samsara/vehicle_fuel_energy_report.py +191 -0
  130. fleetpull/network/__init__.py +9 -0
  131. fleetpull/network/auth/__init__.py +15 -0
  132. fleetpull/network/auth/authenticate.py +271 -0
  133. fleetpull/network/auth/manager.py +223 -0
  134. fleetpull/network/auth/models.py +57 -0
  135. fleetpull/network/auth/strategies.py +169 -0
  136. fleetpull/network/classifiers/__init__.py +11 -0
  137. fleetpull/network/classifiers/geotab.py +145 -0
  138. fleetpull/network/classifiers/motive.py +79 -0
  139. fleetpull/network/classifiers/samsara.py +80 -0
  140. fleetpull/network/client/__init__.py +25 -0
  141. fleetpull/network/client/page.py +32 -0
  142. fleetpull/network/client/profile.py +27 -0
  143. fleetpull/network/client/registry.py +103 -0
  144. fleetpull/network/client/registry_base.py +150 -0
  145. fleetpull/network/client/runtime.py +44 -0
  146. fleetpull/network/client/transport.py +381 -0
  147. fleetpull/network/contract/__init__.py +50 -0
  148. fleetpull/network/contract/auth.py +48 -0
  149. fleetpull/network/contract/classifier.py +189 -0
  150. fleetpull/network/contract/envelope_fetcher.py +33 -0
  151. fleetpull/network/contract/envelopes.py +189 -0
  152. fleetpull/network/contract/outcome.py +43 -0
  153. fleetpull/network/contract/page_decoder.py +101 -0
  154. fleetpull/network/contract/request.py +97 -0
  155. fleetpull/network/decoders/__init__.py +40 -0
  156. fleetpull/network/decoders/_window_stamp.py +78 -0
  157. fleetpull/network/decoders/geotab.py +309 -0
  158. fleetpull/network/decoders/motive.py +204 -0
  159. fleetpull/network/decoders/motive_reports.py +114 -0
  160. fleetpull/network/decoders/samsara.py +343 -0
  161. fleetpull/network/decoders/samsara_reports.py +119 -0
  162. fleetpull/network/decoders/single_page.py +55 -0
  163. fleetpull/network/limits/__init__.py +13 -0
  164. fleetpull/network/limits/bucket_math.py +58 -0
  165. fleetpull/network/limits/limiter.py +153 -0
  166. fleetpull/network/limits/registry.py +96 -0
  167. fleetpull/network/posture/__init__.py +6 -0
  168. fleetpull/network/posture/client_options.py +96 -0
  169. fleetpull/network/retry/__init__.py +13 -0
  170. fleetpull/network/retry/decision.py +155 -0
  171. fleetpull/network/tls/__init__.py +5 -0
  172. fleetpull/network/tls/truststore_context.py +35 -0
  173. fleetpull/orchestrator/__init__.py +30 -0
  174. fleetpull/orchestrator/backfill.py +127 -0
  175. fleetpull/orchestrator/batch.py +155 -0
  176. fleetpull/orchestrator/bisection.py +271 -0
  177. fleetpull/orchestrator/drivers.py +350 -0
  178. fleetpull/orchestrator/entry.py +347 -0
  179. fleetpull/orchestrator/executors.py +128 -0
  180. fleetpull/orchestrator/fanout.py +163 -0
  181. fleetpull/orchestrator/feed_drive.py +266 -0
  182. fleetpull/orchestrator/metadata_projection.py +195 -0
  183. fleetpull/orchestrator/outcome.py +53 -0
  184. fleetpull/orchestrator/recording.py +85 -0
  185. fleetpull/orchestrator/resume.py +143 -0
  186. fleetpull/orchestrator/roster_harvest.py +108 -0
  187. fleetpull/orchestrator/roster_refresh.py +363 -0
  188. fleetpull/orchestrator/runner.py +262 -0
  189. fleetpull/orchestrator/shape_resolution.py +221 -0
  190. fleetpull/orchestrator/spine.py +166 -0
  191. fleetpull/orchestrator/streaming.py +153 -0
  192. fleetpull/orchestrator/unit_loop.py +273 -0
  193. fleetpull/orchestrator/watermark_drive.py +455 -0
  194. fleetpull/paths/__init__.py +13 -0
  195. fleetpull/paths/datasets.py +38 -0
  196. fleetpull/paths/partitions.py +41 -0
  197. fleetpull/paths/resolution.py +98 -0
  198. fleetpull/polars_typing/__init__.py +20 -0
  199. fleetpull/py.typed +0 -0
  200. fleetpull/records/__init__.py +19 -0
  201. fleetpull/records/convert.py +53 -0
  202. fleetpull/records/dataframe.py +63 -0
  203. fleetpull/records/event_time.py +70 -0
  204. fleetpull/records/fields.py +190 -0
  205. fleetpull/records/flatten.py +54 -0
  206. fleetpull/records/roster_members.py +74 -0
  207. fleetpull/records/schema.py +73 -0
  208. fleetpull/records/validation.py +62 -0
  209. fleetpull/resources/__init__.py +10 -0
  210. fleetpull/resources/config.example.yaml +222 -0
  211. fleetpull/roster/__init__.py +16 -0
  212. fleetpull/roster/definition.py +46 -0
  213. fleetpull/roster/key.py +40 -0
  214. fleetpull/roster/registry.py +93 -0
  215. fleetpull/state/__init__.py +36 -0
  216. fleetpull/state/cursors.py +417 -0
  217. fleetpull/state/database.py +463 -0
  218. fleetpull/state/migrations.py +430 -0
  219. fleetpull/state/reconcile.py +127 -0
  220. fleetpull/state/rosters.py +159 -0
  221. fleetpull/state/run_ledger.py +572 -0
  222. fleetpull/state/work_units.py +648 -0
  223. fleetpull/storage/__init__.py +30 -0
  224. fleetpull/storage/append.py +187 -0
  225. fleetpull/storage/atomic.py +161 -0
  226. fleetpull/storage/files.py +176 -0
  227. fleetpull/storage/frames.py +98 -0
  228. fleetpull/storage/metadata.py +161 -0
  229. fleetpull/storage/partitioned.py +205 -0
  230. fleetpull/storage/pruning.py +169 -0
  231. fleetpull/storage/read.py +35 -0
  232. fleetpull/storage/result.py +34 -0
  233. fleetpull/storage/single_file.py +130 -0
  234. fleetpull/storage/splitting.py +88 -0
  235. fleetpull/storage/staging.py +178 -0
  236. fleetpull/storage/writers.py +145 -0
  237. fleetpull/timing/__init__.py +21 -0
  238. fleetpull/timing/canon.py +92 -0
  239. fleetpull/timing/clock.py +192 -0
  240. fleetpull/timing/codec.py +122 -0
  241. fleetpull/timing/sleeper.py +65 -0
  242. fleetpull/vocabulary/__init__.py +16 -0
  243. fleetpull/vocabulary/json_types.py +20 -0
  244. fleetpull/vocabulary/provider.py +29 -0
  245. fleetpull/vocabulary/quota_scope.py +47 -0
  246. fleetpull/vocabulary/response_category.py +32 -0
  247. fleetpull-0.1.0.dist-info/METADATA +248 -0
  248. fleetpull-0.1.0.dist-info/RECORD +252 -0
  249. fleetpull-0.1.0.dist-info/WHEEL +5 -0
  250. fleetpull-0.1.0.dist-info/entry_points.txt +2 -0
  251. fleetpull-0.1.0.dist-info/licenses/LICENSE +202 -0
  252. fleetpull-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,195 @@
1
+ # src/fleetpull/endpoints/samsara/idling_events.py
2
+ """The Samsara idling_events binding: the fleet-wide windowed cursor walk --
3
+ the first windowed+cursor pairing, composed entirely from existing parts.
4
+
5
+ ``GET /idling/events`` is a modern-envelope surface (``data`` +
6
+ ``pagination {endCursor, hasNextPage}``, terminal on ``hasNextPage:
7
+ false`` beside an empty-string ``endCursor``; the cursor walk proven
8
+ live 2026-07-20: 11 pages at limit=200, 2,200/2,200 unique). Events are
9
+ fleet-wide with per-record asset attribution, so there is NO fan-out --
10
+ the default ``SingleFetch`` shape, declared by declaring nothing (the
11
+ Motive driving_periods template on the Samsara cursor decoder).
12
+
13
+ The windowed leaf builder composes with the existing
14
+ ``SamsaraCursorPageDecoder`` because pagination parameters persist by
15
+ construction: the decoder's ``first_request`` merges ``limit`` onto the
16
+ builder's spec, and its ``after`` advance merges onto the SENT spec, so
17
+ the builder's ``startTime``/``endTime`` ride every page of the walk --
18
+ the mechanism proven live on the drivers sweep, now carrying a window
19
+ instead of a status.
20
+
21
+ The per-endpoint ``limit`` maximum is 200, NOT the 512 of
22
+ vehicles/drivers: limit=512 returns a loud JSON 400 (``"limit must be
23
+ lesser or equal than 200 but got value 512"``) -- the first captured
24
+ instance of Samsara's per-endpoint limit tiers; never assume a
25
+ sibling's limit (DESIGN §8, captured 2026-07-20).
26
+
27
+ Retrieval is START-anchored on UTC, proven by a discriminating pair on
28
+ a 6.5-hour event: a 60-second window strictly inside its span does NOT
29
+ return it, while a 60-second window straddling only its start DOES
30
+ (the fourth distinct anchoring datum across providers -- notably NOT
31
+ the company-local overlap behavior of Motive's idle_events sibling;
32
+ never assume the rule, per endpoint, ever). Consequence:
33
+ ``event_time_column='start_time'``, the retrieval anchor and the
34
+ routing anchor coincide natively, no wire pad exists, and the runner's
35
+ post-fetch window filter is pure hygiene (the driving_periods
36
+ situation).
37
+
38
+ Records carry NO end key -- the interval is start plus
39
+ ``durationMilliseconds``; events were only ever observed complete
40
+ (in-progress idles appear to materialize on completion), and the
41
+ watermark lookback absorbs late materialization (accepted residual).
42
+
43
+ The provider enforces a loud sub-3-months range cap -- 91 days
44
+ accepted; 180 days returns JSON 400 ``"Total duration must be less
45
+ than 3 months."``. No builder guard exists for it (the Motive
46
+ driving_periods stance): default 7-day backfill chunks sit far inside,
47
+ and a ``backfill_chunk_days`` raised past the cap fails loudly on the
48
+ first request rather than losing data silently -- the fix is a smaller
49
+ chunk width.
50
+ """
51
+
52
+ from collections.abc import Mapping
53
+ from dataclasses import dataclass
54
+ from datetime import timedelta
55
+ from typing import Final
56
+
57
+ from fleetpull.config import SamsaraConfig
58
+ from fleetpull.endpoints.shared import (
59
+ EndpointDefinition,
60
+ ResumeValue,
61
+ StorageKind,
62
+ WatermarkMode,
63
+ require_date_window,
64
+ )
65
+ from fleetpull.models.samsara import IdlingEvent
66
+ from fleetpull.network.contract import HttpMethod, RequestSpec
67
+ from fleetpull.network.decoders import SamsaraCursorPageDecoder
68
+ from fleetpull.timing import to_iso8601
69
+ from fleetpull.vocabulary import Provider, QuotaScope
70
+
71
+ __all__: list[str] = [
72
+ 'SamsaraIdlingEventsSpecBuilder',
73
+ 'build_endpoint',
74
+ ]
75
+
76
+ _IDLING_EVENTS_PATH: Final[str] = '/idling/events'
77
+ _RECORDS_KEY: Final[str] = 'data'
78
+
79
+ # The per-page record count. 200 is THIS endpoint's probed maximum --
80
+ # NOT the 512 of vehicles/drivers: limit=512 earns a loud JSON 400
81
+ # naming the 200 cap (captured 2026-07-20), the first captured instance
82
+ # of Samsara's per-endpoint limit tiers. Never assume a sibling's limit.
83
+ _RESULTS_LIMIT: Final[int] = 200
84
+
85
+
86
+ @dataclass(frozen=True, slots=True)
87
+ class SamsaraIdlingEventsSpecBuilder:
88
+ """Build the fleet-wide, date-windowed first request for idling events.
89
+
90
+ The ``SpecBuilder`` for the idling_events single chain: a fixed
91
+ ``GET base_url + path`` carrying the resume window as RFC3339
92
+ ``startTime``/``endTime`` (the timing codec's ``to_iso8601``). The
93
+ decoder owns pagination: its ``first_request`` merges ``limit``
94
+ onto this spec and its ``after`` advance merges onto the sent spec,
95
+ so the window parameters persist across the whole cursor walk.
96
+
97
+ The canonical half-open ``[start, end)`` window maps to the wire as
98
+ ``startTime = start`` and ``endTime = end``. Retrieval is
99
+ START-anchored on UTC (module docstring), so a window's records are
100
+ exactly those starting inside it; any boundary-instant reading of
101
+ ``endTime`` the wire might take is absorbed by the runner's
102
+ post-fetch window filter, which keeps only events whose start lies
103
+ in ``[start, end)`` -- pure hygiene, never load-bearing.
104
+
105
+ Attributes:
106
+ base_url: Root of the Samsara API, trailing-slash-normalized by
107
+ the provider config so the leading-slash path joins
108
+ directly.
109
+ path: The endpoint's leading-slash request path
110
+ (``'/idling/events'``).
111
+ """
112
+
113
+ base_url: str
114
+ path: str
115
+
116
+ def build_spec(
117
+ self, resume: ResumeValue, member_values: Mapping[str, str]
118
+ ) -> RequestSpec:
119
+ """Build the fleet-wide, date-windowed GET.
120
+
121
+ Args:
122
+ resume: The run's resume window. Must be a ``DateWindow`` --
123
+ a watermark endpoint always resumes from one; any other
124
+ value is a wiring bug.
125
+ member_values: Accepted to satisfy the protocol; unused -- a
126
+ fleet-wide single chain binds no member.
127
+
128
+ Returns:
129
+ A credential-less ``GET`` for ``base_url + path`` carrying
130
+ the window's bounds as RFC3339 ``startTime``/``endTime``.
131
+ Auth headers are layered on by the client's
132
+ ``ProviderProfile``; pagination parameters are injected by
133
+ the page decoder's ``first_request``.
134
+
135
+ Raises:
136
+ TypeError: ``resume`` is not a ``DateWindow``.
137
+ ValueError: A window bound is not canonical UTC.
138
+
139
+ Side Effects:
140
+ None.
141
+ """
142
+ resume_window = require_date_window(resume, type(self).__name__)
143
+ params = {
144
+ 'startTime': to_iso8601(resume_window.start),
145
+ 'endTime': to_iso8601(resume_window.end),
146
+ }
147
+ return RequestSpec(
148
+ method=HttpMethod.GET,
149
+ url=f'{self.base_url}{self.path}',
150
+ params=params,
151
+ )
152
+
153
+
154
+ def build_endpoint(config: SamsaraConfig) -> EndpointDefinition[IdlingEvent]:
155
+ """Build the Samsara idling_events watermark binding.
156
+
157
+ Fleet-wide idling events fetched incrementally: the run resumes
158
+ from a ``DateWindow`` (watermark with the provider's late-arrival
159
+ lookback from config -- which also absorbs idles materializing on
160
+ completion), the fetched events are written to ``date=YYYY-MM-DD``
161
+ partitions on ``start_time``, and each refetched partition is
162
+ replaced. Records arrive as a top-level list under ``data``, walked
163
+ by explicit cursor pages (``limit`` on page one, ``after`` merged
164
+ thereafter, the window parameters persisting throughout), terminal
165
+ on ``hasNextPage: false``. No request shape is declared -- the
166
+ endpoint is a fleet-wide ``SingleFetch``, the default.
167
+
168
+ Args:
169
+ config: The validated Samsara configuration; supplies the base
170
+ URL the spec-builder joins to the idling-events path and
171
+ the lookback and cutoff the watermark mode carries.
172
+
173
+ Returns:
174
+ The frozen idling_events ``EndpointDefinition``. Construction
175
+ validates the ``WatermarkMode`` / ``DATE_PARTITIONED`` /
176
+ ``event_time_column`` triple against the response model.
177
+ """
178
+ return EndpointDefinition(
179
+ provider=Provider.SAMSARA,
180
+ name='idling_events',
181
+ spec_builder=SamsaraIdlingEventsSpecBuilder(
182
+ base_url=config.base_url, path=_IDLING_EVENTS_PATH
183
+ ),
184
+ page_decoder=SamsaraCursorPageDecoder(
185
+ records_key=_RECORDS_KEY, results_limit=_RESULTS_LIMIT
186
+ ),
187
+ response_model=IdlingEvent,
188
+ quota_scope=QuotaScope.SAMSARA,
189
+ storage_kind=StorageKind.DATE_PARTITIONED,
190
+ sync_mode=WatermarkMode(
191
+ lookback=timedelta(days=config.lookback_days),
192
+ cutoff=timedelta(days=config.cutoff_days),
193
+ ),
194
+ event_time_column='start_time',
195
+ )
@@ -0,0 +1,116 @@
1
+ # src/fleetpull/endpoints/samsara/odometer_readings.py
2
+ """The Samsara odometer_readings binding: the vehicle-stats windowed
3
+ cursor walk for ``types=obdOdometerMeters`` -- one of the three
4
+ endpoints the legacy ``/fleet/vehicles/stats/history`` surface splits
5
+ into (engine_states, gps_readings, odometer_readings; the three stat
6
+ types carry disjoint schemas, so each is its own entity and dataset --
7
+ DESIGN §8).
8
+
9
+ ``GET /fleet/vehicles/stats/history`` is a modern-envelope surface
10
+ (``data`` + ``pagination {endCursor, hasNextPage}``), but its cursor
11
+ walks the VEHICLE axis within the fixed window: three consecutive live
12
+ pages showed zero vehicle-id overlap (captured 2026-07-20). Each
13
+ vehicle record nests one reading series under the requested type's key,
14
+ so the binding pairs the shared windowed builder (which bakes the FIXED
15
+ ``types=obdOdometerMeters`` selector into every request -- the
16
+ ``types`` vocabulary is API-enforced on input, a loud 400 on any
17
+ unknown value) with ``SamsaraVehicleSeriesPageDecoder``, which unnests
18
+ each vehicle's series into flat per-reading records and delegates
19
+ pagination to the inner cursor decoder untouched.
20
+
21
+ Only carrier vehicles are returned per requested type (24-hour walk:
22
+ 135 vehicles, 135 with data -- no empty-array padding observed), and
23
+ the endpoint is fleet-wide with per-record vehicle attribution
24
+ synthesized by the decoder, so there is NO fan-out -- the default
25
+ ``SingleFetch`` shape, declared by declaring nothing, and no roster is
26
+ sourced or consumed.
27
+
28
+ The per-endpoint ``limit`` maximum is 512, probed directly on THIS
29
+ surface: limit=512 returned HTTP 200 and limit=513 a loud HTTP 400 --
30
+ the vehicles/drivers tier, NOT idling's 200 (the per-endpoint
31
+ limit-tier rule, honored by probing rather than assuming).
32
+
33
+ Retrieval is READING-TIME anchored on the half-open ``[startTime,
34
+ endTime)`` window, probe-proven: a 12:00-13:00Z window returned
35
+ readings spanning exactly 12:00:03.062Z..12:59:56.881Z. Consequence:
36
+ ``event_time_column='time'``, the retrieval anchor and the routing
37
+ anchor coincide natively, no wire pad exists, and the runner's
38
+ post-fetch window filter is pure hygiene.
39
+ """
40
+
41
+ from datetime import timedelta
42
+ from typing import Final
43
+
44
+ from fleetpull.config import SamsaraConfig
45
+ from fleetpull.endpoints.samsara._spec_builders import (
46
+ RECORDS_KEY,
47
+ RESULTS_LIMIT,
48
+ STATS_HISTORY_PATH,
49
+ SamsaraVehicleStatsSpecBuilder,
50
+ )
51
+ from fleetpull.endpoints.shared import (
52
+ EndpointDefinition,
53
+ StorageKind,
54
+ WatermarkMode,
55
+ )
56
+ from fleetpull.models.samsara import OdometerReading
57
+ from fleetpull.network.decoders import SamsaraVehicleSeriesPageDecoder
58
+ from fleetpull.vocabulary import Provider, QuotaScope
59
+
60
+ __all__: list[str] = ['build_endpoint']
61
+
62
+ # The requested stat type -- the verbatim `types` wire value AND the
63
+ # per-vehicle series key the decoder unnests (one name on the wire for
64
+ # both roles, captured 2026-07-20).
65
+ _STAT_TYPE: Final[str] = 'obdOdometerMeters'
66
+
67
+
68
+ def build_endpoint(config: SamsaraConfig) -> EndpointDefinition[OdometerReading]:
69
+ """Build the Samsara odometer_readings watermark binding.
70
+
71
+ Fleet-wide OBD odometer readings fetched incrementally: the run
72
+ resumes from a ``DateWindow`` (watermark with the provider's
73
+ late-arrival lookback from config), the fetched readings are
74
+ written to ``date=YYYY-MM-DD`` partitions on ``time``, and each
75
+ refetched partition is replaced. Vehicle records arrive as a
76
+ top-level list under ``data``, walked by explicit cursor pages
77
+ along the VEHICLE axis (``limit`` on page one, ``after`` merged
78
+ thereafter, the window and ``types`` parameters persisting
79
+ throughout), and the decoder unnests each vehicle's
80
+ ``obdOdometerMeters`` series into one flat record per reading. No
81
+ request shape is declared -- the endpoint is a fleet-wide
82
+ ``SingleFetch``, the default.
83
+
84
+ Args:
85
+ config: The validated Samsara configuration; supplies the base
86
+ URL the spec-builder joins to the stats-history path and
87
+ the lookback and cutoff the watermark mode carries.
88
+
89
+ Returns:
90
+ The frozen odometer_readings ``EndpointDefinition``.
91
+ Construction validates the ``WatermarkMode`` /
92
+ ``DATE_PARTITIONED`` / ``event_time_column`` triple against the
93
+ response model.
94
+ """
95
+ return EndpointDefinition(
96
+ provider=Provider.SAMSARA,
97
+ name='odometer_readings',
98
+ spec_builder=SamsaraVehicleStatsSpecBuilder(
99
+ base_url=config.base_url,
100
+ path=STATS_HISTORY_PATH,
101
+ stat_type=_STAT_TYPE,
102
+ ),
103
+ page_decoder=SamsaraVehicleSeriesPageDecoder(
104
+ records_key=RECORDS_KEY,
105
+ results_limit=RESULTS_LIMIT,
106
+ series_key=_STAT_TYPE,
107
+ ),
108
+ response_model=OdometerReading,
109
+ quota_scope=QuotaScope.SAMSARA,
110
+ storage_kind=StorageKind.DATE_PARTITIONED,
111
+ sync_mode=WatermarkMode(
112
+ lookback=timedelta(days=config.lookback_days),
113
+ cutoff=timedelta(days=config.cutoff_days),
114
+ ),
115
+ event_time_column='time',
116
+ )
@@ -0,0 +1,217 @@
1
+ # src/fleetpull/endpoints/samsara/trips.py
2
+ """The Samsara trips binding: the per-vehicle windowed fan-out -- the
3
+ roster machinery's first cross-provider consumer.
4
+
5
+ The trips surface is the LEGACY v1 API only: ``GET /v1/fleet/trips``
6
+ (the modern candidates 404 -- ``/fleet/trips``, ``/beta/fleet/trips``,
7
+ ``/preview/fleet/trips``; captured 2026-07-20). ``vehicleId`` is
8
+ REQUIRED -- omitting it is a loud HTTP 400 text/plain rpc-error, so the
9
+ endpoint is structurally per-vehicle and the binding declares
10
+ ``request_shape=RosterFanOut`` over the Samsara ``vehicle_ids`` roster
11
+ (declared beside its feeder in ``endpoints/samsara/vehicles.py``; this
12
+ module knows only the ``RosterKey``, never the feeder). The fan-out
13
+ member merges as a QUERY parameter -- the drivers-leaf precedent, not
14
+ the Motive path-template one.
15
+
16
+ The envelope is ``{"trips": [...]}`` with no pagination of any kind:
17
+ one response per (vehicle, window), so the shared ``SinglePageDecoder``
18
+ fits (the GeoTab exception_events pairing precedent). Retrieval is
19
+ OVERLAP-anchored, re-verified per-type 2026-07-20 (a 60-second window
20
+ strictly inside a trip's span returned that trip; start- and
21
+ end-anchoring falsified -- DESIGN §4's historical record for this exact
22
+ endpoint, confirmed live). Per §4: overlap retrieval supersets
23
+ start-anchored ownership, so ``event_time_column='start_time'``, the
24
+ runner's post-fetch window filter assigns each trip to the single chunk
25
+ owning its start, and no wire pad is needed (the exception_events
26
+ reasoning verbatim).
27
+
28
+ The provider enforces a LOUD, exactly-90-day range cap -- HTTP 400
29
+ text/plain ``rpc error: ... requested time range cannot exceed 90
30
+ days``; a 90-day window succeeded. No builder guard exists for it (the
31
+ Motive driving_periods stance): default 7-day backfill chunks sit far
32
+ inside, and a ``backfill_chunk_days`` raised past 90 fails loudly on
33
+ the first request rather than losing data silently -- the fix is a
34
+ smaller chunk width.
35
+ """
36
+
37
+ from collections.abc import Mapping
38
+ from dataclasses import dataclass
39
+ from datetime import UTC, datetime, timedelta
40
+ from typing import Final
41
+
42
+ from fleetpull.config import SamsaraConfig
43
+ from fleetpull.endpoints.shared import (
44
+ EndpointDefinition,
45
+ ResumeValue,
46
+ RosterFanOut,
47
+ StorageKind,
48
+ WatermarkMode,
49
+ require_date_window,
50
+ )
51
+ from fleetpull.models.samsara import Trip
52
+ from fleetpull.network.contract import HttpMethod, RequestSpec
53
+ from fleetpull.network.decoders import SinglePageDecoder
54
+ from fleetpull.roster import RosterKey
55
+ from fleetpull.timing import require_utc
56
+ from fleetpull.vocabulary import Provider, QuotaScope
57
+
58
+ __all__: list[str] = [
59
+ 'SamsaraTripsSpecBuilder',
60
+ 'build_endpoint',
61
+ ]
62
+
63
+ _TRIPS_PATH: Final[str] = '/v1/fleet/trips'
64
+ _RECORDS_KEY: Final[str] = 'trips'
65
+
66
+ # The integer-arithmetic epoch origin: float `.timestamp()` math can land
67
+ # a millisecond-precision bound one ms low (float rounding + int()
68
+ # truncation), so the conversion below is exact integer division --
69
+ # symmetric with the model's no-float-epoch-math decode stance.
70
+ _UNIX_EPOCH: Final[datetime] = datetime(1970, 1, 1, tzinfo=UTC)
71
+ _ONE_MILLISECOND: Final[timedelta] = timedelta(milliseconds=1)
72
+
73
+ # The fan-out member key IS the wire query parameter, verbatim: the spec
74
+ # builder merges member_values into params unchanged, so declaring the
75
+ # exact wire token here leaves no translation seam to drift (the drivers
76
+ # sweep-param stance). The RosterFanOut declaration below carries it.
77
+ _VEHICLE_ID_PARAM: Final[str] = 'vehicleId'
78
+
79
+
80
+ def _to_epoch_milliseconds(bound: datetime) -> int:
81
+ """Render one window bound as the epoch-millisecond int the wire takes.
82
+
83
+ The UTC guard is the codec-boundary discipline ``DateWindow`` defers
84
+ to its serialization point (DESIGN §4): a naive or non-UTC bound
85
+ reaching a request is a missed ingress, failed loudly.
86
+
87
+ Args:
88
+ bound: A window bound; must be canonical UTC.
89
+
90
+ Returns:
91
+ Milliseconds since the Unix epoch.
92
+
93
+ Raises:
94
+ ValueError: ``bound`` is naive or not canonical UTC.
95
+
96
+ Side Effects:
97
+ None.
98
+ """
99
+ return (require_utc(bound) - _UNIX_EPOCH) // _ONE_MILLISECOND
100
+
101
+
102
+ @dataclass(frozen=True, slots=True)
103
+ class SamsaraTripsSpecBuilder:
104
+ """Build the per-vehicle, date-windowed first request for trips.
105
+
106
+ The ``SpecBuilder`` for the trips ``RosterFanOut``: a fixed
107
+ ``GET base_url + path`` carrying the chain's member binding as query
108
+ parameters, verbatim (``{'vehicleId': <member>}`` -- the drivers-leaf
109
+ precedent), plus the resume window as ``startMs``/``endMs`` epoch
110
+ milliseconds. The endpoint is not paginated, so this first request
111
+ is the chain's only request; the page decoder returns no successor.
112
+
113
+ The canonical half-open ``[start, end)`` window maps to the wire as
114
+ ``startMs = start`` and ``endMs = end`` in epoch milliseconds. At
115
+ the millisecond boundary the wire's inclusive reading of ``endMs``
116
+ mismatches the half-open convention (a trip touching the exact end
117
+ instant may be returned) -- deliberately unadjusted: retrieval is
118
+ overlap-anchored and supersets start-anchored ownership anyway, and
119
+ the runner's post-fetch window filter keeps only trips whose start
120
+ lies in ``[start, end)``, so each trip lands in exactly the one
121
+ chunk owning its start and the boundary duplicate never persists.
122
+
123
+ Attributes:
124
+ base_url: Root of the Samsara API, trailing-slash-normalized by
125
+ the provider config so the leading-slash path joins
126
+ directly.
127
+ path: The endpoint's leading-slash request path
128
+ (``'/v1/fleet/trips'``).
129
+ """
130
+
131
+ base_url: str
132
+ path: str
133
+
134
+ def build_spec(
135
+ self, resume: ResumeValue, member_values: Mapping[str, str]
136
+ ) -> RequestSpec:
137
+ """Build one vehicle chain's windowed GET.
138
+
139
+ Args:
140
+ resume: The run's resume window. Must be a ``DateWindow`` --
141
+ a watermark endpoint always resumes from one; any other
142
+ value is a wiring bug.
143
+ member_values: The fan-out member binding, merged verbatim
144
+ as query parameters -- ``{'vehicleId': <id>}`` for a
145
+ roster chain (``vehicleId`` is REQUIRED by the wire; an
146
+ empty binding would earn the provider's own loud 400).
147
+
148
+ Returns:
149
+ A credential-less ``GET`` for ``base_url + path`` carrying
150
+ the member binding plus ``startMs``/``endMs`` epoch
151
+ milliseconds for the window's bounds. Auth headers are
152
+ layered on by the client's ``ProviderProfile``.
153
+
154
+ Raises:
155
+ TypeError: ``resume`` is not a ``DateWindow``.
156
+ ValueError: A window bound is not canonical UTC.
157
+
158
+ Side Effects:
159
+ None.
160
+ """
161
+ resume_window = require_date_window(resume, type(self).__name__)
162
+ params = dict(member_values)
163
+ params['startMs'] = str(_to_epoch_milliseconds(resume_window.start))
164
+ params['endMs'] = str(_to_epoch_milliseconds(resume_window.end))
165
+ return RequestSpec(
166
+ method=HttpMethod.GET,
167
+ url=f'{self.base_url}{self.path}',
168
+ params=params,
169
+ )
170
+
171
+
172
+ def build_endpoint(config: SamsaraConfig) -> EndpointDefinition[Trip]:
173
+ """Build the Samsara trips watermark binding.
174
+
175
+ Per-vehicle trip history fetched incrementally: the run resumes from
176
+ a ``DateWindow`` (watermark with the provider's late-arrival
177
+ lookback from config -- which also absorbs trips materializing on
178
+ completion), the fetched trips are written to ``date=YYYY-MM-DD``
179
+ partitions on ``start_time``, and each refetched partition is
180
+ replaced. Records arrive as a top-level list under ``trips``,
181
+ unpaginated -- one response per (vehicle, window). The
182
+ ``request_shape`` declaration (``RosterFanOut``) names the Samsara
183
+ ``vehicle_ids`` roster; the orchestration entry resolves it to
184
+ members and fans one request chain per vehicle, passing each id to
185
+ the spec-builder's ``member_values`` -- this binding only declares
186
+ the strategies and the roster key, never the roster's feeder.
187
+
188
+ Args:
189
+ config: The validated Samsara configuration; supplies the base
190
+ URL the spec-builder joins to the trips path and the
191
+ lookback and cutoff the watermark mode carries.
192
+
193
+ Returns:
194
+ The frozen trips ``EndpointDefinition``. Construction validates
195
+ the ``WatermarkMode`` / ``DATE_PARTITIONED`` /
196
+ ``event_time_column`` triple against the response model.
197
+ """
198
+ return EndpointDefinition(
199
+ provider=Provider.SAMSARA,
200
+ name='trips',
201
+ spec_builder=SamsaraTripsSpecBuilder(
202
+ base_url=config.base_url, path=_TRIPS_PATH
203
+ ),
204
+ page_decoder=SinglePageDecoder(records_key=_RECORDS_KEY),
205
+ response_model=Trip,
206
+ quota_scope=QuotaScope.SAMSARA,
207
+ storage_kind=StorageKind.DATE_PARTITIONED,
208
+ sync_mode=WatermarkMode(
209
+ lookback=timedelta(days=config.lookback_days),
210
+ cutoff=timedelta(days=config.cutoff_days),
211
+ ),
212
+ event_time_column='start_time',
213
+ request_shape=RosterFanOut(
214
+ roster=RosterKey(Provider.SAMSARA, 'vehicle_ids'),
215
+ member_key=_VEHICLE_ID_PARAM,
216
+ ),
217
+ )
@@ -0,0 +1,139 @@
1
+ # src/fleetpull/endpoints/samsara/vehicle_fuel_energy_reports.py
2
+ """The Samsara vehicle_fuel_energy_reports binding: the window-grain
3
+ rollup surface -- the first fixed-unit-width watermark endpoint
4
+ (probe-settled 2026-07-21, DESIGN section 8). The legacy hub's
5
+ ``vehicle_fuel_energy``, renamed per the name=snake-plural-of-model
6
+ invariant (model ``VehicleFuelEnergyReport``).
7
+
8
+ ``GET /fleet/reports/vehicles/fuel-energy`` carries the standard
9
+ ``pagination {endCursor, hasNextPage}`` cursor contract, but the record
10
+ list is NESTED: ``data`` is an OBJECT whose only key is
11
+ ``vehicleReports``, a list of report objects -- the
12
+ ``SamsaraWindowReportPageDecoder``'s shape. Reports are fleet-wide with
13
+ per-record vehicle attribution, so there is NO fan-out -- the default
14
+ ``SingleFetch`` shape, declared by declaring nothing, and no roster is
15
+ sourced or consumed.
16
+
17
+ **THE ROLLUP GRAIN IS THE REQUEST WINDOW, proven twice:** widening a
18
+ 1-day window to 2 days GREW per-vehicle metrics (36 of 47 vehicles
19
+ shared between the 1-day walk and the 2-day window's first page grew),
20
+ and summing two adjacent day rollups reproduced the two-day
21
+ rollup on only 178 of 267 vehicles (89/267 MISMATCHED across distance,
22
+ engine run time, fuel, and energy). Day rows are NOT a lossless
23
+ decomposition of wider windows -- each row is the provider's answer for
24
+ exactly its window. Consequence: the binding declares
25
+ ``fixed_unit_days=1`` on its ``WatermarkMode`` -- the unit width is
26
+ part of the ROW'S MEANING, so it must never float with
27
+ ``sync.backfill_chunk_days``. Rows carry NO event-time key of any kind;
28
+ the decoder stamps each with the sent window, and
29
+ ``event_time_column='window_start'`` routes each day's rollup to its
30
+ own partition (the vehicle-presence union across day windows holds:
31
+ the two day windows' 145- and 242-vehicle sets union to exactly the
32
+ two-day walk's 267, so per-day fetches lose no vehicle).
33
+
34
+ The ``limit`` param is PROVEN IGNORED (the assignments placebo
35
+ posture): limit=512, 513, and 10 on the 2-day window all returned
36
+ identical paging (3 pages, 267 reports), and 513 was NOT rejected -- no
37
+ enforced tier. The declared ``results_limit=100`` is
38
+ documentation-by-declaration of the server's OWN observed ~100-report
39
+ page size, not a working knob.
40
+
41
+ The window rides this surface family's own ``startDate``/``endDate``
42
+ param NAMES (full RFC3339 datetimes accepted despite the names --
43
+ unlike every other probed Samsara vertical's ``startTime``/``endTime``;
44
+ the shared ``SamsaraFuelEnergyReportSpecBuilder`` carries the
45
+ provenance).
46
+ """
47
+
48
+ from datetime import timedelta
49
+ from typing import Final
50
+
51
+ from fleetpull.config import SamsaraConfig
52
+ from fleetpull.endpoints.samsara._spec_builders import (
53
+ SamsaraFuelEnergyReportSpecBuilder,
54
+ )
55
+ from fleetpull.endpoints.shared import (
56
+ EndpointDefinition,
57
+ StorageKind,
58
+ WatermarkMode,
59
+ )
60
+ from fleetpull.models.samsara import VehicleFuelEnergyReport
61
+ from fleetpull.network.decoders import SamsaraWindowReportPageDecoder
62
+ from fleetpull.vocabulary import Provider, QuotaScope
63
+
64
+ __all__: list[str] = ['build_endpoint']
65
+
66
+ _VEHICLE_FUEL_ENERGY_PATH: Final[str] = '/fleet/reports/vehicles/fuel-energy'
67
+ _RECORDS_KEY: Final[str] = 'data'
68
+
69
+ # The nested report key: `data` is an OBJECT whose only key is this
70
+ # arm's report list (captured 2026-07-21).
71
+ _REPORT_KEY: Final[str] = 'vehicleReports'
72
+
73
+ # The per-page record count. ~100 is the server's OWN observed page
74
+ # size, and the `limit` param is PROVEN IGNORED on this surface:
75
+ # limit=512, 513, and 10 on the same 2-day window all returned
76
+ # identical paging (3 pages, 267 reports); 513 was NOT rejected (no
77
+ # enforced tier, captured 2026-07-21). Declaring 100 documents the
78
+ # server's paging; it is not a working knob.
79
+ _RESULTS_LIMIT: Final[int] = 100
80
+
81
+ # The fixed work-unit width, in whole days. The rollup grain is the
82
+ # request window (metrics GREW when the window widened) and day rollups
83
+ # are NON-ADDITIVE into wider windows (89/267 mismatched, captured
84
+ # 2026-07-21), so the unit width is part of the row's meaning and is
85
+ # pinned here rather than left to sync.backfill_chunk_days.
86
+ _FIXED_UNIT_DAYS: Final[int] = 1
87
+
88
+
89
+ def build_endpoint(
90
+ config: SamsaraConfig,
91
+ ) -> EndpointDefinition[VehicleFuelEnergyReport]:
92
+ """Build the Samsara vehicle_fuel_energy_reports watermark binding.
93
+
94
+ Fleet-wide per-vehicle fuel-energy rollups fetched incrementally at
95
+ the fixed 1-day unit width: the run resumes from a ``DateWindow``
96
+ (watermark with the provider's late-arrival lookback from config),
97
+ the planner tiles it into exactly-one-day units (the declared
98
+ ``fixed_unit_days`` -- module docstring for the non-additivity
99
+ proof), each unit's reports are stamped with its window by the
100
+ decoder and written to the ``date=YYYY-MM-DD`` partition on
101
+ ``window_start``, and each refetched partition is replaced. Reports
102
+ arrive nested under ``data.vehicleReports``, walked by explicit
103
+ cursor pages (``limit`` on page one, ``after`` merged thereafter,
104
+ the ``startDate``/``endDate`` window persisting throughout),
105
+ terminal on ``hasNextPage: false``. No request shape is declared --
106
+ the endpoint is a fleet-wide ``SingleFetch``, the default.
107
+
108
+ Args:
109
+ config: The validated Samsara configuration; supplies the base
110
+ URL the spec-builder joins to the report path and the
111
+ lookback and cutoff the watermark mode carries.
112
+
113
+ Returns:
114
+ The frozen vehicle_fuel_energy_reports ``EndpointDefinition``.
115
+ Construction validates the ``WatermarkMode`` /
116
+ ``DATE_PARTITIONED`` / ``event_time_column`` triple against the
117
+ response model.
118
+ """
119
+ return EndpointDefinition(
120
+ provider=Provider.SAMSARA,
121
+ name='vehicle_fuel_energy_reports',
122
+ spec_builder=SamsaraFuelEnergyReportSpecBuilder(
123
+ base_url=config.base_url, path=_VEHICLE_FUEL_ENERGY_PATH
124
+ ),
125
+ page_decoder=SamsaraWindowReportPageDecoder(
126
+ records_key=_RECORDS_KEY,
127
+ report_key=_REPORT_KEY,
128
+ results_limit=_RESULTS_LIMIT,
129
+ ),
130
+ response_model=VehicleFuelEnergyReport,
131
+ quota_scope=QuotaScope.SAMSARA,
132
+ storage_kind=StorageKind.DATE_PARTITIONED,
133
+ sync_mode=WatermarkMode(
134
+ lookback=timedelta(days=config.lookback_days),
135
+ cutoff=timedelta(days=config.cutoff_days),
136
+ fixed_unit_days=_FIXED_UNIT_DAYS,
137
+ ),
138
+ event_time_column='window_start',
139
+ )