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/geotab/device.py
2
+ """GeoTab Device response model (JSON-RPC ``Get`` on ``typeName: Device``).
3
+
4
+ Written from captured live responses (2026-07-09 probe session), never
5
+ from docs. The captured schema is polymorphic -- one entity type spans
6
+ at least three shapes: GO7-era hardware, GO9-era hardware (a superset
7
+ with ``vinInfo*`` and continuous-connect fields), and trailer entries
8
+ (``deviceType: "None"``, ``productId: -1``, ``tmpTrailerId``, no
9
+ telematics-parameter fields at all). Two tracked GO9 records also
10
+ arrived without ``deviceFlags``/``devicePlans`` entirely, so shape
11
+ poverty is itself a shape. The model is therefore the union of observed
12
+ fields with every field optional; ``models_to_dataframe`` lands absent
13
+ fields as nulls, one typed schema across all shapes.
14
+
15
+ Excluded fields (``extra='ignore'`` makes exclusion exactly "don't
16
+ model it"):
17
+
18
+ - ``ignoreDownloadsUntil`` -- observed live at ``0001-01-01``, which
19
+ overflows nanosecond-precision timestamp columns (captured
20
+ 2026-07-09). No other captured field carries a year-one datetime.
21
+ - ``wifiHotspotLimits``, ``customProperties``, ``mediaFiles`` --
22
+ observed only as empty lists, so their element shape is uncaptured
23
+ and cannot be honestly typed.
24
+ - ``groups``, ``devicePlanBillingInfo``, ``autoGroups``,
25
+ ``customParameters`` -- lists of objects; the records layer's schema
26
+ derivation (DESIGN section 9) supports scalars, enums,
27
+ ``list[scalar]``, and nested models only, so these shapes have no
28
+ honest column until the ``list[nested model]`` derivation vertical
29
+ lands. Model them there, or when a consumer forces typed access.
30
+
31
+ Sentinels are stored as-is, never transformed (interpretation is the
32
+ consumer's concern; the boundary model's job is shape): ``activeTo``
33
+ of ``2050-01-01`` means "still active" (and is ns-safe); the VIN fields
34
+ carry ``""`` and a literal ``"?"`` for unknown VINs; ``productId: -1``
35
+ marks non-telematics (trailer) entries.
36
+
37
+ Wire keys are camelCase; fields are snake_case via the ``to_camel``
38
+ alias generator rather than 74 hand-written ``Field(alias=...)`` lines
39
+ -- a typo'd hand alias would land silently as ``None`` under
40
+ ``extra='ignore'``, while the generator is mechanically exact (the
41
+ model tests validate every captured shape against it). The three
42
+ ``DeviceFlags`` acronym keys the generator cannot produce
43
+ (``isHOSAllowed`` / ``isUIAllowed`` / ``isVINAllowed``) carry explicit
44
+ alias overrides, proven non-``None`` against a captured block.
45
+ """
46
+
47
+ from datetime import datetime
48
+
49
+ from pydantic import ConfigDict, Field
50
+ from pydantic.alias_generators import to_camel
51
+
52
+ from fleetpull.model_contract import ResponseModel
53
+
54
+ __all__: list[str] = ['CustomFeatures', 'Device', 'DeviceFlags']
55
+
56
+
57
+ class DeviceFlags(ResponseModel):
58
+ """The ``deviceFlags`` block: account-level capability flags.
59
+
60
+ A pure mirror of the captured block -- one ``activeFeatures`` string
61
+ list plus ten capability booleans; absent from two tracked GO9
62
+ captures and every trailer entry, so the whole block is optional on
63
+ ``Device`` and its columns land as nulls there. ``ratePlans`` is
64
+ excluded from this model's interior: observed only as an empty
65
+ list, its element shape is uncapturable (the same rule as the
66
+ top-level empty lists).
67
+ """
68
+
69
+ model_config = ConfigDict(alias_generator=to_camel)
70
+
71
+ active_features: list[str] | None = None
72
+ is_active_tracking_allowed: bool | None = None
73
+ is_continuous_connect_allowed: bool | None = None
74
+ is_engine_allowed: bool | None = None
75
+ is_garmin_allowed: bool | None = None
76
+ # The acronym-alias trap: to_camel would emit isHosAllowed /
77
+ # isUiAllowed / isVinAllowed, silently landing these as None under
78
+ # extra='ignore'; the wire capitalizes the acronyms, so the three
79
+ # carry explicit aliases (an explicit alias beats the generator).
80
+ is_hos_allowed: bool | None = Field(default=None, alias='isHOSAllowed')
81
+ is_iridium_allowed: bool | None = None
82
+ is_odometer_allowed: bool | None = None
83
+ is_trip_detail_allowed: bool | None = None
84
+ is_ui_allowed: bool | None = Field(default=None, alias='isUIAllowed')
85
+ is_vin_allowed: bool | None = Field(default=None, alias='isVINAllowed')
86
+
87
+
88
+ class CustomFeatures(ResponseModel):
89
+ """The ``customFeatures`` block: per-account feature toggles.
90
+
91
+ Capture-limited: ``autoHos`` is the one key every tracked capture
92
+ carries, and the name suggests the block holds per-account keys --
93
+ any unknown key is absorbed by ``extra='ignore'`` today and gets
94
+ modeled if a capture ever shows it.
95
+ """
96
+
97
+ model_config = ConfigDict(alias_generator=to_camel)
98
+
99
+ auto_hos: bool | None = None
100
+
101
+
102
+ class Device(ResponseModel):
103
+ """One GeoTab Device entity: vehicle-installed GO hardware or a trailer.
104
+
105
+ A pure mirror of the union of captured fields, everything optional
106
+ (the shape polymorphism above). Field semantics are GeoTab's; no
107
+ value is derived or interpreted here. Groups below follow the
108
+ captured record layout: identity and lifecycle, vehicle identity,
109
+ fleet bookkeeping, firmware/parameter state, telematics
110
+ configuration scalars, and the aux-channel arrays.
111
+
112
+ Attributes:
113
+ id: GeoTab's device id -- the seek-paging sort key (hex-suffixed
114
+ string, ascending).
115
+ serial_number: Hardware serial; ``""`` on trailer entries.
116
+ name: The unit's display name.
117
+ device_type: Hardware generation (``GO7``, ``GO9``; trailers
118
+ carry the literal string ``"None"``).
119
+ product_id: Numeric hardware product code; ``-1`` on trailers.
120
+ active_from: Start of the device's active window (UTC).
121
+ active_to: End of the active window; ``2050-01-01`` is GeoTab's
122
+ still-active sentinel, stored as-is.
123
+ vehicle_identification_number: Registered VIN; ``""`` and a
124
+ literal ``"?"`` are captured unknown-VIN sentinels.
125
+ engine_vehicle_identification_number: ECM-reported VIN; same
126
+ sentinels.
127
+ tmp_trailer_id: Trailer-entry identifier; absent on tracked
128
+ vehicles.
129
+ """
130
+
131
+ model_config = ConfigDict(alias_generator=to_camel)
132
+
133
+ # Identity and lifecycle.
134
+ id: str | None = None
135
+ serial_number: str | None = None
136
+ name: str | None = None
137
+ device_type: str | None = None
138
+ product_id: int | None = None
139
+ active_from: datetime | None = None
140
+ active_to: datetime | None = None
141
+ comment: str | None = None
142
+ tmp_trailer_id: str | None = None
143
+
144
+ # Vehicle identity (VIN sentinels: "" and a literal "?").
145
+ vehicle_identification_number: str | None = None
146
+ engine_vehicle_identification_number: str | None = None
147
+ vin_info_make: str | None = None
148
+ vin_info_model: str | None = None
149
+ vin_info_vehicle_type: int | None = None
150
+ vin_info_year: str | None = None
151
+ license_plate: str | None = None
152
+ license_state: str | None = None
153
+
154
+ # Fleet bookkeeping.
155
+ device_plans: list[str] | None = None
156
+ device_flags: DeviceFlags | None = None
157
+ custom_features: CustomFeatures | None = None
158
+ time_zone_id: str | None = None
159
+ work_time: str | None = None
160
+ auto_hos: str | None = None
161
+ go_talk_language: str | None = None
162
+ pin_device: bool | None = None
163
+
164
+ # Firmware / parameter state.
165
+ major: int | None = None
166
+ minor: int | None = None
167
+ parameter_version: int | None = None
168
+ parameter_version_on_device: int | None = None
169
+ enable_must_reprogram: bool | None = None
170
+ time_to_download: str | None = None
171
+
172
+ # Telematics configuration scalars.
173
+ acceleration_warning_threshold: int | None = None
174
+ accelerometer_threshold_warning_factor: int | None = None
175
+ braking_warning_threshold: int | None = None
176
+ cornering_warning_threshold: int | None = None
177
+ communication_threshold_interval_moving: int | None = None
178
+ communication_threshold_interval_stationary: int | None = None
179
+ disable_buzzer: bool | None = None
180
+ disable_sleeper_berth: bool | None = None
181
+ enable_beep_on_dangerous_driving: bool | None = None
182
+ enable_beep_on_idle: bool | None = None
183
+ enable_beep_on_rpm: bool | None = None
184
+ enable_control_external_relay: bool | None = None
185
+ enable_speed_warning: bool | None = None
186
+ engine_hour_offset: int | None = None
187
+ engine_type: str | None = None
188
+ ensure_hot_start: bool | None = None
189
+ external_device_shut_down_delay: int | None = None
190
+ force_active_tracking: bool | None = None
191
+ fuel_tank_capacity: int | None = None
192
+ gps_off_delay: int | None = None
193
+ idle_minutes: int | None = None
194
+ immobilize_arming: int | None = None
195
+ immobilize_unit: bool | None = None
196
+ is_active_tracking_enabled: bool | None = None
197
+ is_continuous_connect_enabled: bool | None = None
198
+ is_driver_seatbelt_warning_on: bool | None = None
199
+ is_iox_connection_enabled: bool | None = None
200
+ is_passenger_seatbelt_warning_on: bool | None = None
201
+ is_reverse_detect_on: bool | None = None
202
+ is_speed_indicator: bool | None = None
203
+ max_seconds_between_logs: int | None = None
204
+ min_accident_speed: int | None = None
205
+ obd_alert_enabled: bool | None = None
206
+ odometer_factor: float | None = None
207
+ odometer_offset: float | None = None
208
+ rpm_value: int | None = None
209
+ seatbelt_warning_speed: int | None = None
210
+ speeding_off: int | None = None
211
+ speeding_on: int | None = None
212
+
213
+ # Aux-channel arrays (fixed-width per hardware generation).
214
+ aux_warning_speed: list[int] | None = None
215
+ enable_aux_warning: list[bool] | None = None
216
+ is_aux_ign_trigger: list[bool] | None = None
217
+ is_aux_inverted: list[bool] | None = None
@@ -0,0 +1,102 @@
1
+ # src/fleetpull/models/geotab/driver_change.py
2
+ """GeoTab DriverChange response model (``GetFeed`` on ``typeName: DriverChange``).
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 DriverChange is one
6
+ driver-to-device assignment event. The type carries a per-record
7
+ ``version``, and DriverChange records are user-editable through the
8
+ provider, so re-emission under newer versions is expected and the
9
+ consumer reconciles by ``(id, max version)`` (DESIGN §4).
10
+
11
+ Requiredness posture (the wave-two conservative stance, DESIGN §8): the
12
+ census is a TENANT-SCOPED observation (1,114 records), so structural
13
+ requiredness is limited to the record identity — ``id``, ``dateTime``
14
+ (the event time), ``version``, and the primary entity ref (``driver``,
15
+ the entity the type names) — and every other field is optional EVEN
16
+ where the census was total. The observed arms:
17
+
18
+ - ``driver`` is PROVEN mixed object-or-string (1,114/1,114, both arms
19
+ observed); its object arm carries ``isDriver`` beside the id, so the
20
+ ref model mirrors it (null exactly on string-arm rows). ``device``
21
+ was object-only on this census; both refs ride the shared
22
+ ``bare_id_to_reference`` lift (the census-scope lesson: a tenant
23
+ census cannot prove the string arm absent).
24
+
25
+ ``dateTime`` is recovered tz-aware by validation, the GeoTab sibling
26
+ idiom. ``type`` is a census-open vocabulary — a plain str, never an
27
+ enum.
28
+ """
29
+
30
+ from datetime import datetime
31
+ from typing import Annotated
32
+
33
+ from pydantic import BeforeValidator, ConfigDict
34
+ from pydantic.alias_generators import to_camel
35
+
36
+ from fleetpull.model_contract import ResponseModel
37
+ from fleetpull.models.geotab.shared import bare_id_to_reference
38
+
39
+ __all__: list[str] = [
40
+ 'DriverChange',
41
+ 'DriverChangeDeviceRef',
42
+ 'DriverChangeDriverRef',
43
+ ]
44
+
45
+
46
+ class DriverChangeDeviceRef(ResponseModel):
47
+ """The assignment's device reference.
48
+
49
+ Census-observed as an ``{id}`` object on every record; the shared
50
+ coercion lifts a bare string defensively (the census-scope lesson).
51
+ """
52
+
53
+ model_config = ConfigDict(alias_generator=to_camel)
54
+
55
+ id: str
56
+
57
+
58
+ class DriverChangeDriverRef(ResponseModel):
59
+ """The assignment's driver reference.
60
+
61
+ PROVEN mixed on this census: an ``{id, isDriver}`` object or a bare
62
+ id string (e.g. the ``"UnknownDriverId"`` sentinel); the shared
63
+ coercion lifts the bare form to ``{"id": <string>}``, so
64
+ ``is_driver`` is null exactly on string-arm rows.
65
+ """
66
+
67
+ model_config = ConfigDict(alias_generator=to_camel)
68
+
69
+ id: str
70
+ is_driver: bool | None = None
71
+
72
+
73
+ class DriverChange(ResponseModel):
74
+ """One GeoTab driver-to-device assignment event.
75
+
76
+ The wave-two conservative mirror (the module docstring's posture):
77
+ ``id`` / ``date_time`` / ``version`` / ``driver`` required,
78
+ everything else optional even where census-total.
79
+
80
+ Attributes:
81
+ date_time: The change's UTC instant — the endpoint's event time.
82
+ device: The assignment's device reference (object-only on this
83
+ census; defensively lifted).
84
+ driver: The assignment's driver reference — proven
85
+ object-or-string, ``is_driver`` null exactly on string-arm
86
+ rows.
87
+ id: GeoTab's record id.
88
+ type: The change-type token (census-open plain str).
89
+ version: The record's version token — the reconcile key beside
90
+ ``id``.
91
+ """
92
+
93
+ model_config = ConfigDict(alias_generator=to_camel)
94
+
95
+ date_time: datetime
96
+ device: Annotated[
97
+ DriverChangeDeviceRef | None, BeforeValidator(bare_id_to_reference)
98
+ ] = None
99
+ driver: Annotated[DriverChangeDriverRef, BeforeValidator(bare_id_to_reference)]
100
+ id: str
101
+ type: str | None = None
102
+ version: str
@@ -0,0 +1,200 @@
1
+ # src/fleetpull/models/geotab/duty_status_log.py
2
+ """GeoTab DutyStatusLog response model (``GetFeed`` on ``typeName: DutyStatusLog``).
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 DutyStatusLog is one HOS
6
+ duty-status event — an EDITABLE log: ``editDateTime`` is the edit
7
+ trail, re-emission under newer ``version`` tokens is expected, and the
8
+ consumer reconciles by ``(id, max version)`` (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), ``version``, and the primary entity ref (``driver``, the log's
14
+ subject) — and every other field is optional EVEN where the census was
15
+ total (2,000/2,000). The observed arms:
16
+
17
+ - ``device`` and ``driver`` are PROVEN mixed object-or-string
18
+ (2,000/2,000 each, both arms observed); every reference field rides
19
+ the shared ``bare_id_to_reference`` lift.
20
+ - ``annotations`` (126/2,000) carried elements that are EXACTLY
21
+ ``{"id": <str>}`` on every sampled record; the records layer supports
22
+ list-of-scalar only, so the field is an ID-LIST — ``list[str]`` via a
23
+ STRICT element lift (``_annotation_ids``): a shape change fails
24
+ loudly, never silently drops sibling keys. The ids join to the
25
+ ``annotation_logs`` vertical (feed wave three) for the full
26
+ annotation records.
27
+ - ``location`` (1,859/2,000) is the shared ``GeotabAddressedLocation``
28
+ wrapper (DVIRLog is the second consumer; DESIGN §8): it carries a
29
+ double-nested ``{location: {x, y}}`` COORDINATE arm or an
30
+ ``{address: {formattedAddress}}`` arm. The 200-sample census saw only
31
+ coordinates; a 24,860-block live-proof walk found 14 address-arm
32
+ blocks (the census-scope lesson), so both arms are modeled optional.
33
+ - Mixed int-or-float numerics (``distanceSinceValidCoordinates``,
34
+ ``engineHours``, ``odometer``) model ``float``.
35
+
36
+ ``dateTime``, ``editDateTime``, and ``verifyDateTime`` are recovered
37
+ tz-aware by validation, the GeoTab sibling idiom. ``deferralStatus``,
38
+ ``malfunction``, ``origin``, ``state``, and ``status`` are census-open
39
+ vocabularies — plain strs, never enums.
40
+ """
41
+
42
+ from datetime import datetime
43
+ from typing import Annotated
44
+
45
+ from pydantic import BeforeValidator, ConfigDict
46
+ from pydantic.alias_generators import to_camel
47
+
48
+ from fleetpull.model_contract import ResponseModel
49
+ from fleetpull.models.geotab.shared import GeotabAddressedLocation, bare_id_to_reference
50
+ from fleetpull.vocabulary import JsonValue
51
+
52
+ __all__: list[str] = [
53
+ 'DutyStatusLog',
54
+ 'DutyStatusLogDeviceRef',
55
+ 'DutyStatusLogDriverRef',
56
+ ]
57
+
58
+
59
+ def _annotation_ids(value: JsonValue) -> JsonValue:
60
+ """Strictly lift the ``annotations`` element list to its id list.
61
+
62
+ The census shows elements carrying ONLY ``{"id": <str>}``; the lift
63
+ is deliberately STRICT so a wire-shape change fails validation
64
+ loudly instead of silently dropping sibling keys the model never
65
+ saw (the loud-failure doctrine).
66
+
67
+ Args:
68
+ value: The raw ``annotations`` wire value.
69
+
70
+ Returns:
71
+ ``None`` passthrough (the field is optional), or the element
72
+ ids: a bare-string element passes verbatim, an EXACTLY
73
+ ``{'id': <str>}`` element becomes its id.
74
+
75
+ Raises:
76
+ ValueError: The value is not a list, or an element is neither a
77
+ bare string nor exactly ``{'id': <str>}`` (extra keys, other
78
+ shapes); the message names the offending element.
79
+ """
80
+ if value is None:
81
+ return None
82
+ if not isinstance(value, list):
83
+ raise ValueError(f'annotations must be a list, got {type(value).__name__}')
84
+ lifted: list[JsonValue] = []
85
+ for element in value:
86
+ if isinstance(element, str):
87
+ lifted.append(element)
88
+ continue
89
+ if (
90
+ isinstance(element, dict)
91
+ and set(element) == {'id'}
92
+ and isinstance(element['id'], str)
93
+ ):
94
+ lifted.append(element['id'])
95
+ continue
96
+ raise ValueError(
97
+ f'annotations element must be a bare id string or exactly '
98
+ f"{{'id': <str>}}, got {element!r}"
99
+ )
100
+ return lifted
101
+
102
+
103
+ class DutyStatusLogDeviceRef(ResponseModel):
104
+ """The recording device's reference.
105
+
106
+ PROVEN mixed on this census: an ``{id}`` object or a bare id
107
+ string, the bare form lifted by the shared coercion so both arms
108
+ land as ``device__id``.
109
+ """
110
+
111
+ model_config = ConfigDict(alias_generator=to_camel)
112
+
113
+ id: str
114
+
115
+
116
+ class DutyStatusLogDriverRef(ResponseModel):
117
+ """The log's driver reference.
118
+
119
+ PROVEN mixed on this census: an ``{id}`` object or a bare id
120
+ string (e.g. the ``"UnknownDriverId"`` sentinel), the bare form
121
+ lifted by the shared coercion so both arms land as ``driver__id``.
122
+ """
123
+
124
+ model_config = ConfigDict(alias_generator=to_camel)
125
+
126
+ id: str
127
+
128
+
129
+ class DutyStatusLog(ResponseModel):
130
+ """One GeoTab HOS duty-status event from the DutyStatusLog feed.
131
+
132
+ The wave-two conservative mirror (the module docstring's posture):
133
+ ``id`` / ``date_time`` / ``version`` / ``driver`` required,
134
+ everything else optional even where census-total.
135
+
136
+ Attributes:
137
+ annotations: The annotation-log ids attached to the event — the
138
+ strict id-list reduction (126/2,000 carriers); join to the
139
+ ``annotation_logs`` vertical for the records.
140
+ date_time: The event's UTC instant — the endpoint's event time.
141
+ deferral_minutes: Deferred off-duty minutes.
142
+ deferral_status: The deferral-status token (census-open).
143
+ device: The recording device's reference — proven
144
+ object-or-string.
145
+ distance_since_valid_coordinates: Distance since the last valid
146
+ GPS fix (308/2,000; mixed int-or-float, modeled float).
147
+ driver: The log's driver reference — proven object-or-string.
148
+ edit_date_time: The last edit's UTC instant — the edit trail.
149
+ engine_hours: The engine-hours reading (1,844/2,000; mixed
150
+ int-or-float, modeled float).
151
+ event_code: The ELD event code (1,623/2,000).
152
+ event_record_status: The ELD event-record status.
153
+ event_type: The ELD event type (1,753/2,000).
154
+ id: GeoTab's record id.
155
+ is_ignored: Whether the log is ignored.
156
+ is_transitioning: Whether the log is mid-transition.
157
+ location: The event's nested location (1,859/2,000; the shared
158
+ wrapper — a ``{x, y}`` coordinate arm, x longitude / y
159
+ latitude, or a ``formattedAddress`` arm).
160
+ malfunction: The malfunction token (census-open plain str).
161
+ odometer: The odometer reading (1,863/2,000; mixed
162
+ int-or-float, modeled float).
163
+ origin: The origin token (census-open plain str).
164
+ sequence: The ELD sequence token (1,753/2,000).
165
+ state: The state token (census-open plain str).
166
+ status: The duty-status token (census-open plain str).
167
+ verify_date_time: The driver-verification UTC instant
168
+ (765/2,000).
169
+ version: The record's version token — the editable-log
170
+ reconcile key beside ``id``.
171
+ """
172
+
173
+ model_config = ConfigDict(alias_generator=to_camel)
174
+
175
+ annotations: Annotated[list[str] | None, BeforeValidator(_annotation_ids)] = None
176
+ date_time: datetime
177
+ deferral_minutes: int | None = None
178
+ deferral_status: str | None = None
179
+ device: Annotated[
180
+ DutyStatusLogDeviceRef | None, BeforeValidator(bare_id_to_reference)
181
+ ] = None
182
+ distance_since_valid_coordinates: float | None = None
183
+ driver: Annotated[DutyStatusLogDriverRef, BeforeValidator(bare_id_to_reference)]
184
+ edit_date_time: datetime | None = None
185
+ engine_hours: float | None = None
186
+ event_code: int | None = None
187
+ event_record_status: int | None = None
188
+ event_type: int | None = None
189
+ id: str
190
+ is_ignored: bool | None = None
191
+ is_transitioning: bool | None = None
192
+ location: GeotabAddressedLocation | None = None
193
+ malfunction: str | None = None
194
+ odometer: float | None = None
195
+ origin: str | None = None
196
+ sequence: str | None = None
197
+ state: str | None = None
198
+ status: str | None = None
199
+ verify_date_time: datetime | None = None
200
+ version: str