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,184 @@
1
+ # src/fleetpull/models/geotab/dvir_log.py
2
+ """GeoTab DVIRLog response model (``GetFeed`` on ``typeName: DVIRLog``).
3
+
4
+ Written from the 2026-07-21 feed wave two census (30-day seeded pulls at
5
+ the probed tenant), never from docs. A DVIRLog is one driver vehicle
6
+ inspection report. The house class casing is ``DvirLog``; the wire
7
+ ``typeName`` stays ``'DVIRLog'`` (the binding's constant). DVIRs are
8
+ certified and edited after creation, so re-emission under newer
9
+ ``version`` tokens is expected and the consumer reconciles by
10
+ ``(id, max version)`` (DESIGN §4).
11
+
12
+ Requiredness posture (the wave-two conservative stance, DESIGN §8): the
13
+ census is a TENANT-SCOPED observation (500 records), so structural
14
+ requiredness is limited to the record identity — ``id``, ``dateTime``
15
+ (the event time), ``version``, and the primary entity ref (``driver``,
16
+ 500/500; ``device`` is only 205/500 and could not be it) — and every
17
+ other field is optional EVEN where the census was total. The observed
18
+ arms and exclusions:
19
+
20
+ - ``device`` is OPTIONAL and commonly absent (205/500 carriers) — a
21
+ plain wire fact of this census, recorded without speculation.
22
+ ``engineHours`` and ``odometer`` travel with it (205/500 each);
23
+ ``engineHours`` was int-only on its carriers, but its sibling
24
+ surface (``DutyStatusLog``) proved the same physical quantity MIXED
25
+ int-or-float, so it models ``float`` here too (cross-surface dtype
26
+ consistency over a thin single-surface census).
27
+ - ``defectList`` is a WIRE-PLURAL NAME carrying ONE defect node
28
+ ``{children, id, name}``. The node models ``id`` + ``name`` ONLY:
29
+ ``children`` is EXCLUDED under the documented-exclusion doctrine —
30
+ an EMPTY list on every one of the 200 ``defectList`` nodes the
31
+ census sampled (the nested-block sample depth; ``defectList`` itself
32
+ was present 500/500), so its element shape is unobservable at this
33
+ tenant, and the records layer deliberately supports only observable
34
+ shapes; ``extra='ignore'`` absorbs it wire-side (a record with
35
+ populated children still validates — the pinned absorption test).
36
+ REVISIT when a tenant shows populated ``children``: capture the
37
+ element shape and model it then.
38
+ - ``location`` (496/500) is the shared ``GeotabAddressedLocation``
39
+ wrapper (DutyStatusLog is the co-consumer; DESIGN §8): a
40
+ ``{location: {x, y}}`` coordinate arm or an
41
+ ``{address: {formattedAddress}}`` arm. This surface showed only the
42
+ coordinate arm on the live-proof walk, but rides the shared wrapper
43
+ whose address arm DutyStatusLog proved.
44
+ - ``duration`` is an opaque duration STRING mirrored verbatim, NOT
45
+ parsed through ``GeotabTimeSpan`` despite that sharing GeoTab's
46
+ provider. The census observed only the wire TYPE (``str``, 500/500),
47
+ never the value FORMAT — unlike ``Trip.duration``, whose TimeSpan
48
+ grammar was probe-confirmed (2026-07-13). Applying the strict
49
+ TimeSpan parser to an unobserved format would crash every record if
50
+ it differs, so the conservative mirror holds until a probe settles
51
+ the format. ``trailer`` (295/500) is an ``{id}`` ref.
52
+ - Every reference field (``device``, ``driver``, ``trailer``) rides
53
+ the shared ``bare_id_to_reference`` lift (the census-scope lesson: a
54
+ tenant census cannot prove the string arm absent).
55
+
56
+ ``dateTime`` is recovered tz-aware by validation, the GeoTab sibling
57
+ idiom. ``logType`` is a census-open vocabulary — a plain str, never an
58
+ enum.
59
+ """
60
+
61
+ from datetime import datetime
62
+ from typing import Annotated
63
+
64
+ from pydantic import BeforeValidator, ConfigDict
65
+ from pydantic.alias_generators import to_camel
66
+
67
+ from fleetpull.model_contract import ResponseModel
68
+ from fleetpull.models.geotab.shared import GeotabAddressedLocation, bare_id_to_reference
69
+
70
+ __all__: list[str] = [
71
+ 'DvirLog',
72
+ 'DvirLogDefectList',
73
+ 'DvirLogDeviceRef',
74
+ 'DvirLogDriverRef',
75
+ 'DvirLogTrailerRef',
76
+ ]
77
+
78
+
79
+ class DvirLogDefectList(ResponseModel):
80
+ """The ``defectList`` block: a WIRE-PLURAL NAME, ONE defect node.
81
+
82
+ Despite the plural wire key, the census shows ONE object —
83
+ ``{children, id, name}`` — mirrored as a single nested model, never
84
+ a list. ``children`` is excluded (never populated on the whole
85
+ census; the module docstring's documented-exclusion record); ``id``
86
+ and ``name`` are required within the block — a defect node without
87
+ its identity is a shape change and must fail loudly.
88
+ """
89
+
90
+ model_config = ConfigDict(alias_generator=to_camel)
91
+
92
+ id: str
93
+ name: str
94
+
95
+
96
+ class DvirLogDeviceRef(ResponseModel):
97
+ """The inspected vehicle unit's reference.
98
+
99
+ Census-observed as an ``{id}`` object on every carrier; the shared
100
+ coercion lifts a bare string defensively (the census-scope lesson).
101
+ """
102
+
103
+ model_config = ConfigDict(alias_generator=to_camel)
104
+
105
+ id: str
106
+
107
+
108
+ class DvirLogDriverRef(ResponseModel):
109
+ """The inspecting driver's reference: the id alone, on every record."""
110
+
111
+ model_config = ConfigDict(alias_generator=to_camel)
112
+
113
+ id: str
114
+
115
+
116
+ class DvirLogTrailerRef(ResponseModel):
117
+ """The inspected trailer's reference: the id alone, on every carrier."""
118
+
119
+ model_config = ConfigDict(alias_generator=to_camel)
120
+
121
+ id: str
122
+
123
+
124
+ class DvirLog(ResponseModel):
125
+ """One GeoTab driver vehicle inspection report from the DVIRLog feed.
126
+
127
+ The wave-two conservative mirror (the module docstring's posture):
128
+ ``id`` / ``date_time`` / ``version`` / ``driver`` required,
129
+ everything else optional even where census-total.
130
+
131
+ Attributes:
132
+ authority_address: The certifying authority's address.
133
+ authority_name: The certifying authority's name.
134
+ certify_remark: The certification remark.
135
+ date_time: The inspection's UTC instant — the endpoint's event
136
+ time.
137
+ defect_list: The plural-named single defect node (``children``
138
+ excluded — the module docstring's record).
139
+ device: The inspected vehicle unit's reference (205/500 — the
140
+ observed wire fact).
141
+ driver: The inspecting driver's reference.
142
+ driver_remark: The driver's remark.
143
+ duration: The inspection duration — an opaque wire string
144
+ mirrored verbatim (the value format is unobserved, so it is
145
+ not parsed through ``GeotabTimeSpan`` the way ``Trip`` is;
146
+ module docstring).
147
+ engine_hours: The engine-hours reading (205/500; modeled float
148
+ per the cross-surface mixed-numeric proof).
149
+ id: GeoTab's record id.
150
+ is_inspected_by_driver: Whether the driver performed the
151
+ inspection.
152
+ location: The inspection's nested location (496/500; the shared
153
+ wrapper — a ``{x, y}`` coordinate arm, x longitude / y
154
+ latitude, or a ``formattedAddress`` arm).
155
+ log_type: The inspection-type token (census-open plain str).
156
+ odometer: The odometer reading (205/500; modeled float).
157
+ trailer: The inspected trailer's reference (295/500).
158
+ version: The record's version token — the certified/edited-log
159
+ reconcile key beside ``id``.
160
+ """
161
+
162
+ model_config = ConfigDict(alias_generator=to_camel)
163
+
164
+ authority_address: str | None = None
165
+ authority_name: str | None = None
166
+ certify_remark: str | None = None
167
+ date_time: datetime
168
+ defect_list: DvirLogDefectList | None = None
169
+ device: Annotated[
170
+ DvirLogDeviceRef | None, BeforeValidator(bare_id_to_reference)
171
+ ] = None
172
+ driver: Annotated[DvirLogDriverRef, BeforeValidator(bare_id_to_reference)]
173
+ driver_remark: str | None = None
174
+ duration: str | None = None
175
+ engine_hours: float | None = None
176
+ id: str
177
+ is_inspected_by_driver: bool | None = None
178
+ location: GeotabAddressedLocation | None = None
179
+ log_type: str | None = None
180
+ odometer: float | None = None
181
+ trailer: Annotated[
182
+ DvirLogTrailerRef | None, BeforeValidator(bare_id_to_reference)
183
+ ] = None
184
+ version: str
@@ -0,0 +1,135 @@
1
+ # src/fleetpull/models/geotab/exception_event.py
2
+ """The GeoTab ExceptionEvent response model (captured 2026-07-13).
3
+
4
+ One record per continuous rule-violation interval per device per rule:
5
+ the interval opens at ``active_from`` and closes at ``active_to``, and
6
+ ``duration = active_to - active_from`` holds exactly on every captured
7
+ record, including a fractional-second span reproducing a fractional
8
+ ``active_from`` (DESIGN §8). Records mutate after creation
9
+ (``last_modified_date_time`` observed ~17 minutes past
10
+ ``created_date_time``) — the provider-level lookback absorbs it.
11
+
12
+ A pure mirror of the union of captured fields, everything optional.
13
+ ``driver`` and ``diagnostic`` arrive as either a reference object or a
14
+ bare sentinel string (``"UnknownDriverId"``, ``"NoDiagnosticId"``) —
15
+ the shared ``bare_id_to_reference`` coercion lifts the bare form to
16
+ ``{"id": <string>}`` verbatim, so the sentinel lands as the reference's
17
+ ``id`` and its other fields null exactly on sentinel rows. The
18
+ object-form ``driver`` shape is inferred from Trip's captured grammar
19
+ (these captures carry only the sentinel); the first object-form capture
20
+ upgrades it to Captured.
21
+ """
22
+
23
+ from datetime import datetime
24
+ from typing import Annotated
25
+
26
+ from pydantic import BeforeValidator, ConfigDict
27
+ from pydantic.alias_generators import to_camel
28
+
29
+ from fleetpull.model_contract import ResponseModel
30
+ from fleetpull.models.geotab.shared import GeotabTimeSpan, bare_id_to_reference
31
+
32
+ __all__: list[str] = [
33
+ 'ExceptionEvent',
34
+ 'ExceptionEventDeviceRef',
35
+ 'ExceptionEventDiagnosticRef',
36
+ 'ExceptionEventDriverRef',
37
+ 'ExceptionEventRuleRef',
38
+ ]
39
+
40
+
41
+ class ExceptionEventRuleRef(ResponseModel):
42
+ """The event's rule reference: state, reason, and the rule id."""
43
+
44
+ model_config = ConfigDict(alias_generator=to_camel)
45
+
46
+ state: str | None = None
47
+ reason: str | None = None
48
+ id: str | None = None
49
+
50
+
51
+ class ExceptionEventDeviceRef(ResponseModel):
52
+ """The event's device reference: the id alone."""
53
+
54
+ model_config = ConfigDict(alias_generator=to_camel)
55
+
56
+ id: str | None = None
57
+
58
+
59
+ class ExceptionEventDriverRef(ResponseModel):
60
+ """The event's driver reference.
61
+
62
+ Arrives as an object or the bare ``"UnknownDriverId"`` sentinel
63
+ string; the ``ExceptionEvent.driver`` field's coercion lifts the
64
+ bare form to ``{"id": <string>}``, so ``is_driver`` is null exactly
65
+ on sentinel rows. The object-form shape mirrors Trip's captured
66
+ driver grammar (inferred; no object-form ExceptionEvent capture
67
+ exists yet).
68
+ """
69
+
70
+ model_config = ConfigDict(alias_generator=to_camel)
71
+
72
+ id: str | None = None
73
+ is_driver: bool | None = None
74
+
75
+
76
+ class ExceptionEventDiagnosticRef(ResponseModel):
77
+ """The event's diagnostic reference.
78
+
79
+ Arrives as the bare ``"NoDiagnosticId"`` sentinel string in every
80
+ capture; the coercion lifts it to ``{"id": <string>}``.
81
+ """
82
+
83
+ model_config = ConfigDict(alias_generator=to_camel)
84
+
85
+ id: str | None = None
86
+
87
+
88
+ class ExceptionEvent(ResponseModel):
89
+ """One GeoTab ExceptionEvent: a rule-violation interval.
90
+
91
+ Attributes:
92
+ id: GeoTab's event id (an ``a``-prefixed GUID-like string — a
93
+ different id space from the ``b``-hex entities).
94
+ version: The record's version token.
95
+ rule: The violated rule's reference (state, reason, rule id).
96
+ device: The vehicle reference.
97
+ driver: The driver reference; the bare ``"UnknownDriverId"``
98
+ sentinel lands as ``driver.id`` verbatim.
99
+ diagnostic: The diagnostic reference; the bare
100
+ ``"NoDiagnosticId"`` sentinel lands as ``diagnostic.id``.
101
+ active_from: Interval start (UTC) — the endpoint's event time
102
+ and partition anchor.
103
+ active_to: Interval end (UTC).
104
+ duration: The interval length (.NET TimeSpan on the wire);
105
+ reproduces ``active_to - active_from`` exactly in capture.
106
+ distance: Distance traveled during the interval, km.
107
+ state: The event's lifecycle state token, mirrored verbatim.
108
+ created_date_time: When the provider materialized the record.
109
+ last_modified_date_time: The record's last mutation.
110
+ """
111
+
112
+ model_config = ConfigDict(alias_generator=to_camel)
113
+
114
+ # Identity.
115
+ id: str | None = None
116
+ version: str | None = None
117
+ rule: ExceptionEventRuleRef | None = None
118
+ device: ExceptionEventDeviceRef | None = None
119
+ driver: Annotated[
120
+ ExceptionEventDriverRef | None, BeforeValidator(bare_id_to_reference)
121
+ ] = None
122
+ diagnostic: Annotated[
123
+ ExceptionEventDiagnosticRef | None, BeforeValidator(bare_id_to_reference)
124
+ ] = None
125
+
126
+ # The interval.
127
+ active_from: datetime | None = None
128
+ active_to: datetime | None = None
129
+ duration: GeotabTimeSpan = None
130
+
131
+ # Measures and state.
132
+ distance: float | None = None
133
+ state: str | None = None
134
+ created_date_time: datetime | None = None
135
+ last_modified_date_time: datetime | None = None
@@ -0,0 +1,168 @@
1
+ # src/fleetpull/models/geotab/fault_data.py
2
+ """GeoTab FaultData response model (``GetFeed`` on ``typeName: FaultData``).
3
+
4
+ Written from the 2026-07-21 feed wave two census (30-day seeded pulls at
5
+ the probed tenant), never from docs. A FaultData record is one
6
+ engine-fault observation — an ACTIVE feed with NO per-record
7
+ ``version`` (the LogRecord asymmetry: append-only storage is trivially
8
+ complete and the consumer reconciles by ``id`` alone, DESIGN §4).
9
+
10
+ Requiredness posture (the wave-two conservative stance, DESIGN §8): the
11
+ census is a TENANT-SCOPED observation, so structural requiredness is
12
+ limited to the record identity — ``id``, ``dateTime`` (the event
13
+ time), and the primary entity ref (``device``, the faulting unit) —
14
+ and every other field is optional EVEN where the census was total
15
+ (2,000/2,000): a tenant census cannot promise another tenant's
16
+ presence. The observed arms:
17
+
18
+ - ``failureMode`` is MIXED object-or-string on this census (the one
19
+ proven mixed ref here); EVERY reference field — ``controller``,
20
+ ``device``, ``diagnostic``, ``failureMode`` — rides the shared
21
+ ``bare_id_to_reference`` lift regardless, because the census-scope
22
+ lesson (DESIGN §8, StatusData's ``controller``) is that a tenant
23
+ census cannot prove the string arm absent and the lift is structural
24
+ and sentinel-agnostic.
25
+ - The RARE QUARTET — ``diagnosticSeverity`` (str), ``riskOfBreakdown``
26
+ (float), ``severity`` (str), ``sourceAddress`` (int) — appeared on
27
+ 2/2,000 records each: optional scalars.
28
+ - ``faultStates`` is a WIRE-PLURAL NAME carrying a SINGULAR object
29
+ shape (``{effectiveStatus}``) — mirrored as one nested model, the
30
+ plural-name/singular-shape wire fact recorded on it.
31
+
32
+ ``dateTime`` is recovered tz-aware by validation, the GeoTab sibling
33
+ idiom. ``faultState`` and ``faultStates.effectiveStatus`` are
34
+ census-open vocabularies — plain strs, never enums.
35
+ """
36
+
37
+ from datetime import datetime
38
+ from typing import Annotated
39
+
40
+ from pydantic import BeforeValidator, ConfigDict
41
+ from pydantic.alias_generators import to_camel
42
+
43
+ from fleetpull.model_contract import ResponseModel
44
+ from fleetpull.models.geotab.shared import bare_id_to_reference
45
+
46
+ __all__: list[str] = [
47
+ 'FaultData',
48
+ 'FaultDataControllerRef',
49
+ 'FaultDataDeviceRef',
50
+ 'FaultDataDiagnosticRef',
51
+ 'FaultDataFailureModeRef',
52
+ 'FaultDataFaultStates',
53
+ ]
54
+
55
+
56
+ class FaultDataControllerRef(ResponseModel):
57
+ """The source controller reference.
58
+
59
+ Census-observed as an ``{id}`` object on every carrier; the shared
60
+ coercion lifts a bare string defensively (the StatusData
61
+ census-scope lesson — an unobserved sentinel arm still lands as
62
+ ``controller__id``).
63
+ """
64
+
65
+ model_config = ConfigDict(alias_generator=to_camel)
66
+
67
+ id: str
68
+
69
+
70
+ class FaultDataDeviceRef(ResponseModel):
71
+ """The faulting vehicle unit's reference: the id alone, on every record."""
72
+
73
+ model_config = ConfigDict(alias_generator=to_camel)
74
+
75
+ id: str
76
+
77
+
78
+ class FaultDataDiagnosticRef(ResponseModel):
79
+ """The fault's diagnostic-definition reference: the id alone."""
80
+
81
+ model_config = ConfigDict(alias_generator=to_camel)
82
+
83
+ id: str
84
+
85
+
86
+ class FaultDataFailureModeRef(ResponseModel):
87
+ """The failure-mode reference.
88
+
89
+ The one PROVEN mixed ref on this census: an ``{id}`` object or a
90
+ bare id string, the bare form lifted by the shared coercion so both
91
+ arms land as ``failure_mode__id``.
92
+ """
93
+
94
+ model_config = ConfigDict(alias_generator=to_camel)
95
+
96
+ id: str
97
+
98
+
99
+ class FaultDataFaultStates(ResponseModel):
100
+ """The ``faultStates`` block: a WIRE-PLURAL NAME, a SINGULAR shape.
101
+
102
+ Despite the plural wire key, the census shows ONE object carrying
103
+ ``effectiveStatus`` (census-open plain str) — mirrored as a single
104
+ nested model, never a list. Required within the block: a
105
+ ``faultStates`` block without its one observed key is a shape
106
+ change and must fail loudly.
107
+ """
108
+
109
+ model_config = ConfigDict(alias_generator=to_camel)
110
+
111
+ effective_status: str
112
+
113
+
114
+ class FaultData(ResponseModel):
115
+ """One GeoTab engine-fault observation from the FaultData feed.
116
+
117
+ The wave-two conservative mirror (the module docstring's posture):
118
+ ``id`` / ``date_time`` / ``device`` required, everything else
119
+ optional even where census-total.
120
+
121
+ Attributes:
122
+ amber_warning_lamp: The amber-warning lamp state.
123
+ controller: The source controller reference (object-only on
124
+ this census; defensively lifted).
125
+ count: The fault's occurrence count.
126
+ date_time: The fault's UTC instant — the endpoint's event time.
127
+ device: The faulting vehicle unit's reference.
128
+ diagnostic: The fault's diagnostic-definition reference.
129
+ diagnostic_severity: Rare-quartet severity token (2/2,000;
130
+ census-open plain str).
131
+ failure_mode: The failure-mode reference — proven
132
+ object-or-string, both arms landing as ``failure_mode__id``.
133
+ fault_state: The fault-state token (census-open plain str).
134
+ fault_states: The plural-named singular status block.
135
+ id: GeoTab's record id.
136
+ malfunction_lamp: The malfunction-indicator lamp state.
137
+ protect_warning_lamp: The protect-warning lamp state.
138
+ red_stop_lamp: The red-stop lamp state.
139
+ risk_of_breakdown: Rare-quartet breakdown-risk score (2/2,000).
140
+ severity: Rare-quartet severity token (2/2,000; census-open).
141
+ source_address: Rare-quartet source address (2/2,000).
142
+ """
143
+
144
+ model_config = ConfigDict(alias_generator=to_camel)
145
+
146
+ amber_warning_lamp: bool | None = None
147
+ controller: Annotated[
148
+ FaultDataControllerRef | None, BeforeValidator(bare_id_to_reference)
149
+ ] = None
150
+ count: int | None = None
151
+ date_time: datetime
152
+ device: Annotated[FaultDataDeviceRef, BeforeValidator(bare_id_to_reference)]
153
+ diagnostic: Annotated[
154
+ FaultDataDiagnosticRef | None, BeforeValidator(bare_id_to_reference)
155
+ ] = None
156
+ diagnostic_severity: str | None = None
157
+ failure_mode: Annotated[
158
+ FaultDataFailureModeRef | None, BeforeValidator(bare_id_to_reference)
159
+ ] = None
160
+ fault_state: str | None = None
161
+ fault_states: FaultDataFaultStates | None = None
162
+ id: str
163
+ malfunction_lamp: bool | None = None
164
+ protect_warning_lamp: bool | None = None
165
+ red_stop_lamp: bool | None = None
166
+ risk_of_breakdown: float | None = None
167
+ severity: str | None = None
168
+ source_address: int | None = None
@@ -0,0 +1,189 @@
1
+ # src/fleetpull/models/geotab/fill_up.py
2
+ """GeoTab FillUp response model (``GetFeed`` on ``typeName: FillUp``).
3
+
4
+ Written from the 2026-07-21 live probe session, never from docs. A
5
+ FillUp is one provider-detected fuel-stop event — a CALCULATED feed:
6
+ past records re-emit under a higher ``version`` on reprocessing, stored
7
+ as emitted and reconciled by ``(id, max version)`` (DESIGN §4).
8
+
9
+ THE ESTIMATES-ONLY-TENANT CAVEAT (DESIGN §8): the probed tenant has NO
10
+ fuel-transaction (fuel-card) integration, so every fuel value on this
11
+ surface is provider-derived from telemetry — estimates, not
12
+ transactions. The census cannot speak for integrated tenants: ``cost``
13
+ was 0.0 on ALL records, ``fuelTransactions`` was an EMPTY list on ALL
14
+ records (excluded below as value-unobservable — on tenants with
15
+ fuel-card integration it populates with a shape never captured; it
16
+ joins the model when a capture types it), and ``productType`` was
17
+ ``'Unknown'`` throughout.
18
+
19
+ Requiredness posture: the census is a uniform whole-page total —
20
+ 100/100 records carried every modeled key — so every field is required,
21
+ with the sentinel arms exactly as observed. The census is a
22
+ TENANT-SCOPED observation. The observed arms:
23
+
24
+ - ``driver`` arrives as either the object reference (``{"id": ...,
25
+ "isDriver": true}``, 87/100) or the bare ``"UnknownDriverId"``
26
+ sentinel string; the shared ``bare_id_to_reference`` coercion (the
27
+ shipped Trip mechanism) lifts the bare form to ``{"id": <string>}``,
28
+ so the sentinel lands as ``driver.id`` and ``is_driver`` is null
29
+ exactly on sentinel rows.
30
+ - ``derivedVolume`` carries an observed ``-1.0`` sentinel (the
31
+ provider's could-not-derive marker) beside real volumes — mirrored
32
+ VERBATIM, never nulled: reshaping a sentinel would be interpretation.
33
+ - ``confidence`` is a comma-joined detection-method token list carried
34
+ as ONE plain string (e.g. ``'FuelLevel, TripStop'``) — splitting it
35
+ would presume a use case; the observed vocabulary is census-open.
36
+ - ``tankCapacity.source`` observed vocabulary: ``EstimateFuelLevel`` /
37
+ ``DiagnosticTankCapacity`` / ``Unknown`` — census-open plain str.
38
+
39
+ Mixed int-or-float wire numerics (``derivedVolume``, ``distance``,
40
+ ``odometer``, ``tankCapacity.volume``, ``volume``) are modeled
41
+ ``float``. ``dateTime`` (the event time) and each extrema point's
42
+ ``dateTime`` are recovered tz-aware by validation, the GeoTab sibling
43
+ idiom.
44
+ """
45
+
46
+ from datetime import datetime
47
+ from typing import Annotated
48
+
49
+ from pydantic import BeforeValidator, ConfigDict
50
+ from pydantic.alias_generators import to_camel
51
+
52
+ from fleetpull.model_contract import ResponseModel
53
+ from fleetpull.models.geotab.shared import bare_id_to_reference
54
+
55
+ __all__: list[str] = [
56
+ 'FillUp',
57
+ 'FillUpDeviceRef',
58
+ 'FillUpDriverRef',
59
+ 'FillUpLocation',
60
+ 'FillUpTankCapacity',
61
+ 'FillUpTankLevelExtrema',
62
+ 'FillUpTankLevelPoint',
63
+ ]
64
+
65
+
66
+ class FillUpDeviceRef(ResponseModel):
67
+ """The fill-up's device reference: the id alone, on every record."""
68
+
69
+ model_config = ConfigDict(alias_generator=to_camel)
70
+
71
+ id: str
72
+
73
+
74
+ class FillUpDriverRef(ResponseModel):
75
+ """The fill-up's driver reference.
76
+
77
+ Arrives as an object or the bare ``"UnknownDriverId"`` sentinel
78
+ string; the ``FillUp.driver`` field's coercion lifts the bare form
79
+ to ``{"id": <string>}``, so ``is_driver`` is null exactly on
80
+ sentinel rows.
81
+ """
82
+
83
+ model_config = ConfigDict(alias_generator=to_camel)
84
+
85
+ id: str
86
+ is_driver: bool | None = None
87
+
88
+
89
+ class FillUpLocation(ResponseModel):
90
+ """The fill-up's coordinate: ``x`` longitude, ``y`` latitude."""
91
+
92
+ model_config = ConfigDict(alias_generator=to_camel)
93
+
94
+ x: float
95
+ y: float
96
+
97
+
98
+ class FillUpTankCapacity(ResponseModel):
99
+ """The provider's tank-capacity estimate and its derivation source.
100
+
101
+ Attributes:
102
+ source: How the capacity was derived — observed vocabulary
103
+ ``EstimateFuelLevel`` / ``DiagnosticTankCapacity`` /
104
+ ``Unknown``, census-open plain str.
105
+ volume: The estimated capacity in liters (mixed int-or-float on
106
+ the wire, modeled float).
107
+ """
108
+
109
+ model_config = ConfigDict(alias_generator=to_camel)
110
+
111
+ source: str
112
+ volume: float
113
+
114
+
115
+ class FillUpTankLevelPoint(ResponseModel):
116
+ """One tank-level extremum: its source, instant, and level reading."""
117
+
118
+ model_config = ConfigDict(alias_generator=to_camel)
119
+
120
+ source: str
121
+ date_time: datetime
122
+ data: float
123
+
124
+
125
+ class FillUpTankLevelExtrema(ResponseModel):
126
+ """The tank-level extrema pair bracketing the detected fill-up."""
127
+
128
+ model_config = ConfigDict(alias_generator=to_camel)
129
+
130
+ maxima_point: FillUpTankLevelPoint
131
+ minima_point: FillUpTankLevelPoint
132
+
133
+
134
+ class FillUp(ResponseModel):
135
+ """One GeoTab provider-detected fuel-stop event.
136
+
137
+ A pure mirror of the 100/100 whole-page census: every modeled key
138
+ present on every record, so everything is required; the sentinel
139
+ arms (``driver`` string-or-object, the ``-1.0`` ``derived_volume``)
140
+ are exactly as observed. The estimates-only-tenant caveat is the
141
+ module docstring's.
142
+
143
+ Attributes:
144
+ confidence: The comma-joined detection-method token list, one
145
+ plain string (census-open).
146
+ cost: The transaction cost — 0.0 on ALL census records (no fuel
147
+ transaction integration on the probed tenant).
148
+ currency_code: The cost's currency code (census-open plain str).
149
+ date_time: The detected fill-up's UTC instant — the endpoint's
150
+ event time.
151
+ derived_volume: The provider-derived fill volume in liters;
152
+ ``-1.0`` is the observed could-not-derive sentinel, mirrored
153
+ verbatim.
154
+ device: The vehicle unit's reference.
155
+ distance: Distance since the prior fill-up, km.
156
+ driver: The driver reference; the bare ``"UnknownDriverId"``
157
+ sentinel lands as ``driver.id`` verbatim.
158
+ id: GeoTab's record id.
159
+ location: The detected stop's coordinate.
160
+ odometer: The odometer reading at the fill-up.
161
+ product_type: The fuel product type — ``'Unknown'`` on all
162
+ census records (census-open plain str).
163
+ tank_capacity: The tank-capacity estimate and its source.
164
+ tank_level_extrema: The level extrema bracketing the fill-up.
165
+ total_fuel_used: Cumulative fuel used at the fill-up, liters.
166
+ version: The record's version token — the calculated-feed
167
+ reconcile key beside ``id``.
168
+ volume: The fill volume in liters (unit reasoning, recorded: the sibling wire key totalIdlingFuelUsedL carries the L suffix, and the probed value range -- median 299, max 848 -- is truck-tank plausible in liters and absurd in gallons).
169
+ """
170
+
171
+ model_config = ConfigDict(alias_generator=to_camel)
172
+
173
+ confidence: str
174
+ cost: float
175
+ currency_code: str
176
+ date_time: datetime
177
+ derived_volume: float
178
+ device: FillUpDeviceRef
179
+ distance: float
180
+ driver: Annotated[FillUpDriverRef, BeforeValidator(bare_id_to_reference)]
181
+ id: str
182
+ location: FillUpLocation
183
+ odometer: float
184
+ product_type: str
185
+ tank_capacity: FillUpTankCapacity
186
+ tank_level_extrema: FillUpTankLevelExtrema
187
+ total_fuel_used: float
188
+ version: str
189
+ volume: float