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,105 @@
1
+ # src/fleetpull/models/geotab/shipment_log.py
2
+ """GeoTab ShipmentLog response model (``GetFeed`` on ``typeName: ShipmentLog``).
3
+
4
+ Written from the 2026-07-21 feed wave three SCALE census (walked at
5
+ scale at the probed tenant), never from docs. A ShipmentLog is one
6
+ shipment-manifest record attached to a driver and device over an active
7
+ window — a versioned feed, so re-emission under newer ``version``
8
+ tokens is expected and the consumer reconciles by ``(id, max version)``
9
+ (DESIGN §4).
10
+
11
+ Requiredness posture (the wave-two conservative stance, DESIGN §8): the
12
+ census is a TENANT-SCOPED observation (2,771 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
+ consistent with the log family) — and every other field is optional
16
+ EVEN where the census was total. The observed arms:
17
+
18
+ - ``device`` and ``driver`` were object-only (``{id}``) at scale; both
19
+ ride the shared ``bare_id_to_reference`` lift regardless (the
20
+ census-scope lesson, DESIGN §8: a tenant census cannot prove the
21
+ string arm absent).
22
+
23
+ ``activeFrom``/``activeTo`` (the shipment's active window) and
24
+ ``dateTime`` are recovered tz-aware by validation, the GeoTab sibling
25
+ idiom. ``commodity``, ``documentNumber``, and ``shipperName`` are
26
+ census-open free-text strings.
27
+ """
28
+
29
+ from datetime import datetime
30
+ from typing import Annotated
31
+
32
+ from pydantic import BeforeValidator, ConfigDict
33
+ from pydantic.alias_generators import to_camel
34
+
35
+ from fleetpull.model_contract import ResponseModel
36
+ from fleetpull.models.geotab.shared import bare_id_to_reference
37
+
38
+ __all__: list[str] = [
39
+ 'ShipmentLog',
40
+ 'ShipmentLogDeviceRef',
41
+ 'ShipmentLogDriverRef',
42
+ ]
43
+
44
+
45
+ class ShipmentLogDeviceRef(ResponseModel):
46
+ """The shipment's device reference.
47
+
48
+ Census-observed as an ``{id}`` object at scale; the shared coercion
49
+ lifts a bare string defensively (the census-scope lesson).
50
+ """
51
+
52
+ model_config = ConfigDict(alias_generator=to_camel)
53
+
54
+ id: str
55
+
56
+
57
+ class ShipmentLogDriverRef(ResponseModel):
58
+ """The shipment's driver reference.
59
+
60
+ Census-observed as an ``{id}`` object at scale; the shared coercion
61
+ lifts a bare string defensively (the census-scope lesson).
62
+ """
63
+
64
+ model_config = ConfigDict(alias_generator=to_camel)
65
+
66
+ id: str
67
+
68
+
69
+ class ShipmentLog(ResponseModel):
70
+ """One GeoTab shipment-manifest record from the ShipmentLog feed.
71
+
72
+ The wave-two conservative mirror (the module docstring's posture):
73
+ ``id`` / ``date_time`` / ``version`` / ``driver`` required,
74
+ everything else optional even where census-total.
75
+
76
+ Attributes:
77
+ active_from: The shipment window's UTC start.
78
+ active_to: The shipment window's UTC end.
79
+ commodity: The shipped commodity (census-open str).
80
+ date_time: The record's UTC instant — the endpoint's event time.
81
+ device: The shipment's device reference (object-only at scale;
82
+ defensively lifted).
83
+ document_number: The manifest document number (census-open str).
84
+ driver: The shipment's driver reference (object-only at scale;
85
+ defensively lifted).
86
+ id: GeoTab's record id.
87
+ shipper_name: The shipper company name (census-open str).
88
+ version: The record's version token — the reconcile key beside
89
+ ``id``.
90
+ """
91
+
92
+ model_config = ConfigDict(alias_generator=to_camel)
93
+
94
+ active_from: datetime | None = None
95
+ active_to: datetime | None = None
96
+ commodity: str | None = None
97
+ date_time: datetime
98
+ device: Annotated[
99
+ ShipmentLogDeviceRef | None, BeforeValidator(bare_id_to_reference)
100
+ ] = None
101
+ document_number: str | None = None
102
+ driver: Annotated[ShipmentLogDriverRef, BeforeValidator(bare_id_to_reference)]
103
+ id: str
104
+ shipper_name: str | None = None
105
+ version: str
@@ -0,0 +1,108 @@
1
+ # src/fleetpull/models/geotab/status_data.py
2
+ """GeoTab StatusData response model (``GetFeed`` on ``typeName: StatusData``).
3
+
4
+ Written from the 2026-07-21 live probe session, never from docs. A
5
+ StatusData record is one engine-diagnostic reading — the LogRecord
6
+ stream's sibling ACTIVE feed, with one deliberate asymmetry: StatusData
7
+ DOES carry a per-record ``version`` (LogRecord carries none), observed on
8
+ every census record and mirrored — the consumer still reconciles this
9
+ active feed by ``id``, the version riding along as wire truth
10
+ (DESIGN §4).
11
+
12
+ Requiredness posture: the census is a large uniform whole-page total —
13
+ 2,000/2,000 records carried every key — so every field is required with
14
+ no nullable arm (none was observed). The census is a TENANT-SCOPED
15
+ observation (DESIGN §8). Volume on the probed tenant is ~24,500
16
+ records/hour, which is why the leaf declares the 50,000
17
+ protocol-maximum ``resultsLimit``.
18
+
19
+ ``dateTime`` (the event time) is recovered tz-aware by validation, the
20
+ GeoTab sibling idiom. ``data`` — the diagnostic's value — is MIXED
21
+ int-or-float on the wire and modeled ``float`` (the one dtype that
22
+ carries both arms losslessly). ``controller`` is STRING-OR-OBJECT:
23
+ the ``"ControllerNoneId"`` sentinel string on 49,745 of a 50,000-record
24
+ live full-page census (2026-07-21) and an ``{id}`` reference on the
25
+ other 255 — the Trip ``UnknownDriverId`` mechanism verbatim (the
26
+ initial one-hour census saw only the sentinel arm; the live proof's
27
+ full walk surfaced the object arm — a census-scope lesson recorded in
28
+ the section 8 block). The shared ``bare_id_to_reference`` coercion
29
+ lifts the sentinel to ``{"id": <string>}``.
30
+ """
31
+
32
+ from datetime import datetime
33
+ from typing import Annotated
34
+
35
+ from pydantic import BeforeValidator, ConfigDict
36
+ from pydantic.alias_generators import to_camel
37
+
38
+ from fleetpull.model_contract import ResponseModel
39
+ from fleetpull.models.geotab.shared import bare_id_to_reference
40
+
41
+ __all__: list[str] = [
42
+ 'StatusData',
43
+ 'StatusDataControllerRef',
44
+ 'StatusDataDeviceRef',
45
+ 'StatusDataDiagnosticRef',
46
+ ]
47
+
48
+
49
+ class StatusDataControllerRef(ResponseModel):
50
+ """The source controller reference.
51
+
52
+ Arrives as an ``{id}`` object (255 of the 50,000-record live
53
+ census) or the bare ``"ControllerNoneId"`` sentinel string
54
+ (49,745), which the shared coercion lifts to ``{"id": <string>}``.
55
+ """
56
+
57
+ id: str | None = None
58
+
59
+
60
+ class StatusDataDeviceRef(ResponseModel):
61
+ """The reading's device reference: the id alone, on every record."""
62
+
63
+ model_config = ConfigDict(alias_generator=to_camel)
64
+
65
+ id: str
66
+
67
+
68
+ class StatusDataDiagnosticRef(ResponseModel):
69
+ """The reading's diagnostic reference: the id alone, on every record."""
70
+
71
+ model_config = ConfigDict(alias_generator=to_camel)
72
+
73
+ id: str
74
+
75
+
76
+ class StatusData(ResponseModel):
77
+ """One GeoTab diagnostic reading from the StatusData feed.
78
+
79
+ A pure mirror of the 2,000/2,000 whole-page census: seven keys, all
80
+ present and non-null on every record, so all seven are required.
81
+
82
+ Attributes:
83
+ controller: The source controller reference — an ``{id}``
84
+ object or the bare ``"ControllerNoneId"`` sentinel string,
85
+ lifted by the shared coercion so the sentinel lands as
86
+ ``controller__id`` (census-open
87
+ plain str).
88
+ data: The diagnostic's value — mixed int-or-float on the wire,
89
+ modeled float.
90
+ date_time: The reading's UTC instant — the endpoint's event time.
91
+ device: The emitting vehicle unit's reference.
92
+ diagnostic: The diagnostic definition's reference.
93
+ id: GeoTab's record id.
94
+ version: The record's version token — present on this active
95
+ feed (unlike LogRecord), mirrored as wire truth.
96
+ """
97
+
98
+ model_config = ConfigDict(alias_generator=to_camel)
99
+
100
+ controller: Annotated[
101
+ StatusDataControllerRef, BeforeValidator(bare_id_to_reference)
102
+ ]
103
+ data: float
104
+ date_time: datetime
105
+ device: StatusDataDeviceRef
106
+ diagnostic: StatusDataDiagnosticRef
107
+ id: str
108
+ version: str
@@ -0,0 +1,125 @@
1
+ # src/fleetpull/models/geotab/text_message.py
2
+ """GeoTab TextMessage response model (``GetFeed`` on ``typeName: TextMessage``).
3
+
4
+ Written from the 2026-07-21 feed wave three SCALE census (walked at
5
+ scale at the probed tenant), never from docs. A TextMessage is one
6
+ dispatch message between the office and a vehicle.
7
+
8
+ NO per-record ``version`` key AND NO ``dateTime`` key — the append-only
9
+ asymmetry (FaultData/LogRecord): append-only storage is trivially
10
+ complete and the consumer reconciles by ``id`` alone (DESIGN §4). The
11
+ event-time identity is ``sent`` (the send instant, 25,000/25,000), so
12
+ the binding anchors ``event_time_column='sent'`` and ``sent`` is a
13
+ REQUIRED datetime — storage partitions on it. The feed's own
14
+ ``toVersion`` still advances (delivered/read receipts re-emit a message
15
+ under a newer FEED version); those re-emissions are stored-as-emitted,
16
+ the feed's versioning rather than a per-record ``version`` key.
17
+
18
+ Requiredness posture (the wave-two conservative stance, DESIGN §8): the
19
+ census is a TENANT-SCOPED observation (25,000 records), so structural
20
+ requiredness is limited to the record identity — ``id`` and ``sent``
21
+ (the event time) — and every other field is optional EVEN where the
22
+ census was total. The observed arms:
23
+
24
+ - ``device`` was object-only (``{id}``) at scale; it rides the shared
25
+ ``bare_id_to_reference`` lift (the census-scope lesson). It is
26
+ OPTIONAL — a text message has no required primary entity ref.
27
+ - ``messageContent`` is a NESTED block ``{contentType, ids}``; both
28
+ keys are required WITHIN the block on the nested-block-required
29
+ convention (200/200 nested, 25,000/25,000 present). ``ids`` is a
30
+ PLAIN ``list[str]`` — the elements ARE strings on the wire, so this
31
+ is a §9 list-of-scalar direct field, NOT the DutyStatusLog
32
+ ``annotations`` id-object reduction (there the elements were
33
+ ``{id}`` objects needing a strict lift).
34
+ - ``delivered`` and ``read`` are receipt datetimes present on
35
+ 24,995/25,000 — optional.
36
+
37
+ ``activeFrom``/``activeTo``, ``delivered``, ``read``, and ``sent`` are
38
+ recovered tz-aware by validation, the GeoTab sibling idiom.
39
+ ``contentType`` and ``recipient`` are census-open strings;
40
+ ``isDirectionToVehicle`` is a bool, ``messageSize`` an int.
41
+ """
42
+
43
+ from datetime import datetime
44
+ from typing import Annotated
45
+
46
+ from pydantic import BeforeValidator, ConfigDict
47
+ from pydantic.alias_generators import to_camel
48
+
49
+ from fleetpull.model_contract import ResponseModel
50
+ from fleetpull.models.geotab.shared import bare_id_to_reference
51
+
52
+ __all__: list[str] = [
53
+ 'TextMessage',
54
+ 'TextMessageContent',
55
+ 'TextMessageDeviceRef',
56
+ ]
57
+
58
+
59
+ class TextMessageDeviceRef(ResponseModel):
60
+ """The message's device reference (the vehicle end of the exchange).
61
+
62
+ Census-observed as an ``{id}`` object at scale; the shared coercion
63
+ lifts a bare string defensively (the census-scope lesson).
64
+ """
65
+
66
+ model_config = ConfigDict(alias_generator=to_camel)
67
+
68
+ id: str
69
+
70
+
71
+ class TextMessageContent(ResponseModel):
72
+ """The ``messageContent`` block: the message payload descriptor.
73
+
74
+ Both keys are required WITHIN the block (the nested-block-required
75
+ convention): a present ``messageContent`` block missing either is a
76
+ shape change and must fail loudly. ``ids`` is a direct
77
+ ``list[str]`` (the elements are strings on the wire, a §9
78
+ list-of-scalar), NOT the ``annotations`` id-object reduction.
79
+ """
80
+
81
+ model_config = ConfigDict(alias_generator=to_camel)
82
+
83
+ content_type: str
84
+ ids: list[str]
85
+
86
+
87
+ class TextMessage(ResponseModel):
88
+ """One GeoTab dispatch message from the TextMessage feed.
89
+
90
+ The wave-two conservative mirror with the append-only asymmetry (the
91
+ module docstring's posture): ``id`` / ``sent`` required, NO
92
+ per-record ``version``, everything else optional even where
93
+ census-total.
94
+
95
+ Attributes:
96
+ active_from: The message window's UTC start.
97
+ active_to: The message window's UTC end.
98
+ delivered: The delivery-receipt UTC instant (24,995/25,000).
99
+ device: The message's device reference (object-only at scale;
100
+ defensively lifted). Optional — no required primary ref.
101
+ id: GeoTab's record id.
102
+ is_direction_to_vehicle: Whether the message is office→vehicle.
103
+ message_content: The message payload descriptor block.
104
+ message_size: The message payload size.
105
+ read: The read-receipt UTC instant (24,995/25,000).
106
+ recipient: The message recipient address (census-open str).
107
+ sent: The send UTC instant — the endpoint's event time (the
108
+ event-time identity, in place of the absent ``dateTime``).
109
+ """
110
+
111
+ model_config = ConfigDict(alias_generator=to_camel)
112
+
113
+ active_from: datetime | None = None
114
+ active_to: datetime | None = None
115
+ delivered: datetime | None = None
116
+ device: Annotated[
117
+ TextMessageDeviceRef | None, BeforeValidator(bare_id_to_reference)
118
+ ] = None
119
+ id: str
120
+ is_direction_to_vehicle: bool | None = None
121
+ message_content: TextMessageContent | None = None
122
+ message_size: int | None = None
123
+ read: datetime | None = None
124
+ recipient: str | None = None
125
+ sent: datetime
@@ -0,0 +1,162 @@
1
+ # src/fleetpull/models/geotab/trip.py
2
+ """GeoTab Trip response model (JSON-RPC ``Get`` on ``typeName: Trip``).
3
+
4
+ Written from captured live responses (2026-07-13 probe session), never
5
+ from docs. A Trip is one engine-on movement interval plus its trailing
6
+ stop window; the model is the union of observed fields with every field
7
+ optional, a pure mirror -- no value is derived or interpreted here.
8
+
9
+ Interval semantics (12 of 12 captured records):
10
+ ``driving_duration = stop - start``; ``stop_duration = next_trip_start
11
+ - stop``; ``idling_duration`` measures engine-on time WITHIN the
12
+ post-trip stop window, never within the drive. The zero-distance
13
+ degenerate shape has ``start == stop``, ``driving_duration`` of zero,
14
+ and NO ``averageSpeed`` key at all -- absence is a shape, landing as a
15
+ null.
16
+
17
+ Units (delta-arithmetic confirmed against the captures):
18
+
19
+ - distances (``distance``, ``after_hours_distance``, ``work_distance``)
20
+ are kilometers;
21
+ - speeds (``average_speed``, ``maximum_speed``) are km/h;
22
+ - ``odometer`` is METERS (confirmed by delta arithmetic against a
23
+ trip's own km distance);
24
+ - ``engine_hours`` is SECONDS despite the name -- a captured 26.1M
25
+ "hours" is 7,251 real engine-hours. The value is stored verbatim;
26
+ renaming or converting it would break the pure-mirror rule, so the
27
+ trap is documented here and at every mention instead.
28
+
29
+ Durations arrive as .NET TimeSpan strings, parsed at the boundary
30
+ through ``GeotabTimeSpan`` (``models/geotab/shared.py``); day-prefixed
31
+ spans (``"4.16:41:16"``) occur whenever a stop window crosses days.
32
+
33
+ The ``driver`` reference arrives as either an object
34
+ (``{"id": ..., "isDriver": true}``) or the bare known-id sentinel
35
+ string ``"UnknownDriverId"``; the shared ``bare_id_to_reference``
36
+ coercion lifts any bare string to ``{"id": <string>}`` verbatim, so the
37
+ sentinel lands as ``driver__id`` and ``driver__is_driver`` stays null
38
+ on sentinel rows. ``maximum_speed`` and the ``speed_range1/2/3`` trio
39
+ are modeled ``float`` although every captured value is integral: they
40
+ are physical measurements like ``average_speed``, and JSON numbers do
41
+ not distinguish ``94`` from ``94.0`` (``speed_range*`` semantics are
42
+ unconfirmed -- captured all-zero).
43
+ """
44
+
45
+ from datetime import datetime
46
+ from typing import Annotated
47
+
48
+ from pydantic import BeforeValidator, ConfigDict
49
+ from pydantic.alias_generators import to_camel
50
+
51
+ from fleetpull.model_contract import ResponseModel
52
+ from fleetpull.models.geotab.shared import GeotabTimeSpan, bare_id_to_reference
53
+
54
+ __all__: list[str] = ['Trip', 'TripDeviceRef', 'TripDriverRef', 'TripStopPoint']
55
+
56
+
57
+ class TripDeviceRef(ResponseModel):
58
+ """The trip's device reference: the id alone."""
59
+
60
+ model_config = ConfigDict(alias_generator=to_camel)
61
+
62
+ id: str | None = None
63
+
64
+
65
+ class TripDriverRef(ResponseModel):
66
+ """The trip's driver reference.
67
+
68
+ Arrives as an object or the bare ``"UnknownDriverId"`` sentinel
69
+ string; the ``Trip.driver`` field's coercion lifts the bare form to
70
+ ``{"id": <string>}``, so ``is_driver`` is null exactly on sentinel
71
+ rows.
72
+ """
73
+
74
+ model_config = ConfigDict(alias_generator=to_camel)
75
+
76
+ id: str | None = None
77
+ is_driver: bool | None = None
78
+
79
+
80
+ class TripStopPoint(ResponseModel):
81
+ """The trip's stop coordinate: ``x`` longitude, ``y`` latitude."""
82
+
83
+ model_config = ConfigDict(alias_generator=to_camel)
84
+
85
+ x: float | None = None
86
+ y: float | None = None
87
+
88
+
89
+ class Trip(ResponseModel):
90
+ """One GeoTab Trip: a movement interval and its trailing stop window.
91
+
92
+ A pure mirror of the union of captured fields, everything optional.
93
+ Groups below follow the captured record layout: identity, the
94
+ interval, durations, distances and speeds, counters, and flags. The
95
+ unit traps (``engine_hours`` is seconds, ``odometer`` meters) are
96
+ documented in the module docstring.
97
+
98
+ Attributes:
99
+ id: GeoTab's trip id -- the seek-paging sort key.
100
+ version: The record's version token (ids and versions share one
101
+ counter space).
102
+ device: The vehicle reference.
103
+ driver: The driver reference; the bare ``"UnknownDriverId"``
104
+ sentinel lands as ``driver.id`` verbatim.
105
+ start: Trip start (UTC) -- the endpoint's event time.
106
+ stop: Trip end (UTC); equals ``start`` on the zero-distance
107
+ degenerate shape.
108
+ next_trip_start: The following trip's start; bounds this trip's
109
+ stop window.
110
+ engine_hours: Cumulative engine SECONDS despite the name (the
111
+ module-docstring trap), stored verbatim.
112
+ odometer: Cumulative odometer in METERS, stored verbatim.
113
+ average_speed: Mean speed in km/h; the key is absent on
114
+ zero-distance trips (lands null).
115
+ """
116
+
117
+ model_config = ConfigDict(alias_generator=to_camel)
118
+
119
+ # Identity.
120
+ id: str | None = None
121
+ version: str | None = None
122
+ device: TripDeviceRef | None = None
123
+ driver: Annotated[TripDriverRef | None, BeforeValidator(bare_id_to_reference)] = (
124
+ None
125
+ )
126
+
127
+ # The interval.
128
+ start: datetime | None = None
129
+ stop: datetime | None = None
130
+ next_trip_start: datetime | None = None
131
+
132
+ # Durations (.NET TimeSpan strings on the wire).
133
+ driving_duration: GeotabTimeSpan = None
134
+ stop_duration: GeotabTimeSpan = None
135
+ idling_duration: GeotabTimeSpan = None
136
+ after_hours_driving_duration: GeotabTimeSpan = None
137
+ after_hours_stop_duration: GeotabTimeSpan = None
138
+ work_driving_duration: GeotabTimeSpan = None
139
+ work_stop_duration: GeotabTimeSpan = None
140
+ speed_range1_duration: GeotabTimeSpan = None
141
+ speed_range2_duration: GeotabTimeSpan = None
142
+ speed_range3_duration: GeotabTimeSpan = None
143
+
144
+ # Distances (km) and speeds (km/h).
145
+ distance: float | None = None
146
+ after_hours_distance: float | None = None
147
+ work_distance: float | None = None
148
+ average_speed: float | None = None
149
+ maximum_speed: float | None = None
150
+ speed_range1: float | None = None
151
+ speed_range2: float | None = None
152
+ speed_range3: float | None = None
153
+
154
+ # Cumulative counters (the unit traps: seconds and meters).
155
+ engine_hours: float | None = None
156
+ odometer: float | None = None
157
+
158
+ # Flags and the stop coordinate.
159
+ after_hours_start: bool | None = None
160
+ after_hours_end: bool | None = None
161
+ is_seat_belt_off: bool | None = None
162
+ stop_point: TripStopPoint | None = None
@@ -0,0 +1,187 @@
1
+ # src/fleetpull/models/geotab/user.py
2
+ """GeoTab User response model (JSON-RPC ``Get`` on ``typeName: User``).
3
+
4
+ Written from captured live responses (2026-07-16 probe session: a
5
+ full-population sweep of all 157 accounts plus seven captured record
6
+ variants), never from docs. Drivers ARE users in GeoTab -- ``isDriver``
7
+ splits the population (129/157 captured) and the driver-only key block
8
+ (``licenseNumber``, ``licenseProvince``, ``viewDriversOwnDataOnly``)
9
+ is ABSENT, not null, on non-driver accounts; the sweep observed no null
10
+ value and no type variance on any key, so optionality here is
11
+ absence-shaped. Fields the sweep proved on every record are required;
12
+ the partial-presence fields are optional (presence counts in the
13
+ attribute docs).
14
+
15
+ Excluded fields (``extra='ignore'`` makes exclusion exactly "don't
16
+ model it"):
17
+
18
+ - ``activeDashboardReports``, ``activeDefaultDashboards``,
19
+ ``availableDashboardReports``, ``bookmarks``, ``cannedResponseOptions``,
20
+ ``companyGroups``, ``driverGroups``, ``jobPriorities``, ``keys``,
21
+ ``mapViews``, ``mediaFiles``, ``privateUserGroups``, ``reportGroups``,
22
+ ``securityGroups`` -- lists; the records layer's schema derivation
23
+ (DESIGN section 9) supports scalars, enums, ``list[scalar]``, and
24
+ nested models only, and these are UI/grouping plumbing whose struct
25
+ shapes have no honest column until the ``list[nested model]``
26
+ derivation vertical lands (the Device exclusion precedent).
27
+ - ``iAMMetadata`` -- identity-provider plumbing (an IAM GUID, a
28
+ connection name, two provisioning flags), present on only 42/157
29
+ swept records.
30
+
31
+ Every value mirrors verbatim, empty strings included -- the model
32
+ preserves ``""`` faithfully from the wire, and empty strings normalize
33
+ to null once, at the DataFrame boundary (DESIGN section 9). Sentinels
34
+ likewise: ``activeTo`` of ``2050-01-01`` is GeoTab's still-active
35
+ sentinel, and ``hosRuleSet`` carries the literal string ``"None"`` on
36
+ non-driving accounts -- the provider's vocabulary, never interpreted.
37
+
38
+ Wire keys are camelCase; fields are snake_case via the ``to_camel``
39
+ alias generator. The five acronym keys the generator cannot produce
40
+ (``acceptedEULA``, ``isEULAAccepted``, ``isExemptHOSEnabled``,
41
+ ``maxPCDistancePerDay``, ``wifiEULA``) carry explicit alias overrides,
42
+ each pinned against a captured value in the model tests (the Device
43
+ acronym-trap precedent).
44
+ """
45
+
46
+ from datetime import datetime
47
+
48
+ from pydantic import ConfigDict, Field
49
+ from pydantic.alias_generators import to_camel
50
+
51
+ from fleetpull.model_contract import ResponseModel
52
+
53
+ __all__: list[str] = ['User', 'UserAccessGroupFilterRef']
54
+
55
+
56
+ class UserAccessGroupFilterRef(ResponseModel):
57
+ """The ``accessGroupFilter`` reference: a data-scope filter by id.
58
+
59
+ Observed on exactly one of the 157 swept records (2026-07-16) -- an
60
+ account whose visibility is restricted to an access group. The id is
61
+ the ``a``-prefixed GUID-like form (the ExceptionEvent id shape), a
62
+ pure reference mirrored as-is.
63
+ """
64
+
65
+ model_config = ConfigDict(alias_generator=to_camel)
66
+
67
+ id: str
68
+
69
+
70
+ class User(ResponseModel):
71
+ """One GeoTab User entity: a console account, driver or otherwise.
72
+
73
+ A pure mirror of the captured scalar fields. Field semantics are
74
+ GeoTab's; no value is derived or interpreted here. Groups below
75
+ follow the captured record layout: identity and lifecycle, person
76
+ and contact, the driver-only block, company/authority identity,
77
+ locale and display preferences, HOS and driving flags, and the
78
+ notification/UI flags.
79
+
80
+ Attributes:
81
+ id: GeoTab's user id -- the seek-paging sort key (hex-suffixed
82
+ string, ascending; the id space trip ``driver`` refs point
83
+ into).
84
+ name: The login identifier -- an email address on most captured
85
+ accounts, a bare username on one.
86
+ active_from: Start of the account's active window (UTC).
87
+ active_to: End of the active window; ``2050-01-01`` is GeoTab's
88
+ still-active sentinel, stored as-is.
89
+ last_access_date: Last console/app access (UTC); absent on one
90
+ never-accessed swept account (156/157).
91
+ is_driver: Whether the account is a driver -- the key whose
92
+ value predicts the driver-only block's presence.
93
+ license_number: Driver's license number; driver-only (129/157).
94
+ license_province: Driver's license state/province; driver-only
95
+ (129/157).
96
+ view_drivers_own_data_only: Driver data-visibility restriction;
97
+ driver-only (129/157).
98
+ access_group_filter: Data-scope filter reference; observed on
99
+ one swept record (1/157).
100
+ max_pc_distance_per_day: Personal-conveyance distance cap;
101
+ ``0`` captured everywhere it appears (126/157, not aligned
102
+ with the driver split).
103
+ hos_rule_set: HOS ruleset name; the literal string ``"None"``
104
+ on non-driving accounts, mirrored verbatim.
105
+ carrier_number: Motor-carrier number; captured populated on
106
+ driver accounts, ``""`` elsewhere, mirrored verbatim.
107
+ """
108
+
109
+ model_config = ConfigDict(alias_generator=to_camel)
110
+
111
+ # Identity and lifecycle.
112
+ id: str
113
+ name: str
114
+ active_from: datetime
115
+ active_to: datetime
116
+ last_access_date: datetime | None = None
117
+ is_auto_added: bool
118
+ user_authentication_type: str
119
+ change_password: bool
120
+ access_group_filter: UserAccessGroupFilterRef | None = None
121
+
122
+ # Person and contact.
123
+ first_name: str
124
+ last_name: str
125
+ designation: str
126
+ employee_no: str
127
+ phone_number: str
128
+ phone_number_extension: str
129
+ comment: str
130
+
131
+ # Driver-only block (absent, not null, on non-driver accounts).
132
+ is_driver: bool
133
+ license_number: str | None = None
134
+ license_province: str | None = None
135
+ view_drivers_own_data_only: bool | None = None
136
+
137
+ # Company / authority identity.
138
+ company_name: str
139
+ company_address: str
140
+ authority_name: str
141
+ authority_address: str
142
+ carrier_number: str
143
+
144
+ # Locale and display preferences.
145
+ language: str
146
+ country_code: str
147
+ time_zone_id: str
148
+ date_format: str
149
+ first_day_of_week: str
150
+ display_currency: str
151
+ is_metric: bool
152
+ fuel_economy_unit: str
153
+ electric_energy_economy_unit: str
154
+ default_page: str
155
+ default_map_engine: str
156
+ default_google_map_style: str
157
+ default_here_map_style: str
158
+ default_open_street_map_style: str
159
+ zone_display_mode: str
160
+ feature_preview: str
161
+
162
+ # HOS and driving flags.
163
+ hos_rule_set: str
164
+ is_exempt_hos_enabled: bool = Field(alias='isExemptHOSEnabled')
165
+ is_yard_move_enabled: bool
166
+ is_personal_conveyance_enabled: bool
167
+ is_adverse_driving_enabled: bool
168
+ max_pc_distance_per_day: int | None = Field(
169
+ default=None, alias='maxPCDistancePerDay'
170
+ )
171
+
172
+ # EULA state.
173
+ is_eula_accepted: bool = Field(alias='isEULAAccepted')
174
+ accepted_eula: int = Field(alias='acceptedEULA')
175
+ wifi_eula: int = Field(alias='wifiEULA')
176
+
177
+ # Notification and UI flags.
178
+ is_email_report_enabled: bool
179
+ is_maintenance_notification_enabled: bool
180
+ is_service_disruption_notifications_enabled: bool
181
+ sms_notifications_opt_in: bool
182
+ whats_app_notifications_opt_in: bool
183
+ is_news_enabled: bool
184
+ is_labs_enabled: bool
185
+ is_ace_disclaimer_disabled: bool
186
+ show_click_once_warning: bool
187
+ show_rate_this_app: bool