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,217 @@
1
+ # src/fleetpull/models/motive/user.py
2
+ """The Motive user response model (``GET /v1/users``, captured 2026-07-21).
3
+
4
+ One record per account — driver or otherwise. Written from a
5
+ whole-population walk (2,665 records, 27 pages at ``per_page`` 100) whose
6
+ shape is perfectly role-partitioned: ``role='driver'`` records (2,359)
7
+ carry a driver-only key block on top of the shared keys, while ``admin``
8
+ (32) and ``fleet_user`` (274) records carry exactly the shared keys —
9
+ zero partial-presence keys within any role. This is the Samsara drivers
10
+ decision-1 posture with the split inverted: one population and ONE
11
+ dataset, but a role-dependent shape rather than a provider filter quirk —
12
+ the ``role`` column carries the split, and the driver-only block is
13
+ ABSENT, not null, on non-driver records.
14
+
15
+ Requiredness mirrors the census: the shared keys, present on every one of
16
+ the 2,665 records, are required (nullable exactly where the census
17
+ observed null); the driver-only keys are optional on the model — absent
18
+ for non-drivers — with nullability per the census inside the driver role.
19
+ ``role`` (``driver``/``admin``/``fleet_user`` observed) and ``status``
20
+ (``active``: 1,020 / ``deactivated``: 1,645) are census-closed
21
+ vocabularies only, not API-enforced on output, so they mirror as plain
22
+ strings; likewise ``duty_status`` (observed ``on_duty``/``off_duty``/
23
+ ``driving``), ``eld_mode`` (``logs``/``none``/``exempt``),
24
+ ``violation_alerts`` (``never``/``1_hour``/``45_minutes``/
25
+ ``30_minutes``/``15_minutes``), and the HOS cycle pair (``cycle2``
26
+ observed values like ``70_8_2020``/``60_7_o_2020`` on 37 drivers).
27
+ ``joined_at`` is a DATE-ONLY wire value (``YYYY-MM-DD``, 34 of 2,359
28
+ drivers populated -- the whole-population value census), recovered as a
29
+ ``date``; null on the rest.
30
+
31
+ The always-present key partition, exactly: 22 keys ride every record
32
+ (20 modeled; ``external_ids`` and ``phone_ext`` never populated
33
+ anywhere); ``admin``/``fleet_user`` records add 3 keys of their own
34
+ (``expires_at``, ``phone2``, ``phone_country_code2`` -- never populated
35
+ among them); driver records add 39 (38 modeled;
36
+ ``associated_dispatcher_id`` never populated).
37
+
38
+ Excluded fields, per capture discipline: the six never-populated keys
39
+ named above were present but always null/empty across all 2,665
40
+ records -- the value types are unobservable (the value-unobservable
41
+ exclusion rule); they join the model when a capture types them
42
+ (``extra='ignore'`` makes exclusion exactly "don't model it"). Contrast
43
+ ``joined_at``: value-OBSERVED (34 dates), hence modeled.
44
+ """
45
+
46
+ from datetime import date, datetime
47
+
48
+ from pydantic import Field
49
+
50
+ from fleetpull.model_contract import ResponseModel
51
+
52
+ __all__: list[str] = ['User']
53
+
54
+
55
+ class User(ResponseModel):
56
+ """One Motive user account: a driver, admin, or fleet_user.
57
+
58
+ A pure mirror of the role-partitioned census. Groups below follow
59
+ the partition: the shared block first (required on every record),
60
+ then the driver-only block (absent, not null, on non-driver
61
+ records — every field there defaults to ``None``). Field semantics
62
+ are Motive's; no value is derived or interpreted here.
63
+
64
+ Attributes:
65
+ user_id: Motive's internal user identifier (wire key ``id``).
66
+ first_name: User's first name.
67
+ last_name: User's last name.
68
+ email: User's email address; null when unset.
69
+ phone: Contact phone number; null when unset.
70
+ phone_country_code: Phone country code; null when unset.
71
+ company_reference_id: Company-assigned reference identifier;
72
+ null when unset.
73
+ role: Free-form user-role string; ``driver`` / ``admin`` /
74
+ ``fleet_user`` observed — the column that carries the
75
+ shape partition.
76
+ status: Free-form account-status string; ``active`` /
77
+ ``deactivated`` observed.
78
+ group_ids: Identifiers of the groups the user belongs to;
79
+ often empty.
80
+ metric_units: Whether the account displays metric units.
81
+ time_zone: Rails-style time-zone display name; null when unset.
82
+ created_at: When the account was created.
83
+ updated_at: Last modification timestamp.
84
+ mobile_current_sign_in_at: Current mobile-session sign-in;
85
+ null when none.
86
+ mobile_last_active_at: Last mobile activity; null when none.
87
+ mobile_last_sign_in_at: Previous mobile sign-in; null when none.
88
+ web_current_sign_in_at: Current web-session sign-in; null when
89
+ none.
90
+ web_last_active_at: Last web activity; null when none.
91
+ web_last_sign_in_at: Previous web sign-in; null when none.
92
+ username: Login username; driver-only; null when unset.
93
+ driver_company_id: Company-assigned driver identifier;
94
+ driver-only; null when unset.
95
+ drivers_license_number: Driver's license number; driver-only;
96
+ null when unset.
97
+ drivers_license_state: Driver's license state/province;
98
+ driver-only; null when unset.
99
+ joined_at: Driver-only; the driver's join date -- a DATE-ONLY
100
+ wire value (``YYYY-MM-DD``; 34 of 2,359 populated), null on
101
+ the rest.
102
+ conservative nullable-str posture (see module docstring).
103
+ duty_status: Free-form HOS duty-status string; driver-only.
104
+ eld_mode: Free-form ELD-mode string; driver-only.
105
+ cycle: Free-form HOS cycle string; driver-only; null when
106
+ unset.
107
+ cycle2: Second-jurisdiction HOS cycle; driver-only; null when
108
+ unset.
109
+ violation_alerts: Free-form violation-alert cadence string;
110
+ driver-only.
111
+ carrier_name: Carrier name; driver-only.
112
+ carrier_street: Carrier street address; driver-only.
113
+ carrier_city: Carrier city; driver-only.
114
+ carrier_state: Carrier state; driver-only.
115
+ carrier_zip: Carrier postal code; driver-only.
116
+ terminal_street: Home-terminal street; driver-only; null when
117
+ unset.
118
+ terminal_city: Home-terminal city; driver-only; null when
119
+ unset.
120
+ terminal_state: Home-terminal state; driver-only; null when
121
+ unset.
122
+ terminal_zip: Home-terminal postal code; driver-only; null
123
+ when unset.
124
+ exception_24_hour_restart: HOS exception flag; driver-only.
125
+ exception_8_hour_break: HOS exception flag; driver-only.
126
+ exception_adverse_driving: HOS exception flag; driver-only.
127
+ exception_ca_farm_school_bus: HOS exception flag; driver-only.
128
+ exception_short_haul: HOS exception flag; driver-only.
129
+ exception_wait_time: HOS exception flag; driver-only.
130
+ exception_24_hour_restart2: Second-jurisdiction HOS exception
131
+ flag; driver-only (likewise the other ``*2`` flags).
132
+ exception_8_hour_break2: See above; driver-only.
133
+ exception_adverse_driving2: See above; driver-only.
134
+ exception_ca_farm_school_bus2: See above; driver-only.
135
+ exception_short_haul2: See above; driver-only.
136
+ exception_wait_time2: See above; driver-only.
137
+ export_combined: Log-export setting; driver-only.
138
+ export_odometers: Log-export setting; driver-only.
139
+ export_recap: Log-export setting; driver-only.
140
+ manual_driving_enabled: Driving-mode setting; driver-only.
141
+ minute_logs: Minute-grain logging setting; driver-only.
142
+ personal_conveyance_enabled: Driving-mode setting; driver-only.
143
+ yard_moves_enabled: Driving-mode setting; driver-only.
144
+ """
145
+
146
+ # The shared block: present on every record of every role.
147
+ user_id: int = Field(alias='id')
148
+ first_name: str
149
+ last_name: str
150
+ email: str | None
151
+ phone: str | None
152
+ phone_country_code: str | None
153
+ company_reference_id: str | None
154
+ role: str
155
+ status: str
156
+ group_ids: list[int]
157
+ metric_units: bool
158
+ time_zone: str | None
159
+ created_at: datetime
160
+ updated_at: datetime
161
+ mobile_current_sign_in_at: datetime | None
162
+ mobile_last_active_at: datetime | None
163
+ mobile_last_sign_in_at: datetime | None
164
+ web_current_sign_in_at: datetime | None
165
+ web_last_active_at: datetime | None
166
+ web_last_sign_in_at: datetime | None
167
+
168
+ # The driver-only block: absent, not null, on non-driver records.
169
+ # Identity and licensing.
170
+ username: str | None = None
171
+ driver_company_id: str | None = None
172
+ drivers_license_number: str | None = None
173
+ drivers_license_state: str | None = None
174
+ joined_at: date | None = None
175
+
176
+ # HOS configuration.
177
+ duty_status: str | None = None
178
+ eld_mode: str | None = None
179
+ cycle: str | None = None
180
+ cycle2: str | None = None
181
+ violation_alerts: str | None = None
182
+
183
+ # Carrier and home-terminal identity.
184
+ carrier_name: str | None = None
185
+ carrier_street: str | None = None
186
+ carrier_city: str | None = None
187
+ carrier_state: str | None = None
188
+ carrier_zip: str | None = None
189
+ terminal_street: str | None = None
190
+ terminal_city: str | None = None
191
+ terminal_state: str | None = None
192
+ terminal_zip: str | None = None
193
+
194
+ # HOS exception flags (the *2 variants are the second jurisdiction).
195
+ exception_24_hour_restart: bool | None = None
196
+ exception_8_hour_break: bool | None = None
197
+ exception_adverse_driving: bool | None = None
198
+ exception_ca_farm_school_bus: bool | None = None
199
+ exception_short_haul: bool | None = None
200
+ exception_wait_time: bool | None = None
201
+ exception_24_hour_restart2: bool | None = None
202
+ exception_8_hour_break2: bool | None = None
203
+ exception_adverse_driving2: bool | None = None
204
+ exception_ca_farm_school_bus2: bool | None = None
205
+ exception_short_haul2: bool | None = None
206
+ exception_wait_time2: bool | None = None
207
+
208
+ # Log-export settings.
209
+ export_combined: bool | None = None
210
+ export_odometers: bool | None = None
211
+ export_recap: bool | None = None
212
+
213
+ # Driving-mode settings.
214
+ manual_driving_enabled: bool | None = None
215
+ minute_logs: bool | None = None
216
+ personal_conveyance_enabled: bool | None = None
217
+ yard_moves_enabled: bool | None = None
@@ -0,0 +1,162 @@
1
+ # src/fleetpull/models/motive/vehicle.py
2
+ """Motive vehicles-endpoint response model (``/v1/vehicles``).
3
+
4
+ Holds the ``Vehicle`` record and the shapes used only by it —
5
+ ``AvailabilityDetails`` and the ``VehicleStatus`` / ``AvailabilityStatus``
6
+ enums. Cross-endpoint embedded shapes (``UserSummary``, ``EldDeviceInfo``)
7
+ are imported from ``fleetpull.models.motive.shared``.
8
+
9
+ Pure API mirrors — typed fields and nothing else. No use-case logic, no
10
+ derived properties, no normalizing validators: flattening and schema
11
+ derivation are the records layer's generic concern (DESIGN §9), and
12
+ fleetpull assumes no end use, so a field is mirrored, never interpreted.
13
+ The response *wrapper* (the ``{"vehicles": [{"vehicle": {...}}]}`` envelope)
14
+ is not modeled here — the endpoints layer's extractor and paginator own it,
15
+ so this module mirrors only the inner per-vehicle object.
16
+ """
17
+
18
+ from datetime import datetime
19
+ from enum import StrEnum
20
+
21
+ from pydantic import Field
22
+
23
+ from fleetpull.model_contract import ResponseModel
24
+ from fleetpull.models.motive.shared import EldDeviceInfo, UserSummary
25
+
26
+ __all__: list[str] = [
27
+ 'AvailabilityDetails',
28
+ 'AvailabilityStatus',
29
+ 'Vehicle',
30
+ 'VehicleStatus',
31
+ ]
32
+
33
+
34
+ class VehicleStatus(StrEnum):
35
+ """Operational status for a Motive vehicle.
36
+
37
+ A closed mirror of Motive's documented status vocabulary. Kept as an
38
+ enum (not downgraded to ``str``) because the wire values match the
39
+ member values exactly — no normalizing logic is needed to land them,
40
+ so the enum stays a faithful mirror while adding documentation and
41
+ membership validation. Live responses show ``active`` and
42
+ ``deactivated``; ``inactive`` is documented.
43
+ """
44
+
45
+ ACTIVE = 'active'
46
+ INACTIVE = 'inactive'
47
+ DEACTIVATED = 'deactivated'
48
+
49
+
50
+ class AvailabilityStatus(StrEnum):
51
+ """In-service / out-of-service availability for a Motive vehicle.
52
+
53
+ Closed mirror of Motive's documented availability vocabulary; an exact
54
+ wire-to-member match, so it stays an enum for the same reason as
55
+ ``VehicleStatus``.
56
+ """
57
+
58
+ IN_SERVICE = 'in_service'
59
+ OUT_OF_SERVICE = 'out_of_service'
60
+
61
+
62
+ class AvailabilityDetails(ResponseModel):
63
+ """Availability status block embedded in the vehicle record.
64
+
65
+ Tracks whether a vehicle is in or out of service and when the status
66
+ last changed.
67
+
68
+ Attributes:
69
+ availability_status: Current in-service / out-of-service value.
70
+ updated_at: Timestamp of the last status change.
71
+ """
72
+
73
+ availability_status: AvailabilityStatus
74
+ updated_at: datetime
75
+
76
+
77
+ class Vehicle(ResponseModel):
78
+ """Complete vehicle record from Motive's vehicles endpoint.
79
+
80
+ A single fleet vehicle with its metadata, current and permanent driver
81
+ references, ELD device, and availability block. A pure mirror: every
82
+ field maps a Motive response field, with no derived or interpreted
83
+ values.
84
+
85
+ Attributes:
86
+ vehicle_id: Motive's internal vehicle identifier (wire key ``id``).
87
+ company_id: Parent company identifier.
88
+ number: User-assigned fleet/unit number.
89
+ status: Vehicle operational status.
90
+ ifta: Whether the vehicle is IFTA-reportable.
91
+ vin: Vehicle Identification Number; null when unset.
92
+ make: Manufacturer; null when unset.
93
+ model: Model name; null when unset.
94
+ year: Model year, mirrored as a string (Motive's convention); null
95
+ when unset.
96
+ license_plate_state: Registration state/province; null when unset.
97
+ license_plate_number: License plate number; null when unset.
98
+ license_plate_country_code: Registration country code; null when
99
+ unset. Present in live responses but absent from the predecessor
100
+ model — added here so the mirror is faithful to the wire.
101
+ metric_units: Whether the vehicle displays metric units.
102
+ fuel_type: Primary fuel type, mirrored as a free-form ``str``
103
+ rather than a constrained enum — Motive's casing varies and
104
+ fleetpull does not interpret the value, so a string mirror
105
+ avoids carrying a normalizing validator on an otherwise pure
106
+ model; null when unset.
107
+ prevent_auto_odometer_entry: Whether automatic odometer capture is
108
+ disabled.
109
+ notes: Free-form notes; null when unset.
110
+ incab_alert_live_stream_enable: Motive camera-config field; ``-1``
111
+ is Motive's "not configured" sentinel and the default.
112
+ driver_facing_camera: Camera-config field; ``-1`` sentinel default.
113
+ incab_audio_recording: Camera-config field; ``-1`` sentinel default.
114
+ group_ids: Identifiers of the groups the vehicle belongs to.
115
+ created_at: When the vehicle was added to Motive.
116
+ updated_at: Last modification timestamp.
117
+ permanent_driver: Permanently assigned driver; null when none.
118
+ availability_details: Current availability block; null when absent.
119
+ eld_device: Installed ELD hardware; null when none.
120
+ current_driver: Currently logged-in driver; null when none.
121
+ carb_ctc_test_enabled: CARB compliance flag; null when absent.
122
+ carb_ctc_emission_status: CARB emission status string; null when
123
+ absent.
124
+ registration_expiry_date: Registration expiry, mirrored as a string
125
+ (Motive's convention); null when absent.
126
+ """
127
+
128
+ vehicle_id: int = Field(alias='id')
129
+ company_id: int
130
+ number: str
131
+ status: VehicleStatus
132
+ ifta: bool
133
+ vin: str | None = None
134
+ make: str | None = None
135
+ model: str | None = None
136
+ year: str | None = None
137
+ license_plate_state: str | None = None
138
+ license_plate_number: str | None = None
139
+ license_plate_country_code: str | None = None
140
+ metric_units: bool = False
141
+ fuel_type: str | None = None
142
+ prevent_auto_odometer_entry: bool = False
143
+ notes: str | None = None
144
+
145
+ # Motive uses -1 as a "not configured" sentinel for these camera ints.
146
+ incab_alert_live_stream_enable: int = -1
147
+ driver_facing_camera: int = -1
148
+ incab_audio_recording: int = -1
149
+
150
+ group_ids: list[int] = Field(default_factory=list)
151
+
152
+ created_at: datetime
153
+ updated_at: datetime
154
+
155
+ permanent_driver: UserSummary | None = None
156
+ availability_details: AvailabilityDetails | None = None
157
+ eld_device: EldDeviceInfo | None = None
158
+ current_driver: UserSummary | None = None
159
+
160
+ carb_ctc_test_enabled: bool | None = None
161
+ carb_ctc_emission_status: str | None = None
162
+ registration_expiry_date: str | None = None
@@ -0,0 +1,127 @@
1
+ # src/fleetpull/models/motive/vehicle_location.py
2
+ """Motive vehicle-locations-endpoint response model (``/v3/vehicle_locations``).
3
+
4
+ Holds the ``VehicleLocation`` breadcrumb record and the ``VehicleLocationType``
5
+ enum used only by it. Cross-endpoint embedded shapes (``UserSummary``,
6
+ ``EldDeviceInfo``) are imported from ``fleetpull.models.motive.shared``.
7
+
8
+ Pure API mirrors — typed fields and nothing else. No use-case logic, no derived
9
+ properties, no normalizing validators: flattening and schema derivation are the
10
+ records layer's generic concern (DESIGN §9), and fleetpull assumes no end use, so a
11
+ field is mirrored, never interpreted. The response *wrapper* (the
12
+ ``{"vehicle_locations": [{"vehicle_location": {...}}]}`` envelope) is not modeled
13
+ here — the endpoints layer's decoder owns it, so this module mirrors only the inner
14
+ per-location object.
15
+ """
16
+
17
+ from datetime import datetime
18
+ from enum import StrEnum
19
+
20
+ from pydantic import Field
21
+
22
+ from fleetpull.model_contract import ResponseModel
23
+ from fleetpull.models.motive.shared import EldDeviceInfo, UserSummary
24
+
25
+ __all__: list[str] = [
26
+ 'VehicleLocation',
27
+ 'VehicleLocationType',
28
+ ]
29
+
30
+
31
+ class VehicleLocationType(StrEnum):
32
+ """Type of a Motive vehicle-location (breadcrumb) record.
33
+
34
+ A closed mirror of Motive's documented location-type vocabulary. Kept as an
35
+ enum (not downgraded to ``str``) for the same reason as ``VehicleStatus``: the
36
+ wire values match the member values exactly, so the enum is a faithful mirror
37
+ that adds documentation and membership validation with no normalizing logic.
38
+ The blast radius differs from ``VehicleStatus`` — this endpoint is high-volume
39
+ per-vehicle breadcrumbs, so a type Motive begins emitting that is not a member
40
+ fails validation loudly across every affected row rather than a handful of
41
+ vehicles. That is fail-fast-and-loud as intended (extend the member set,
42
+ re-fetch); it is also the field most likely to want ``str`` instead if a silent
43
+ land-as-data is ever preferred on this surface.
44
+ """
45
+
46
+ BREADCRUMB = 'breadcrumb'
47
+ VEHICLE_STOPPED = 'vehicle_stopped'
48
+ VEHICLE_MOVING = 'vehicle_moving'
49
+ IGNITION_ON = 'ignition_on'
50
+ IGNITION_OFF = 'ignition_off'
51
+ ENGINE_START = 'engine_start'
52
+ ENGINE_STOP = 'engine_stop'
53
+ GPS_MOVING = 'gps_moving'
54
+ GPS_STOPPED = 'gps_stopped'
55
+
56
+
57
+ class VehicleLocation(ResponseModel):
58
+ """A single vehicle-location (breadcrumb) record from Motive.
59
+
60
+ One point-in-time fix of a vehicle's position and telemetry from the
61
+ ``/v3/vehicle_locations/{vehicle_id}`` endpoint, which returns a vehicle's
62
+ location history over a date range. A pure mirror: every field maps a Motive
63
+ response field, with no derived or interpreted values.
64
+
65
+ Attributes:
66
+ location_id: Motive's identifier for this location record, a UUID string
67
+ (wire key ``id``).
68
+ located_at: Timestamp the location was recorded -- the endpoint's event
69
+ time.
70
+ latitude: GPS latitude in decimal degrees (wire key ``lat``).
71
+ longitude: GPS longitude in decimal degrees (wire key ``lon``).
72
+ location_type: The kind of location event (wire key ``type``).
73
+ description: Human-readable place description (e.g. city, state); null
74
+ when absent.
75
+ speed: Vehicle speed in the vehicle's own units; null when absent.
76
+ bearing: Compass heading in degrees; null when absent.
77
+ battery_voltage: Vehicle battery voltage; null when absent.
78
+ odometer: Calculated odometer reading; null when absent.
79
+ true_odometer: ECM-reported odometer; null when absent.
80
+ engine_hours: Calculated engine-hours reading; null when absent.
81
+ true_engine_hours: ECM-reported engine hours; null when absent.
82
+ fuel: Cumulative fuel consumption; null when absent.
83
+ fuel_primary_remaining_percentage: Primary tank level, 0-100; null when
84
+ absent.
85
+ fuel_secondary_remaining_percentage: Secondary tank level, 0-100; null
86
+ when absent.
87
+ veh_range: Estimated remaining range (EV); null for non-EV.
88
+ hvb_state_of_charge: High-voltage-battery charge state (EV); null for
89
+ non-EV.
90
+ hvb_charge_status: High-voltage-battery charge status string (EV); null
91
+ for non-EV.
92
+ hvb_charge_source: High-voltage-battery charge source string (EV); null
93
+ for non-EV.
94
+ hvb_lifetime_energy_output: High-voltage-battery lifetime energy output
95
+ (EV); null for non-EV.
96
+ driver: Driver logged in at capture time; null when none.
97
+ eld_device: ELD device that captured the location; null when none.
98
+ """
99
+
100
+ location_id: str = Field(alias='id')
101
+ located_at: datetime
102
+ latitude: float = Field(alias='lat')
103
+ longitude: float = Field(alias='lon')
104
+ location_type: VehicleLocationType = Field(alias='type')
105
+ description: str | None = None
106
+
107
+ speed: float | None = None
108
+ bearing: float | None = None
109
+
110
+ battery_voltage: float | None = None
111
+ odometer: float | None = None
112
+ true_odometer: float | None = None
113
+ engine_hours: float | None = None
114
+ true_engine_hours: float | None = None
115
+
116
+ fuel: float | None = None
117
+ fuel_primary_remaining_percentage: float | None = None
118
+ fuel_secondary_remaining_percentage: float | None = None
119
+
120
+ veh_range: float | None = None
121
+ hvb_state_of_charge: float | None = None
122
+ hvb_charge_status: str | None = None
123
+ hvb_charge_source: str | None = None
124
+ hvb_lifetime_energy_output: float | None = None
125
+
126
+ driver: UserSummary | None = None
127
+ eld_device: EldDeviceInfo | None = None
@@ -0,0 +1,128 @@
1
+ # src/fleetpull/models/motive/vehicle_utilization.py
2
+ """Motive VehicleUtilization response model
3
+ (``GET /v2/vehicle_utilization``, post-decoder window-stamped grain).
4
+
5
+ Written from captured live responses (2026-07-21 probe session: 120
6
+ records sampled across the 1,466-vehicle listing, structurally uniform
7
+ -- every key on every sampled record), never from docs. The model
8
+ mirrors the record ``MotiveWindowReportPageDecoder`` emits, and its two
9
+ field families have different provenance:
10
+
11
+ - ``window_start`` / ``window_end`` are DECODER-SYNTHESIZED
12
+ (``windowStartDate``/``windowEndDate``): rollup rows carry NO date or
13
+ time identity of any kind, so the decoder stamps each row with the
14
+ window the SENT spec asked for, copied verbatim from its
15
+ ``start_date``/``end_date`` date labels (the fuel-energy pair's
16
+ request-sourced stamp, on Motive wire). ``MotiveWindowStamp`` lifts
17
+ each label to its UTC-midnight instant -- a representation for
18
+ partition routing, never a timezone conversion (``shared.py``).
19
+ - Everything else is WIRE-VERBATIM: the metric core, the ``vehicle``
20
+ ref (the shared ``VehicleSummary``, its third carrying surface), and
21
+ the ``message`` status string, snake_case keys mirrored directly.
22
+
23
+ **THE COMPANY-LOCAL CAVEAT (the documented obligation on this
24
+ mirror).** The ``start_date``/``end_date`` labels are interpreted in
25
+ COMPANY-LOCAL days -- the account's ``/v1/companies`` capture carries a
26
+ company-local zone at a UTC-5 offset (DESIGN section 8) -- so each row
27
+ is the provider's rollup over its company-local day(s), mirrored
28
+ verbatim. The window stamps carry the day LABELS, not UTC day
29
+ boundaries; nothing here converts anything.
30
+
31
+ **THE ROLLUP GRAIN IS THE REQUEST WINDOW -- proven on this surface;
32
+ do not sum day rows (precedent-based).** A 1-day and a 6-day request
33
+ each returned exactly one rollup row per vehicle over the SAME
34
+ 1,466-vehicle population -- the grain is the window, which is why the
35
+ binding declares ``fixed_unit_days=1``. Additivity was NOT probed here;
36
+ on the provider family's only probed rollup surfaces (the Samsara
37
+ fuel-energy pair) day rollups were NON-ADDITIVE into wider windows
38
+ (89/267 mismatched), so the same posture applies as precedent: day rows
39
+ MUST NOT be summed to reproduce a wider window's rollup.
40
+
41
+ The population is the WHOLE vehicle fleet regardless of window (the
42
+ 1-day and 6-day totals were both 1,466): inactive vehicles ride with
43
+ zeroed metrics and a populated ``message`` status string -- there is no
44
+ absence arm on this surface, so the metric core is REQUIRED (the
45
+ fuel-energy whole-walk reasoning: a rollup surface computes its metrics
46
+ per window and has no absence mechanism to be conservative about). The
47
+ window stamps and the ``vehicle`` ref are required STRUCTURALLY -- a
48
+ rollup row without its window or its entity is meaningless.
49
+
50
+ ``last_located_at`` is the one nullable key (str-or-None in census) and
51
+ mirrors VERBATIM as a string: its value format and zone semantics are
52
+ unprobed, and Motive's rollup timestamps are documented company-local,
53
+ so parsing it would presume what no capture has shown. ``message`` is
54
+ free text -- a plain ``str``, no vocabulary claim.
55
+ """
56
+
57
+ from pydantic import Field
58
+
59
+ from fleetpull.model_contract import ResponseModel
60
+ from fleetpull.models.motive.shared import MotiveWindowStamp, VehicleSummary
61
+
62
+ __all__: list[str] = ['VehicleUtilization']
63
+
64
+
65
+ class VehicleUtilization(ResponseModel):
66
+ """One vehicle's utilization rollup over exactly one request window.
67
+
68
+ A pure mirror of the window-stamped post-decoder record (module
69
+ docstring: the window stamps are decoder-synthesized from the sent
70
+ spec's date labels; everything else is wire-verbatim). Field
71
+ semantics and units are Motive's; no value is derived or
72
+ interpreted here. Day rows MUST NOT be summed to reproduce a wider
73
+ window's rollup (module docstring -- the provider-family
74
+ precedent).
75
+
76
+ Attributes:
77
+ window_start: The request window's start-date label
78
+ (decoder-synthesized ``windowStartDate``, verbatim from the
79
+ sent ``start_date``) at UTC midnight -- the event-time
80
+ column: the row's time identity is the window that produced
81
+ it. A COMPANY-LOCAL day label (module docstring).
82
+ window_end: The request window's end-date label
83
+ (decoder-synthesized ``windowEndDate``, verbatim from the
84
+ sent ``end_date``) at UTC midnight. INCLUSIVE: at the fixed
85
+ 1-day unit it equals ``window_start``.
86
+ vehicle: The rollup's vehicle entity (the shared
87
+ ``VehicleSummary``; ``vin`` null on some rows of this
88
+ surface, every other key populated in census).
89
+ driving_fuel: Fuel used while driving in the window, provider
90
+ units mirrored verbatim.
91
+ driving_time: Time spent driving in the window, a float
92
+ (contrast the driver arm's bare-int durations -- each arm
93
+ mirrors its own wire).
94
+ idle_fuel: Fuel used while idling in the window.
95
+ idle_time: Time spent idling in the window, a float.
96
+ total_distance: Distance covered in the window.
97
+ total_fuel: Total fuel used in the window.
98
+ utilization_percentage: The provider's utilization figure for
99
+ the window, mirrored uninterpreted.
100
+ last_located_at: The vehicle's last-location timestamp, mirrored
101
+ VERBATIM as a string (format and zone semantics unprobed;
102
+ the company-local documentation obligation -- module
103
+ docstring); null when the provider has none.
104
+ message: Free-text status string -- populated on inactive
105
+ zero-metric rows (e.g. a no-data notice), empty otherwise;
106
+ a plain ``str``, no vocabulary claim.
107
+ """
108
+
109
+ # Decoder-synthesized window identity (the sent spec's own labels).
110
+ window_start: MotiveWindowStamp = Field(alias='windowStartDate')
111
+ window_end: MotiveWindowStamp = Field(alias='windowEndDate')
112
+
113
+ # The rollup's entity.
114
+ vehicle: VehicleSummary
115
+
116
+ # The wire-verbatim metric core (required -- no absence arm exists
117
+ # on this surface; provider units mirrored verbatim).
118
+ driving_fuel: float
119
+ driving_time: float
120
+ idle_fuel: float
121
+ idle_time: float
122
+ total_distance: float
123
+ total_fuel: float
124
+ utilization_percentage: float
125
+
126
+ # The wire-verbatim status pair.
127
+ last_located_at: str | None = None
128
+ message: str