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,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
+ )