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,174 @@
1
+ # src/fleetpull/models/samsara/idling_event.py
2
+ """Samsara IdlingEvent response model (``GET /idling/events``).
3
+
4
+ Written from captured live responses (2026-07-20 probe session: a
5
+ 2,200-event census walked over 11 cursor pages), never from docs. The
6
+ census observed ZERO real nulls -- optionality is absence-shaped
7
+ (Samsara omits keys, the vehicles/drivers posture): every key below
8
+ except the three partial blocks was present on every record, so those
9
+ fields are required; ``operator`` (1546/2200 -- driver attribution when
10
+ known), ``airTemperatureMillicelsius`` (1833/2200), and ``address``
11
+ (552/2200) are optional. Within a carrying ``address`` block,
12
+ ``addressTypes`` is itself absent on ~31 of the 552 blocks, so it is
13
+ optional inside the block too.
14
+
15
+ There is NO end key: the interval is start plus duration. Events were
16
+ only ever observed complete, with implied ends in the past even in a
17
+ last-30-minutes probe -- in-progress idles appear to materialize on
18
+ completion; the watermark lookback absorbs late materialization
19
+ (accepted residual, DESIGN §8). ``durationMilliseconds`` stays a
20
+ verbatim unit-suffixed int mirror -- no timedelta recovery: the value
21
+ is directly consumable, and recovery would presume a use.
22
+
23
+ ``startTime`` is an RFC3339 string recovered as a tz-aware UTC datetime
24
+ by Pydantic's standard parse (the vehicles/drivers pattern, NOT the
25
+ trips epoch-ms path). ``ptoState`` is a plain ``str``, NOT an enum:
26
+ only ``'inactive'`` was observed in 2,200 records, but the value set is
27
+ not closed by evidence (unlike ``driverActivationStatus``'s 400-proven
28
+ closure), so membership is not enforced. ``fuelConsumedMilliliters`` is
29
+ MIXED int|float on the wire -- modeled ``float``, lax coercion lifting
30
+ the int shape. The two money blocks mirror verbatim as strings
31
+ (``{amount, currency}``) -- never parsed to a numeric type. NOTE the id
32
+ type split, mirrored exactly: ``address.id`` is a STRING while
33
+ ``asset.id`` and ``operator.id`` are BARE INTs.
34
+
35
+ Wire keys are camelCase; fields are snake_case via the ``to_camel``
36
+ alias generator.
37
+ """
38
+
39
+ from datetime import datetime
40
+
41
+ from pydantic import ConfigDict
42
+ from pydantic.alias_generators import to_camel
43
+
44
+ from fleetpull.model_contract import ResponseModel
45
+
46
+ __all__: list[str] = [
47
+ 'AssetRef',
48
+ 'FuelCost',
49
+ 'IdlingAddress',
50
+ 'IdlingEvent',
51
+ 'OperatorRef',
52
+ ]
53
+
54
+
55
+ class AssetRef(ResponseModel):
56
+ """The ``asset`` block: the event's vehicle reference.
57
+
58
+ Present on every captured record -- events are fleet-wide with
59
+ per-record asset attribution, which is what makes the endpoint a
60
+ single fetch with no fan-out.
61
+
62
+ Attributes:
63
+ id: Samsara's asset id -- a BARE int on the wire, unlike the
64
+ numeric-string ids of the vehicles/drivers surfaces.
65
+ """
66
+
67
+ id: int
68
+
69
+
70
+ class OperatorRef(ResponseModel):
71
+ """The ``operator`` block: driver attribution when known (1546/2200).
72
+
73
+ Attributes:
74
+ id: Samsara's operator id -- a BARE int on the wire, like
75
+ ``asset.id``.
76
+ """
77
+
78
+ id: int
79
+
80
+
81
+ class IdlingAddress(ResponseModel):
82
+ """The ``address`` block: a matched defined-address reference (552/2200).
83
+
84
+ Attributes:
85
+ id: Samsara's address id -- a STRING on the wire, unlike the
86
+ bare-int ``asset.id``/``operator.id`` beside it (mirrored
87
+ exactly; never coerced to match its siblings).
88
+ address_types: The defined address's type tags (element
89
+ ``'yard'`` observed); absent on ~31 of the 552 captured
90
+ address blocks, so optional within the block.
91
+ """
92
+
93
+ model_config = ConfigDict(alias_generator=to_camel)
94
+
95
+ id: str
96
+ address_types: list[str] | None = None
97
+
98
+
99
+ class FuelCost(ResponseModel):
100
+ """A money block: the ``{amount, currency}`` shape.
101
+
102
+ The one shape both cost fields (``fuelCost``, ``gaseousFuelCost``)
103
+ carry, both keys present in every captured block. Mirrored verbatim
104
+ as strings -- parsing money would presume a use.
105
+
106
+ Attributes:
107
+ amount: The monetary amount, a decimal string.
108
+ currency: The currency code string.
109
+ """
110
+
111
+ amount: str
112
+ currency: str
113
+
114
+
115
+ class IdlingEvent(ResponseModel):
116
+ """One Samsara idling event, start-anchored per window.
117
+
118
+ A pure mirror of the captured fields (module docstring). Field
119
+ semantics and units are Samsara's; no value is derived or
120
+ interpreted here. The interval is ``start_time`` plus
121
+ ``duration_milliseconds`` -- there is no end key on the wire.
122
+
123
+ Attributes:
124
+ event_uuid: The event's id, a UUID string.
125
+ start_time: Idle start (RFC3339, recovered tz-aware UTC) -- the
126
+ endpoint's event time; retrieval is START-anchored on UTC
127
+ (DESIGN §8), so retrieval and routing coincide natively.
128
+ duration_milliseconds: Idle duration in milliseconds, a bare
129
+ int mirrored verbatim (no end key exists; module
130
+ docstring).
131
+ asset: The vehicle reference (bare-int id).
132
+ latitude: Event latitude, decimal degrees.
133
+ longitude: Event longitude, decimal degrees.
134
+ pto_state: Power-take-off state -- a plain string; only
135
+ ``'inactive'`` observed in 2,200 records, but the value set
136
+ is not evidence-closed, so no enum (module docstring).
137
+ fuel_consumed_milliliters: Fuel consumed while idling, in
138
+ milliliters -- MIXED int|float on the wire, modeled float.
139
+ fuel_cost: The fuel cost money block, strings verbatim.
140
+ gaseous_fuel_consumed_grams: Gaseous fuel consumed, in grams, a
141
+ bare int.
142
+ gaseous_fuel_cost: The gaseous-fuel cost money block, strings
143
+ verbatim.
144
+ operator: Driver attribution when known (1546/2200; bare-int
145
+ id).
146
+ air_temperature_millicelsius: Ambient air temperature in
147
+ millidegrees Celsius, a bare int (1833/2200).
148
+ address: The matched defined-address block (552/2200; STRING
149
+ id).
150
+ """
151
+
152
+ model_config = ConfigDict(alias_generator=to_camel)
153
+
154
+ # Identity and the start+duration interval (no end key).
155
+ event_uuid: str
156
+ start_time: datetime
157
+ duration_milliseconds: int
158
+
159
+ # Attribution and position.
160
+ asset: AssetRef
161
+ latitude: float
162
+ longitude: float
163
+
164
+ # State and consumption (provider units mirrored verbatim).
165
+ pto_state: str
166
+ fuel_consumed_milliliters: float
167
+ fuel_cost: FuelCost
168
+ gaseous_fuel_consumed_grams: int
169
+ gaseous_fuel_cost: FuelCost
170
+
171
+ # The partial blocks (absence-shaped).
172
+ operator: OperatorRef | None = None
173
+ air_temperature_millicelsius: int | None = None
174
+ address: IdlingAddress | None = None
@@ -0,0 +1,90 @@
1
+ # src/fleetpull/models/samsara/odometer_reading.py
2
+ """Samsara OdometerReading response model
3
+ (``GET /fleet/vehicles/stats/history``, ``types=obdOdometerMeters``,
4
+ post-decoder reading grain).
5
+
6
+ Written from captured live responses (2026-07-20 probe session: a
7
+ 9,480-reading census over a 24-hour window, 135 vehicles -- every
8
+ vehicle returned per the requested type carried data; no empty-array
9
+ padding was observed), never from docs. The model mirrors the FLAT
10
+ record ``SamsaraVehicleSeriesPageDecoder`` emits, one row per reading
11
+ -- the grain the records pipeline represents (scalars, not
12
+ list-of-objects; DESIGN section 9) -- and its two field families have
13
+ different provenance:
14
+
15
+ - ``vehicle_id`` / ``vehicle_name`` / ``vehicle_serial`` /
16
+ ``vehicle_vin`` are DECODER-SYNTHESIZED: the unnesting lifts them
17
+ from the per-vehicle ``id``/``name`` keys and the ``externalIds``
18
+ object's literal dotted ``samsara.serial``/``samsara.vin`` wire keys
19
+ onto every reading. ``vehicle_id`` and ``vehicle_name`` were 74/74
20
+ on the censused mixed-type page and are required; ``vehicle_serial``
21
+ and ``vehicle_vin`` were also 74/74 there, but one page is not a
22
+ whole-population oath (an unplugged or serial-less unit could omit
23
+ its ``externalIds`` block -- the vehicles surface shows exactly that
24
+ variance), so both stay OPTIONAL -- the drivers conservative posture.
25
+ - ``time`` / ``value`` are WIRE-VERBATIM reading keys: the series
26
+ census observed exactly ``{time, value}`` on every one of the 9,480
27
+ readings, so both are required.
28
+
29
+ ``value`` is the OBD odometer in METERS -- a bare int on every observed
30
+ reading (range 3,552,000..1,012,456,215 in census), mirrored verbatim
31
+ under the model's unitless wire name; the unit lives here and in the
32
+ series' stat-type name (``obdOdometerMeters``), never converted.
33
+
34
+ ``time`` is an RFC3339 string recovered as a tz-aware UTC datetime by
35
+ Pydantic's standard parse. Readings fall strictly inside the requested
36
+ ``[startTime, endTime)`` window (probe-proven), so ``time`` is the
37
+ endpoint's event-time column with retrieval and routing coinciding
38
+ natively.
39
+
40
+ Wire keys are camelCase; fields are snake_case via the ``to_camel``
41
+ alias generator.
42
+ """
43
+
44
+ from datetime import datetime
45
+
46
+ from pydantic import ConfigDict
47
+ from pydantic.alias_generators import to_camel
48
+
49
+ from fleetpull.model_contract import ResponseModel
50
+
51
+ __all__: list[str] = ['OdometerReading']
52
+
53
+
54
+ class OdometerReading(ResponseModel):
55
+ """One OBD odometer reading of one vehicle, at the reading grain.
56
+
57
+ A pure mirror of the flat post-decoder record (module docstring:
58
+ the identity fields are decoder-synthesized, the reading fields
59
+ wire-verbatim). Field semantics and units are Samsara's; no value
60
+ is derived or interpreted here.
61
+
62
+ Attributes:
63
+ vehicle_id: The vehicle's Samsara id -- a numeric string,
64
+ decoder-synthesized from the vehicle record's ``id``.
65
+ vehicle_name: The vehicle's display name, decoder-synthesized
66
+ from the vehicle record's ``name``.
67
+ vehicle_serial: The gateway serial, decoder-synthesized from
68
+ ``externalIds['samsara.serial']`` (74/74 on the censused
69
+ page; optional -- module docstring).
70
+ vehicle_vin: The VIN, decoder-synthesized from
71
+ ``externalIds['samsara.vin']`` (74/74 on the censused page;
72
+ optional -- module docstring).
73
+ time: The reading instant (RFC3339, recovered tz-aware UTC) --
74
+ the event-time column; readings fall strictly inside the
75
+ requested window.
76
+ value: The OBD odometer in meters -- a bare int on every
77
+ observed reading, mirrored verbatim (module docstring).
78
+ """
79
+
80
+ model_config = ConfigDict(alias_generator=to_camel)
81
+
82
+ # Decoder-synthesized vehicle identity.
83
+ vehicle_id: str
84
+ vehicle_name: str
85
+ vehicle_serial: str | None = None
86
+ vehicle_vin: str | None = None
87
+
88
+ # Wire-verbatim reading payload.
89
+ time: datetime
90
+ value: int
@@ -0,0 +1,199 @@
1
+ # src/fleetpull/models/samsara/trip.py
2
+ """Samsara Trip response model (``GET /v1/fleet/trips`` -- the legacy v1
3
+ surface; every modern candidate path 404s).
4
+
5
+ Written from captured live responses (2026-07-20 probe session: a
6
+ 725-trip census across 60 vehicles), never from docs. The census
7
+ observed ZERO nulls anywhere: every key below except the two address
8
+ blocks was present on every record, so those fields are required; the
9
+ address blocks (``startAddress`` 177/725, ``endAddress`` 185/725) are
10
+ present only when the trip endpoint matched a defined address/geofence
11
+ and are optional.
12
+
13
+ ``startMs``/``endMs`` are epoch-MILLISECOND ints on the wire. The
14
+ mirror RECOVERS them as tz-aware UTC datetimes via a mode='before'
15
+ validator -- type recovery is structural and belongs on the mirror (the
16
+ ``GeotabTimeSpan`` precedent), not interpretation. The field names
17
+ (``start_time``/``end_time``) drop the wire's unit suffix because the
18
+ recovered type carries the unit; naming is the model's to own. ``endMs``
19
+ was present on every observed trip including the two most recent --
20
+ in-progress trips were never observed and appear to materialize on
21
+ completion; the watermark lookback absorbs late materialization
22
+ (accepted residual, DESIGN §8), so ``end_time`` stays required and a
23
+ trip ever arriving without it fails validation loudly.
24
+
25
+ Everything else mirrors verbatim: the unit-suffixed int family
26
+ (``distanceMeters``, ``fuelConsumedMl``, ``tollMeters``) and the
27
+ odometer pair keep provider units untouched; ``driverId`` is an int
28
+ whose ``0`` is the UNASSIGNED sentinel (110/725) -- mirrored verbatim,
29
+ never nulled or interpreted. ``assetIds``/``codriverIds`` were observed
30
+ ONLY EMPTY across all 725 records; they are typed ``list[int]`` --
31
+ the int-id family, in the ``list[scalar]`` form the records layer's
32
+ schema derivation represents (DESIGN §9; the §9 pipeline has no tuple
33
+ form) -- so a first non-empty capture lands as data, not a crash.
34
+
35
+ The record does NOT echo the requested ``vehicleId``: per-vehicle
36
+ attribution lives in the request parameter (the roster fan-out member),
37
+ not on the wire record. The response *wrapper* (the ``{"trips": [...]}``
38
+ envelope, unpaginated) is the endpoints layer's decoder concern; this
39
+ module mirrors only the inner per-trip object.
40
+
41
+ Wire keys are camelCase; fields are snake_case via the ``to_camel``
42
+ alias generator (``startMs``/``endMs`` carry explicit aliases, since
43
+ their field names diverge by design).
44
+ """
45
+
46
+ from datetime import UTC, datetime, timedelta
47
+ from typing import Annotated, Final
48
+
49
+ from pydantic import BeforeValidator, ConfigDict, Field
50
+ from pydantic.alias_generators import to_camel
51
+
52
+ from fleetpull.model_contract import ResponseModel
53
+ from fleetpull.vocabulary import JsonValue
54
+
55
+ __all__: list[str] = [
56
+ 'Trip',
57
+ 'TripAddress',
58
+ 'TripCoordinates',
59
+ ]
60
+
61
+ _EPOCH_START: Final[datetime] = datetime(1970, 1, 1, tzinfo=UTC)
62
+
63
+
64
+ def _epoch_milliseconds_to_datetime(value: JsonValue | datetime) -> datetime:
65
+ """The epoch-ms ingress: recover a tz-aware UTC datetime from a wire int.
66
+
67
+ Strict by design: the census observed BARE ints only, so anything
68
+ else -- a quoted number, a float, a bool -- is a wire drift that
69
+ should fail validation loudly, not pass mangled. Exact integer
70
+ arithmetic (no float epoch math), so millisecond precision is
71
+ preserved verbatim.
72
+
73
+ Args:
74
+ value: The raw wire value, or an already-recovered datetime on a
75
+ Pydantic revalidation path.
76
+
77
+ Returns:
78
+ The instant as a timezone-aware UTC datetime (passthrough for an
79
+ already-recovered value).
80
+
81
+ Raises:
82
+ ValueError: ``value`` is not a bare int (bools excluded).
83
+ """
84
+ if isinstance(value, datetime):
85
+ return value
86
+ if isinstance(value, bool) or not isinstance(value, int):
87
+ raise ValueError(
88
+ f'expected a bare epoch-millisecond int, got {type(value).__name__}'
89
+ )
90
+ return _EPOCH_START + timedelta(milliseconds=value)
91
+
92
+
93
+ # The epoch-ms datetime field type. Plain assignment, not a `type`
94
+ # statement: Pydantic must evaluate the Annotated form eagerly so the
95
+ # metadata lifts into FieldInfo and the records field walk sees the bare
96
+ # `datetime` leaf (the GeotabTimeSpan stance).
97
+ _EpochMillisecondsDatetime = Annotated[
98
+ datetime, BeforeValidator(_epoch_milliseconds_to_datetime)
99
+ ]
100
+
101
+
102
+ class TripCoordinates(ResponseModel):
103
+ """A ``{latitude, longitude}`` coordinate pair, decimal-degree floats.
104
+
105
+ The one shape both trip endpoints' coordinate blocks
106
+ (``startCoordinates``, ``endCoordinates``) carry -- both keys
107
+ present in every captured block.
108
+ """
109
+
110
+ latitude: float
111
+ longitude: float
112
+
113
+
114
+ class TripAddress(ResponseModel):
115
+ """A matched address/geofence block: the ``{address, id, name}`` shape.
116
+
117
+ Present only when a trip endpoint matched a defined address/geofence
118
+ (``startAddress`` 177/725, ``endAddress`` 185/725); all three keys
119
+ present in every carrying block.
120
+
121
+ Attributes:
122
+ address: The defined address's street address string.
123
+ id: Samsara's address id -- a BARE int on the wire, unlike the
124
+ string ids of the vehicles/drivers surfaces.
125
+ name: The defined address's display name.
126
+ """
127
+
128
+ address: str
129
+ id: int
130
+ name: str
131
+
132
+
133
+ class Trip(ResponseModel):
134
+ """One Samsara vehicle trip, overlap-retrieved per (vehicle, window).
135
+
136
+ A pure mirror of the captured fields (module docstring). Field
137
+ semantics and units are Samsara's; no value is derived or
138
+ interpreted here. The requested ``vehicleId`` is not echoed on the
139
+ record.
140
+
141
+ Attributes:
142
+ start_time: Trip start, recovered from the wire's epoch-ms
143
+ ``startMs`` -- the endpoint's event time (start-anchored
144
+ ownership, DESIGN §4).
145
+ end_time: Trip end, recovered from the wire's epoch-ms
146
+ ``endMs``; present on every observed trip (in-progress trips
147
+ appear to materialize on completion).
148
+ distance_meters: Trip distance in meters, a bare int.
149
+ fuel_consumed_ml: Fuel consumed in milliliters, a bare int.
150
+ toll_meters: Distance on toll roads in meters, a bare int.
151
+ start_odometer: Odometer at trip start -- provider units
152
+ mirrored verbatim, a bare int.
153
+ end_odometer: Odometer at trip end -- provider units mirrored
154
+ verbatim, a bare int.
155
+ driver_id: The assigned driver's id; ``0`` is the UNASSIGNED
156
+ sentinel (110/725), mirrored verbatim.
157
+ start_location: Reverse-geocoded start location string.
158
+ end_location: Reverse-geocoded end location string.
159
+ start_coordinates: The start ``{latitude, longitude}`` block.
160
+ end_coordinates: The end ``{latitude, longitude}`` block.
161
+ asset_ids: Attached asset ids; observed ONLY EMPTY across all
162
+ 725 census records.
163
+ codriver_ids: Co-driver ids; observed ONLY EMPTY across all 725
164
+ census records.
165
+ start_address: The matched start address/geofence block
166
+ (177/725); null when no defined address matched.
167
+ end_address: The matched end address/geofence block (185/725);
168
+ null when no defined address matched.
169
+ """
170
+
171
+ model_config = ConfigDict(alias_generator=to_camel)
172
+
173
+ # The trip interval (epoch-ms recovered, module docstring).
174
+ start_time: _EpochMillisecondsDatetime = Field(alias='startMs')
175
+ end_time: _EpochMillisecondsDatetime = Field(alias='endMs')
176
+
177
+ # Distances, fuel, and odometers (provider units, bare ints).
178
+ distance_meters: int
179
+ fuel_consumed_ml: int
180
+ toll_meters: int
181
+ start_odometer: int
182
+ end_odometer: int
183
+
184
+ # Assignment (0 = unassigned, mirrored verbatim).
185
+ driver_id: int
186
+
187
+ # Endpoints: geocoded strings and coordinate blocks.
188
+ start_location: str
189
+ end_location: str
190
+ start_coordinates: TripCoordinates
191
+ end_coordinates: TripCoordinates
192
+
193
+ # The int-id lists (only empties observed).
194
+ asset_ids: list[int]
195
+ codriver_ids: list[int]
196
+
197
+ # The partial address/geofence matches.
198
+ start_address: TripAddress | None = None
199
+ end_address: TripAddress | None = None
@@ -0,0 +1,167 @@
1
+ # src/fleetpull/models/samsara/vehicle.py
2
+ """Samsara Vehicle response model (``GET /fleet/vehicles``).
3
+
4
+ Written from captured live responses (2026-07-17 probe session: a
5
+ full-population sweep of all 608 records plus six captured record
6
+ variants), never from docs. The sweep observed no null value and no
7
+ type variance on any of the 20 observed keys -- Samsara omits absent
8
+ keys rather than nulling them (the GeoTab posture), while also using
9
+ empty strings (``notes: ""`` on every captured record), which mirror
10
+ verbatim and normalize to null at the DataFrame boundary. Fields the
11
+ sweep proved on every record are required; the partial-presence fields
12
+ are optional (presence counts in the attribute docs, out of 608). The
13
+ minimal captured shape is the bare 7-key form -- units with no gateway
14
+ carry serial-shaped default names.
15
+
16
+ ``year`` arrives as a quoted integer (``"2013"``); lax coercion types
17
+ it. No empty-string ``year`` was observed in 608 records -- if one ever
18
+ arrives, the loud ``int_parsing`` failure names it (the Motive ``year``
19
+ history, replayed on purpose).
20
+
21
+ Excluded fields (``extra='ignore'`` makes exclusion exactly "don't
22
+ model it"):
23
+
24
+ - ``tags`` -- a list of ``{id, name, parentTagId}`` objects (549/608,
25
+ ``parentTagId`` itself partial within elements); the records layer's
26
+ schema derivation supports scalars, enums, ``list[scalar]``, and
27
+ nested models only (the Device/User exclusion precedent) -- modeled
28
+ when the list-of-structs derivation vertical lands.
29
+
30
+ ``externalIds`` is an OPEN, user-definable map whose keys are dotted
31
+ namespace names -- not a struct. The two Samsara-managed keys observed
32
+ in every carrying record are modeled as aliased fields
33
+ (``samsara.serial``, ``samsara.vin``, each mirroring its top-level
34
+ sibling exactly in capture); user-defined keys are absorbed by
35
+ ``extra='ignore'`` until a capture shows one, per the union-of-observed
36
+ discipline.
37
+
38
+ Wire keys are camelCase; fields are snake_case via the ``to_camel``
39
+ alias generator (the dotted ``externalIds`` keys carry explicit
40
+ aliases, above the generator's reach).
41
+ """
42
+
43
+ from datetime import datetime
44
+
45
+ from pydantic import ConfigDict, Field
46
+ from pydantic.alias_generators import to_camel
47
+
48
+ from fleetpull.model_contract import ResponseModel
49
+
50
+ __all__: list[str] = [
51
+ 'Vehicle',
52
+ 'VehicleExternalIds',
53
+ 'VehicleGatewayRef',
54
+ 'VehicleStaticAssignedDriverRef',
55
+ ]
56
+
57
+
58
+ class VehicleExternalIds(ResponseModel):
59
+ """The ``externalIds`` block: an open map of namespaced external ids.
60
+
61
+ Only the two Samsara-managed keys are modeled (see the module
62
+ docstring's open-map caveat). Each key is independently optional --
63
+ the sweep observed carriers with a serial and no VIN (595 blocks:
64
+ ``samsara.serial`` on 509, ``samsara.vin`` on 576).
65
+
66
+ Attributes:
67
+ samsara_serial: The gateway serial, undashed (wire key
68
+ ``samsara.serial``; equal to the record's top-level
69
+ ``serial`` in every capture).
70
+ samsara_vin: The VIN (wire key ``samsara.vin``; equal to the
71
+ record's top-level ``vin`` in every capture).
72
+ """
73
+
74
+ samsara_serial: str | None = Field(default=None, alias='samsara.serial')
75
+ samsara_vin: str | None = Field(default=None, alias='samsara.vin')
76
+
77
+
78
+ class VehicleGatewayRef(ResponseModel):
79
+ """The installed telematics gateway: serial and hardware model.
80
+
81
+ Attributes:
82
+ serial: The gateway serial in Samsara's dashed 4-3-3 rendering
83
+ (the record's top-level ``serial`` carries the undashed
84
+ form of the same value in every capture).
85
+ model: The gateway hardware model (``"VG34"`` captured).
86
+ """
87
+
88
+ serial: str
89
+ model: str
90
+
91
+
92
+ class VehicleStaticAssignedDriverRef(ResponseModel):
93
+ """The statically assigned driver reference: id and display name."""
94
+
95
+ id: str
96
+ name: str
97
+
98
+
99
+ class Vehicle(ResponseModel):
100
+ """One Samsara fleet vehicle.
101
+
102
+ A pure mirror of the captured fields (``tags`` excluded, module
103
+ docstring). Field semantics are Samsara's; no value is derived or
104
+ interpreted here.
105
+
106
+ Attributes:
107
+ id: Samsara's vehicle id -- a numeric string, mirrored as
108
+ string.
109
+ name: The unit's display name; units with no gateway carry
110
+ serial-shaped default names (captured).
111
+ notes: Free-form notes; ``""`` on every captured record,
112
+ mirrored verbatim.
113
+ harsh_acceleration_setting_type: Settings vocabulary
114
+ (``"automatic"`` captured), mirrored, never interpreted.
115
+ vehicle_regulation_mode: ``"regulated"`` / ``"unregulated"``
116
+ captured, mirrored, never interpreted.
117
+ created_at_time: Record creation (UTC).
118
+ updated_at_time: Last record update (UTC).
119
+ camera_serial: Dash-cam serial, dashed rendering (404/608).
120
+ external_ids: The open external-id map's modeled slice
121
+ (595/608).
122
+ gateway: The installed gateway reference (509/608; absent on
123
+ unplugged units -- the minimal shape).
124
+ serial: The gateway serial, undashed (509/608; the dashed twin
125
+ lives on ``gateway.serial``).
126
+ esn: Electronic serial number; both captured shapes are
127
+ alphanumeric strings (255/608).
128
+ license_plate: Registration plate (410/608).
129
+ make: Manufacturer (576/608).
130
+ model: Vehicle model name (576/608).
131
+ vin: Vehicle identification number (576/608).
132
+ year: Model year; a quoted integer on the wire, typed by lax
133
+ coercion (576/608).
134
+ static_assigned_driver: Statically assigned driver reference
135
+ (198/608).
136
+ aux_input_type1: Auxiliary input assignment (12/608;
137
+ ``"powerTakeOff"`` captured), mirrored, never interpreted.
138
+ """
139
+
140
+ model_config = ConfigDict(alias_generator=to_camel)
141
+
142
+ # Identity and lifecycle.
143
+ id: str
144
+ name: str
145
+ notes: str
146
+ harsh_acceleration_setting_type: str
147
+ vehicle_regulation_mode: str
148
+ created_at_time: datetime
149
+ updated_at_time: datetime
150
+
151
+ # Installed hardware.
152
+ camera_serial: str | None = None
153
+ external_ids: VehicleExternalIds | None = None
154
+ gateway: VehicleGatewayRef | None = None
155
+ serial: str | None = None
156
+ esn: str | None = None
157
+
158
+ # Vehicle identity.
159
+ license_plate: str | None = None
160
+ make: str | None = None
161
+ model: str | None = None
162
+ vin: str | None = None
163
+ year: int | None = None
164
+
165
+ # Assignment.
166
+ static_assigned_driver: VehicleStaticAssignedDriverRef | None = None
167
+ aux_input_type1: str | None = None