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,43 @@
1
+ """Motive response models; the face re-exports each endpoint module's models."""
2
+
3
+ from fleetpull.models.motive.driver_idle_rollup import DriverIdleRollup
4
+ from fleetpull.models.motive.driving_period import DrivingPeriod
5
+ from fleetpull.models.motive.group import Group
6
+ from fleetpull.models.motive.idle_event import IdleEvent
7
+ from fleetpull.models.motive.shared import (
8
+ EldDeviceInfo,
9
+ MotiveWindowStamp,
10
+ UserSummary,
11
+ VehicleSummary,
12
+ )
13
+ from fleetpull.models.motive.user import User
14
+ from fleetpull.models.motive.vehicle import (
15
+ AvailabilityDetails,
16
+ AvailabilityStatus,
17
+ Vehicle,
18
+ VehicleStatus,
19
+ )
20
+ from fleetpull.models.motive.vehicle_location import (
21
+ VehicleLocation,
22
+ VehicleLocationType,
23
+ )
24
+ from fleetpull.models.motive.vehicle_utilization import VehicleUtilization
25
+
26
+ __all__: list[str] = [
27
+ 'AvailabilityDetails',
28
+ 'AvailabilityStatus',
29
+ 'DriverIdleRollup',
30
+ 'DrivingPeriod',
31
+ 'EldDeviceInfo',
32
+ 'Group',
33
+ 'IdleEvent',
34
+ 'MotiveWindowStamp',
35
+ 'User',
36
+ 'UserSummary',
37
+ 'Vehicle',
38
+ 'VehicleLocation',
39
+ 'VehicleLocationType',
40
+ 'VehicleStatus',
41
+ 'VehicleSummary',
42
+ 'VehicleUtilization',
43
+ ]
@@ -0,0 +1,109 @@
1
+ # src/fleetpull/models/motive/driver_idle_rollup.py
2
+ """Motive DriverIdleRollup response model
3
+ (``GET /v2/driver_utilization``, post-decoder window-stamped grain).
4
+
5
+ Written from captured live responses (2026-07-21 probe session: 100
6
+ records sampled, structurally uniform), never from docs. NOTE THE
7
+ NAMING: the wire's OWN envelope vocabulary is
8
+ ``driver_idle_rollups``/``driver_idle_rollup`` -- different from its
9
+ ``/v2/driver_utilization`` path -- and the model mirrors the wire's
10
+ vocabulary (the endpoint records the legacy-name mapping in
11
+ ``ENDPOINTS.md``).
12
+
13
+ The model mirrors the record ``MotiveWindowReportPageDecoder`` emits:
14
+ ``window_start`` / ``window_end`` are DECODER-SYNTHESIZED
15
+ (``windowStartDate``/``windowEndDate``) -- rollup rows carry NO date or
16
+ time identity of any kind, so the decoder stamps each row with the
17
+ window the SENT spec asked for, copied verbatim from its
18
+ ``start_date``/``end_date`` date labels, and ``MotiveWindowStamp``
19
+ lifts each label to its UTC-midnight instant (``shared.py``);
20
+ everything else is wire-verbatim.
21
+
22
+ **THE COMPANY-LOCAL CAVEAT (the documented obligation on this
23
+ mirror).** The date labels are interpreted in COMPANY-LOCAL days -- the
24
+ account's ``/v1/companies`` capture carries a company-local zone at a
25
+ UTC-5 offset (DESIGN section 8) -- so each row is the provider's rollup
26
+ over its company-local day(s), mirrored verbatim. The window stamps
27
+ carry the day LABELS, not UTC day boundaries; nothing here converts
28
+ anything.
29
+
30
+ **THE ROLLUP GRAIN IS THE REQUEST WINDOW -- proven on this surface;
31
+ do not sum day rows (precedent-based).** A quiet single day returned 13
32
+ rows and a six-day window 653 -- one rollup row per driver with
33
+ activity in the window, per window (contrast the vehicle arm's
34
+ whole-fleet-every-window population). The grain forces the binding's
35
+ ``fixed_unit_days=1``. Additivity was NOT probed here; on the provider
36
+ family's only probed rollup surfaces (the Samsara fuel-energy pair) day
37
+ rollups were NON-ADDITIVE into wider windows (89/267 mismatched), so
38
+ the same posture applies as precedent: day rows MUST NOT be summed to
39
+ reproduce a wider window's rollup.
40
+
41
+ ``driver`` is the shared 8-key ``UserSummary`` (its fourth carrying
42
+ surface) and NULLABLE: the census found it populated on 99 of 100
43
+ sampled rows and NULL on one -- an UNATTRIBUTED rollup bucket the
44
+ provider emits alongside the per-driver rows, mirrored as a null ref,
45
+ never dropped. The metric core is REQUIRED (every key on every sampled
46
+ record; the rollup-surface reasoning -- metrics are computed per
47
+ window, with no absence mechanism to be conservative about), and the
48
+ window stamps are required STRUCTURALLY. ``idle_time`` and
49
+ ``driving_time`` are bare INTS on this arm -- floats on the vehicle
50
+ arm -- and each arm mirrors its own wire.
51
+ """
52
+
53
+ from pydantic import Field
54
+
55
+ from fleetpull.model_contract import ResponseModel
56
+ from fleetpull.models.motive.shared import MotiveWindowStamp, UserSummary
57
+
58
+ __all__: list[str] = ['DriverIdleRollup']
59
+
60
+
61
+ class DriverIdleRollup(ResponseModel):
62
+ """One driver's idle rollup over exactly one request window.
63
+
64
+ A pure mirror of the window-stamped post-decoder record (module
65
+ docstring: the window stamps are decoder-synthesized from the sent
66
+ spec's date labels; everything else is wire-verbatim). Field
67
+ semantics and units are Motive's; no value is derived or
68
+ interpreted here. Day rows MUST NOT be summed to reproduce a wider
69
+ window's rollup (module docstring -- the provider-family
70
+ precedent).
71
+
72
+ Attributes:
73
+ window_start: The request window's start-date label
74
+ (decoder-synthesized ``windowStartDate``, verbatim from the
75
+ sent ``start_date``) at UTC midnight -- the event-time
76
+ column: the row's time identity is the window that produced
77
+ it. A COMPANY-LOCAL day label (module docstring).
78
+ window_end: The request window's end-date label
79
+ (decoder-synthesized ``windowEndDate``, verbatim from the
80
+ sent ``end_date``) at UTC midnight. INCLUSIVE: at the fixed
81
+ 1-day unit it equals ``window_start``.
82
+ driver: The rollup's driver entity (the shared ``UserSummary``);
83
+ NULL on the unattributed rollup bucket row (module
84
+ docstring), populated with the full 8-key shape otherwise.
85
+ utilization: The provider's utilization figure for the window,
86
+ mirrored uninterpreted.
87
+ driving_time: Time spent driving in the window, a bare int
88
+ (contrast the vehicle arm's float durations -- each arm
89
+ mirrors its own wire).
90
+ idle_time: Time spent idling in the window, a bare int.
91
+ driving_fuel: Fuel used while driving in the window, provider
92
+ units mirrored verbatim.
93
+ idle_fuel: Fuel used while idling in the window.
94
+ """
95
+
96
+ # Decoder-synthesized window identity (the sent spec's own labels).
97
+ window_start: MotiveWindowStamp = Field(alias='windowStartDate')
98
+ window_end: MotiveWindowStamp = Field(alias='windowEndDate')
99
+
100
+ # The rollup's entity; null on the unattributed bucket row.
101
+ driver: UserSummary | None = None
102
+
103
+ # The wire-verbatim metric core (required -- rollup surfaces have no
104
+ # absence arm; int durations on THIS arm, provider units verbatim).
105
+ utilization: float
106
+ driving_time: int
107
+ idle_time: int
108
+ driving_fuel: float
109
+ idle_fuel: float
@@ -0,0 +1,82 @@
1
+ # src/fleetpull/models/motive/driving_period.py
2
+ """The Motive driving-period response model (captured 2026-07-15).
3
+
4
+ One record per contiguous driving span from ``GET /v1/driving_periods``.
5
+ Completed records reproduce ``duration = end_time - start_time`` exactly
6
+ (float seconds); in-progress records (``status: "in_progress"``) null
7
+ every end-side field — ``end_time``, ``end_kilometers``, ``distance``,
8
+ the destination and its coordinates — while ``duration`` keeps counting
9
+ as a fractional elapsed value. ``distance`` is a provider-formatted
10
+ string (``"42.2 mi"``) mirrored verbatim — the merely-ugly side of the
11
+ coercion boundary; real distance arithmetic uses the kilometer odometer
12
+ fields. ``start_time`` was never observed null and is the endpoint's
13
+ retrieval anchor (start-anchored UTC window matching, DESIGN §8).
14
+
15
+ Excluded fields, per capture discipline: ``source`` and the four
16
+ ``*_hvb_*`` battery fields were never observed non-null (a diesel
17
+ fleet's capture), so no honest dtype exists; they join the model when a
18
+ capture pins their types.
19
+ """
20
+
21
+ from datetime import datetime
22
+
23
+ from pydantic import Field
24
+
25
+ from fleetpull.model_contract import ResponseModel
26
+ from fleetpull.models.motive.shared import UserSummary, VehicleSummary
27
+
28
+ __all__: list[str] = ['DrivingPeriod']
29
+
30
+
31
+ class DrivingPeriod(ResponseModel):
32
+ """One contiguous driving span for one vehicle.
33
+
34
+ Attributes:
35
+ period_id: Motive's internal period identifier (wire key ``id``).
36
+ start_time: UTC start of the span; the retrieval anchor.
37
+ end_time: UTC end of the span; null while the span is in progress.
38
+ status: Free-form lifecycle string (``"complete"`` /
39
+ ``"in_progress"`` observed), mirrored, never interpreted.
40
+ period_type: Free-form span-type string (wire key ``type``;
41
+ ``"driving"`` observed), mirrored, never interpreted.
42
+ annotation_status: Provider annotation state; null when absent.
43
+ notes: Free-form annotation text; null when absent.
44
+ duration: Span length in float seconds; on an in-progress record,
45
+ the elapsed value so far.
46
+ start_kilometers: Odometer at span start, kilometers.
47
+ end_kilometers: Odometer at span end; null while in progress.
48
+ driver: Attributed driver; null when the span is unattributed.
49
+ vehicle: The vehicle the span belongs to.
50
+ origin: Provider-formatted start address, mirrored verbatim
51
+ (empty strings normalize to null at the DataFrame boundary);
52
+ null when absent.
53
+ origin_lat: Start latitude; null when the provider has none.
54
+ origin_lon: Start longitude; null when the provider has none.
55
+ destination_lat: End latitude; null while in progress.
56
+ destination_lon: End longitude; null while in progress.
57
+ destination: Provider-formatted end address, mirrored verbatim —
58
+ the captured in-progress record carries ``""`` here (nulled
59
+ at the DataFrame boundary); null when absent.
60
+ distance: Provider-formatted distance string (``"42.2 mi"``),
61
+ mirrored verbatim; null while in progress.
62
+ """
63
+
64
+ period_id: int = Field(alias='id')
65
+ start_time: datetime
66
+ end_time: datetime | None = None
67
+ status: str
68
+ period_type: str = Field(alias='type')
69
+ annotation_status: int | None = None
70
+ notes: str | None = None
71
+ duration: float
72
+ start_kilometers: float
73
+ end_kilometers: float | None = None
74
+ driver: UserSummary | None = None
75
+ vehicle: VehicleSummary
76
+ origin: str | None = None
77
+ origin_lat: float | None = None
78
+ origin_lon: float | None = None
79
+ destination_lat: float | None = None
80
+ destination_lon: float | None = None
81
+ destination: str | None = None
82
+ distance: str | None = None
@@ -0,0 +1,49 @@
1
+ # src/fleetpull/models/motive/group.py
2
+ """The Motive group response model (``GET /v1/groups``, captured 2026-07-21).
3
+
4
+ One record per organizational group. Written from a whole-population walk
5
+ (152 records, four pages at ``per_page`` 50): every modeled key was present
6
+ on all 152 records, so everything here is required -- nullability is the
7
+ only optionality, and it follows the census exactly. ``parent_id`` is null
8
+ on root groups and an existing group id otherwise (the groups form a
9
+ tree).
10
+
11
+ ``user`` is the group's owner/creator reference -- the compact
12
+ users-endpoint account shape, carried by the shared ``UserSummary`` (the
13
+ ``shared.py`` promotion rule: the identical wire shape already rides the
14
+ driving-period and idle-event driver references). On THIS surface the
15
+ census observed ``username`` and ``driver_company_id`` null on all 152
16
+ records and ``role``/``status``/names populated on all 152;
17
+ ``UserSummary``'s union-lax optionality absorbs both facts, and the
18
+ sibling surfaces are where the null-here keys carry values.
19
+ """
20
+
21
+ from pydantic import Field
22
+
23
+ from fleetpull.model_contract import ResponseModel
24
+ from fleetpull.models.motive.shared import UserSummary
25
+
26
+ __all__: list[str] = ['Group']
27
+
28
+
29
+ class Group(ResponseModel):
30
+ """One Motive organizational group.
31
+
32
+ A pure mirror of the whole-population census: five keys, all present
33
+ on every record.
34
+
35
+ Attributes:
36
+ group_id: Motive's internal group identifier (wire key ``id``).
37
+ company_id: Parent company identifier.
38
+ name: The group's display name.
39
+ parent_id: The parent group's id; null on root groups (the
40
+ groups form a tree).
41
+ user: The group's owner/creator reference (the shared compact
42
+ users-endpoint account shape).
43
+ """
44
+
45
+ group_id: int = Field(alias='id')
46
+ company_id: int
47
+ name: str
48
+ parent_id: int | None
49
+ user: UserSummary
@@ -0,0 +1,77 @@
1
+ # src/fleetpull/models/motive/idle_event.py
2
+ """The Motive idle-event response model (captured 2026-07-15).
3
+
4
+ One record per engine-idle interval from ``GET /v1/idle_events``. Both
5
+ timestamps were always present in capture — the endpoint has no
6
+ in-progress shape analogue. ``veh_fuel_start`` / ``veh_fuel_end`` are the
7
+ ELD's cumulative fuel counters, mirrored as-is. The ``rg_*`` fields are
8
+ the provider's reverse-geocode metadata; when ``rg_match`` is false,
9
+ ``location`` carries a distance-direction prefix (``"2.6 mi NW of …"``)
10
+ instead of a bare place name — both formats mirror verbatim.
11
+
12
+ The endpoint's date window is interpreted on company-local day boundaries
13
+ and matched by overlap (DESIGN §8, captured 2026-07-15) — the reason its
14
+ endpoint leaf pads the wire window; nothing about that reaches this
15
+ model, whose ``start_time`` remains the routing anchor.
16
+ """
17
+
18
+ from datetime import datetime
19
+
20
+ from pydantic import Field
21
+
22
+ from fleetpull.model_contract import ResponseModel
23
+ from fleetpull.models.motive.shared import (
24
+ EldDeviceInfo,
25
+ UserSummary,
26
+ VehicleSummary,
27
+ )
28
+
29
+ __all__: list[str] = ['IdleEvent']
30
+
31
+
32
+ class IdleEvent(ResponseModel):
33
+ """One engine-idle interval for one vehicle.
34
+
35
+ Attributes:
36
+ event_id: Motive's internal event identifier (wire key ``id``).
37
+ start_time: UTC start of the idle interval; the routing anchor.
38
+ end_time: UTC end of the idle interval.
39
+ veh_fuel_start: ELD cumulative fuel counter at interval start;
40
+ null when the vehicle reports no fuel counters
41
+ (live-observed 2026-07-16).
42
+ veh_fuel_end: ELD cumulative fuel counter at interval end; null
43
+ when the vehicle reports no fuel counters.
44
+ lat: Event latitude.
45
+ lon: Event longitude.
46
+ city: Reverse-geocoded place name.
47
+ state: Reverse-geocoded state / province code.
48
+ rg_brg: Reverse-geocode bearing from the matched place, degrees.
49
+ rg_km: Reverse-geocode distance from the matched place,
50
+ kilometers.
51
+ rg_match: Whether the reverse geocoder matched a place directly.
52
+ end_type: Free-form interval-end reason (``"engine_stop"`` /
53
+ ``"vehicle_moving"`` observed), mirrored, never interpreted.
54
+ driver: Attributed driver; null when the idle is unattributed.
55
+ vehicle: The vehicle the interval belongs to.
56
+ eld_device: The ELD hardware that reported the interval.
57
+ location: Provider-formatted place string; carries a
58
+ distance-direction prefix when ``rg_match`` is false.
59
+ """
60
+
61
+ event_id: int = Field(alias='id')
62
+ start_time: datetime
63
+ end_time: datetime
64
+ veh_fuel_start: float | None = None
65
+ veh_fuel_end: float | None = None
66
+ lat: float
67
+ lon: float
68
+ city: str
69
+ state: str
70
+ rg_brg: float
71
+ rg_km: float
72
+ rg_match: bool
73
+ end_type: str
74
+ driver: UserSummary | None = None
75
+ vehicle: VehicleSummary
76
+ eld_device: EldDeviceInfo
77
+ location: str
@@ -0,0 +1,192 @@
1
+ # src/fleetpull/models/motive/shared.py
2
+ """Motive embedded shapes shared across more than one endpoint.
3
+
4
+ This module holds the per-record building blocks that appear on multiple
5
+ Motive responses — ``UserSummary`` and ``EldDeviceInfo`` on the vehicle
6
+ and vehicle-location records (and the driving-period and idle-event
7
+ records), ``VehicleSummary`` on the driving-period, idle-event, and
8
+ vehicle-utilization records — plus ``MotiveWindowStamp``, the
9
+ decoder-synthesized window-identity type the utilization rollup pair's
10
+ models share. Endpoint-private sub-shapes live in their endpoint module,
11
+ not here; a shape is promoted into this module only once a second
12
+ endpoint actually uses it.
13
+ """
14
+
15
+ import re
16
+ from datetime import UTC, date, datetime, time
17
+ from typing import Annotated, Final
18
+
19
+ from pydantic import BeforeValidator, Field
20
+
21
+ from fleetpull.model_contract import ResponseModel, empty_str_to_none
22
+ from fleetpull.vocabulary import JsonValue
23
+
24
+ __all__: list[str] = [
25
+ 'EldDeviceInfo',
26
+ 'MotiveWindowStamp',
27
+ 'UserSummary',
28
+ 'VehicleSummary',
29
+ ]
30
+
31
+
32
+ # Exactly the dashed calendar-date label the builders render; fullmatch
33
+ # keeps date.fromisoformat's laxer forms (compact YYYYMMDD, ISO week
34
+ # dates) failing loudly as the wiring drift they would be.
35
+ _DATE_LABEL_PATTERN: Final[re.Pattern[str]] = re.compile(r'\d{4}-\d{2}-\d{2}')
36
+
37
+
38
+ def _date_label_to_utc_midnight(value: JsonValue | datetime) -> datetime:
39
+ """The ``MotiveWindowStamp`` ingress: lift a date label to an instant.
40
+
41
+ The Motive window-report decoder stamps each rollup row with the
42
+ sent spec's ``start_date``/``end_date`` values VERBATIM — day-only
43
+ ``YYYY-MM-DD`` labels, never instants. A date label cannot validate
44
+ into the timezone-aware datetime the event-time machinery requires
45
+ (an unzoned event time is never assumed), so this lift is the
46
+ structural type recovery DESIGN section 9 allows on a mirror: the
47
+ calendar-day label is preserved exactly (the result's ``.date()`` IS
48
+ the label) and UTC midnight is attached as the label's canonical
49
+ instant representation — a labeling convention for partition
50
+ routing, never a timezone conversion of the data. Strict by design:
51
+ the builder only ever renders date labels, so any other string —
52
+ including a full RFC3339 datetime — is a wiring drift that should
53
+ fail validation loudly, not pass mangled.
54
+
55
+ Args:
56
+ value: The raw stamp value, or an already-recovered datetime on
57
+ a Pydantic revalidation path.
58
+
59
+ Returns:
60
+ The label's UTC-midnight instant (passthrough for an
61
+ already-recovered tz-aware value).
62
+
63
+ Raises:
64
+ ValueError: ``value`` is not exactly a dashed ``YYYY-MM-DD``
65
+ label (the laxer ``date.fromisoformat`` forms -- compact
66
+ digits, week dates -- reject too), or is a NAIVE datetime.
67
+ """
68
+ if isinstance(value, datetime):
69
+ if value.tzinfo is None:
70
+ raise ValueError(
71
+ 'window stamp datetime is naive -- an unzoned event time '
72
+ 'is never assumed'
73
+ )
74
+ return value
75
+ if isinstance(value, str) and _DATE_LABEL_PATTERN.fullmatch(value):
76
+ return datetime.combine(date.fromisoformat(value), time.min, tzinfo=UTC)
77
+ raise ValueError(f'expected a YYYY-MM-DD window-stamp label, got {value!r}')
78
+
79
+
80
+ # The window-stamp field type the utilization rollup models use. Plain
81
+ # assignment, not a `type` statement: Pydantic must evaluate the
82
+ # Annotated form eagerly for the metadata lift (the GeotabTimeSpan
83
+ # precedent).
84
+ MotiveWindowStamp = Annotated[datetime, BeforeValidator(_date_label_to_utc_midnight)]
85
+
86
+
87
+ class UserSummary(ResponseModel):
88
+ """Abbreviated user reference embedded in other Motive records.
89
+
90
+ The compact user-account shape that appears when a user is referenced
91
+ from another entity: the vehicle record's ``permanent_driver`` /
92
+ ``current_driver``, the driving-period and idle-event ``driver``
93
+ references, the group record's owner ``user``, and the
94
+ driver-idle-rollup ``driver`` reference (the fourth carrying
95
+ surface, captured 2026-07-21: the exact 8-key shape, populated on
96
+ every attributed rollup and null on the unattributed bucket row).
97
+ The full record comes from the users endpoint. Optionality is
98
+ union-lax across the carrying surfaces (a key populated on one
99
+ surface may be null on another -- e.g. ``username`` carries values
100
+ on driver references and was null on all 152 group owners); each
101
+ consumer's docstring pins its own surface's census.
102
+
103
+ ``status`` and ``role`` are modeled as free-form ``str`` rather than
104
+ constrained enums: Motive documents both as plain strings, fleetpull
105
+ does not interpret them, and mirroring them as strings keeps the model
106
+ faithful and evolution-safe.
107
+
108
+ Attributes:
109
+ user_id: Motive's internal user identifier (wire key ``id``).
110
+ first_name: Driver's first name.
111
+ last_name: Driver's last name.
112
+ username: Login username; null when unset.
113
+ email: Driver's email address; null when unset.
114
+ driver_company_id: Company-assigned driver identifier; null when
115
+ unset.
116
+ status: Free-form account-status string; null when absent.
117
+ role: Free-form user-role string; null when absent.
118
+ """
119
+
120
+ user_id: int = Field(alias='id')
121
+ first_name: str
122
+ last_name: str
123
+ username: str | None = None
124
+ email: str | None = None
125
+ driver_company_id: str | None = None
126
+ status: str | None = None
127
+ role: str | None = None
128
+
129
+
130
+ class VehicleSummary(ResponseModel):
131
+ """Abbreviated vehicle reference embedded in other Motive records.
132
+
133
+ The compact vehicle shape that appears when a vehicle is referenced
134
+ from another record: the driving-period and idle-event records
135
+ (captured 2026-07-15) and the vehicle-utilization rollup record
136
+ (captured 2026-07-21 -- the same seven wire keys exactly, its third
137
+ carrying surface). The full vehicle record comes from the vehicles
138
+ endpoint. Optionality is union-lax across the carrying surfaces
139
+ (the UserSummary posture): ``vin`` carried values on every event
140
+ record but is null on some utilization rows, so it is nullable
141
+ here; each consumer's docstring pins its own surface's census.
142
+
143
+ ``year`` arrives as a quoted integer (``"2022"``); lax coercion types
144
+ it, and the captured ``"0"`` not-configured sentinel mirrors as
145
+ ``0``, never interpreted. The empty-string wire shape (live-observed
146
+ 2026-07-16, failing ``int_parsing``) is lifted by a before-validator
147
+ — the type-recovery case DESIGN section 9 allows on a mirror, since
148
+ ``""`` cannot validate as an integer at all. ``make`` and ``model``
149
+ arrive as empty strings where the provider has no value and mirror
150
+ verbatim: empty strings normalize to null at the DataFrame boundary,
151
+ never on a string field of the model.
152
+
153
+ Attributes:
154
+ vehicle_id: Motive's internal vehicle identifier (wire key ``id``).
155
+ number: Company-assigned unit number.
156
+ year: Model year; ``0`` is the provider's not-configured
157
+ sentinel; null when the provider sends an empty string or
158
+ nothing.
159
+ make: Manufacturer; the captured empty string mirrors verbatim;
160
+ null when absent.
161
+ model: Model name; the captured empty string mirrors verbatim;
162
+ null when absent.
163
+ vin: Vehicle identification number; null where the provider has
164
+ none (observed on the vehicle-utilization surface).
165
+ metric_units: Whether the vehicle's Motive profile reports metric.
166
+ """
167
+
168
+ vehicle_id: int = Field(alias='id')
169
+ number: str
170
+ year: Annotated[int | None, BeforeValidator(empty_str_to_none)] = None
171
+ make: str | None = None
172
+ model: str | None = None
173
+ vin: str | None = None
174
+ metric_units: bool
175
+
176
+
177
+ class EldDeviceInfo(ResponseModel):
178
+ """ELD hardware embedded in other Motive records.
179
+
180
+ Identifies the physical telematics device installed in a vehicle. The
181
+ source documents this shape on both the vehicle record and the
182
+ vehicle-location record.
183
+
184
+ Attributes:
185
+ device_id: Motive's internal device identifier (wire key ``id``).
186
+ identifier: Device serial / hardware identifier.
187
+ model: Device model name (e.g. the Motive LBB designation).
188
+ """
189
+
190
+ device_id: int = Field(alias='id')
191
+ identifier: str
192
+ model: str