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,108 @@
1
+ # src/fleetpull/models/samsara/__init__.py
2
+ """Samsara response models; the face re-exports each endpoint module's models."""
3
+
4
+ from fleetpull.models.samsara.address import (
5
+ Address,
6
+ AddressGeofence,
7
+ AddressGeofenceCircle,
8
+ AddressGeofenceSettings,
9
+ )
10
+ from fleetpull.models.samsara.asset_location import (
11
+ AssetLocation,
12
+ AssetLocationAssetRef,
13
+ AssetLocationFix,
14
+ )
15
+ from fleetpull.models.samsara.driver import (
16
+ Driver,
17
+ DriverActivationStatus,
18
+ DriverCarrierSettings,
19
+ DriverHosSetting,
20
+ DriverStaticAssignedVehicleRef,
21
+ DriverTagRef,
22
+ )
23
+ from fleetpull.models.samsara.driver_fuel_energy_report import (
24
+ DriverFuelEnergyCost,
25
+ DriverFuelEnergyDriverRef,
26
+ DriverFuelEnergyReport,
27
+ )
28
+ from fleetpull.models.samsara.driver_vehicle_assignment import (
29
+ AssignmentDriverRef,
30
+ AssignmentVehicleExternalIds,
31
+ AssignmentVehicleRef,
32
+ DriverVehicleAssignment,
33
+ )
34
+ from fleetpull.models.samsara.engine_state import EngineState
35
+ from fleetpull.models.samsara.gps_reading import (
36
+ GpsReading,
37
+ GpsReadingAddressRef,
38
+ GpsReadingReverseGeo,
39
+ )
40
+ from fleetpull.models.samsara.idling_event import (
41
+ AssetRef,
42
+ FuelCost,
43
+ IdlingAddress,
44
+ IdlingEvent,
45
+ OperatorRef,
46
+ )
47
+ from fleetpull.models.samsara.odometer_reading import OdometerReading
48
+ from fleetpull.models.samsara.trip import (
49
+ Trip,
50
+ TripAddress,
51
+ TripCoordinates,
52
+ )
53
+ from fleetpull.models.samsara.vehicle import (
54
+ Vehicle,
55
+ VehicleExternalIds,
56
+ VehicleGatewayRef,
57
+ VehicleStaticAssignedDriverRef,
58
+ )
59
+ from fleetpull.models.samsara.vehicle_fuel_energy_report import (
60
+ VehicleFuelEnergyCost,
61
+ VehicleFuelEnergyExternalIds,
62
+ VehicleFuelEnergyReport,
63
+ VehicleFuelEnergyVehicleRef,
64
+ )
65
+
66
+ __all__: list[str] = [
67
+ 'Address',
68
+ 'AddressGeofence',
69
+ 'AddressGeofenceCircle',
70
+ 'AddressGeofenceSettings',
71
+ 'AssetLocation',
72
+ 'AssetLocationAssetRef',
73
+ 'AssetLocationFix',
74
+ 'AssetRef',
75
+ 'AssignmentDriverRef',
76
+ 'AssignmentVehicleExternalIds',
77
+ 'AssignmentVehicleRef',
78
+ 'Driver',
79
+ 'DriverActivationStatus',
80
+ 'DriverCarrierSettings',
81
+ 'DriverFuelEnergyCost',
82
+ 'DriverFuelEnergyDriverRef',
83
+ 'DriverFuelEnergyReport',
84
+ 'DriverHosSetting',
85
+ 'DriverStaticAssignedVehicleRef',
86
+ 'DriverTagRef',
87
+ 'DriverVehicleAssignment',
88
+ 'EngineState',
89
+ 'FuelCost',
90
+ 'GpsReading',
91
+ 'GpsReadingAddressRef',
92
+ 'GpsReadingReverseGeo',
93
+ 'IdlingAddress',
94
+ 'IdlingEvent',
95
+ 'OdometerReading',
96
+ 'OperatorRef',
97
+ 'Trip',
98
+ 'TripAddress',
99
+ 'TripCoordinates',
100
+ 'Vehicle',
101
+ 'VehicleExternalIds',
102
+ 'VehicleFuelEnergyCost',
103
+ 'VehicleFuelEnergyExternalIds',
104
+ 'VehicleFuelEnergyReport',
105
+ 'VehicleFuelEnergyVehicleRef',
106
+ 'VehicleGatewayRef',
107
+ 'VehicleStaticAssignedDriverRef',
108
+ ]
@@ -0,0 +1,149 @@
1
+ # src/fleetpull/models/samsara/address.py
2
+ """Samsara Address response model (``GET /addresses``).
3
+
4
+ Written from captured live responses (2026-07-20 probe session: a
5
+ full-population walk -- one page, all 25 records), never from docs. The
6
+ walk observed no null value anywhere -- Samsara omits absent keys
7
+ rather than nulling them (the vehicles posture). Because the walk was
8
+ the whole population, the vehicles optionality posture applies: the
9
+ seven 25/25 keys (``id``, ``name``, ``createdAtTime``,
10
+ ``formattedAddress``, ``latitude``, ``longitude``, ``geofence``) are
11
+ required; ``addressTypes`` (20/25) is optional. ``createdAtTime`` is
12
+ UTC ISO-8601 with milliseconds, recovered as a tz-aware datetime by
13
+ Pydantic's standard parse.
14
+
15
+ Within the required ``geofence`` block (presence out of 25 blocks):
16
+ ``circle`` 1/25 (``{latitude, longitude, radiusMeters}``, all three in
17
+ the one carrying block), ``polygon`` 24/25, ``settings`` 13/25
18
+ (``{showAddresses}``, present in every carrying block). ``circle`` and
19
+ ``polygon`` were mutually exclusive in capture (1 vs 24, never both) --
20
+ both are mirrored as independent optionals with NO XOR enforcement
21
+ (mirror, never interpret).
22
+
23
+ Excluded fields (``extra='ignore'`` makes exclusion exactly "don't
24
+ model it"):
25
+
26
+ - ``tags`` -- a list of tag objects (9/25); the records layer's schema
27
+ derivation supports scalars, enums, ``list[scalar]``, and nested
28
+ models only (the GeoTab Device/User exclusion precedent, same as the
29
+ vehicles/drivers models) -- modeled when the list-of-structs
30
+ derivation vertical lands.
31
+ - ``geofence.polygon`` -- excluded WHOLESALE (24/25): its ONLY key is
32
+ ``vertices``, a list of ``{latitude, longitude}`` objects, so the
33
+ same exclusion precedent applies one level down and an empty polygon
34
+ model would mirror nothing. The top-level ``latitude``/``longitude``
35
+ still carry the address's center point on every record, so a
36
+ polygon-fenced address keeps its location while the boundary awaits
37
+ the list-of-structs vertical.
38
+
39
+ Wire keys are camelCase; fields are snake_case via the ``to_camel``
40
+ alias generator.
41
+ """
42
+
43
+ from datetime import datetime
44
+
45
+ from pydantic import ConfigDict
46
+ from pydantic.alias_generators import to_camel
47
+
48
+ from fleetpull.model_contract import ResponseModel
49
+
50
+ __all__: list[str] = [
51
+ 'Address',
52
+ 'AddressGeofence',
53
+ 'AddressGeofenceCircle',
54
+ 'AddressGeofenceSettings',
55
+ ]
56
+
57
+
58
+ class AddressGeofenceCircle(ResponseModel):
59
+ """The ``geofence.circle`` block: a circular boundary (1/25).
60
+
61
+ All three keys were present in the one carrying block. Mutually
62
+ exclusive with ``polygon`` in capture, mirrored without enforcement
63
+ (module docstring).
64
+
65
+ Attributes:
66
+ latitude: The circle center's latitude, decimal degrees.
67
+ longitude: The circle center's longitude, decimal degrees.
68
+ radius_meters: The circle's radius in meters, a bare int.
69
+ """
70
+
71
+ model_config = ConfigDict(alias_generator=to_camel)
72
+
73
+ latitude: float
74
+ longitude: float
75
+ radius_meters: int
76
+
77
+
78
+ class AddressGeofenceSettings(ResponseModel):
79
+ """The ``geofence.settings`` block: geofence display settings (13/25).
80
+
81
+ Attributes:
82
+ show_addresses: Whether the geofence displays addresses --
83
+ present in every carrying block.
84
+ """
85
+
86
+ model_config = ConfigDict(alias_generator=to_camel)
87
+
88
+ show_addresses: bool
89
+
90
+
91
+ class AddressGeofence(ResponseModel):
92
+ """The ``geofence`` block: boundary and settings (present on all 25).
93
+
94
+ Models ``circle`` and ``settings`` only -- ``polygon`` is excluded
95
+ wholesale because its only content is a list-of-objects vertex list
96
+ (module docstring), so a polygon-fenced address validates to a
97
+ geofence with both modeled fields ``None`` while its center point
98
+ survives on the parent's ``latitude``/``longitude``.
99
+
100
+ Attributes:
101
+ circle: The circular boundary (1/25; mutually exclusive with
102
+ the unmodeled ``polygon`` in capture, not enforced).
103
+ settings: The display-settings block (13/25).
104
+ """
105
+
106
+ circle: AddressGeofenceCircle | None = None
107
+ settings: AddressGeofenceSettings | None = None
108
+
109
+
110
+ class Address(ResponseModel):
111
+ """One Samsara defined address (a named location with a geofence).
112
+
113
+ A pure mirror of the captured fields (``tags`` and
114
+ ``geofence.polygon`` excluded, module docstring). Field semantics
115
+ are Samsara's; no value is derived or interpreted here. The walk
116
+ was the whole 25-record population, so the always-present keys are
117
+ required (the vehicles posture).
118
+
119
+ Attributes:
120
+ id: Samsara's address id -- a string, mirrored as string.
121
+ name: The address's display name.
122
+ created_at_time: Record creation (UTC, millisecond ISO-8601).
123
+ formatted_address: The full street address, one formatted
124
+ string.
125
+ latitude: The address's center-point latitude, decimal degrees
126
+ -- carried on every record, polygon-fenced ones included.
127
+ longitude: The address's center-point longitude, decimal
128
+ degrees.
129
+ geofence: The geofence block (circle/settings modeled; polygon
130
+ excluded).
131
+ address_types: The address's type tags, a list of strings
132
+ (20/25).
133
+ """
134
+
135
+ model_config = ConfigDict(alias_generator=to_camel)
136
+
137
+ # Identity and lifecycle.
138
+ id: str
139
+ name: str
140
+ created_at_time: datetime
141
+
142
+ # Location.
143
+ formatted_address: str
144
+ latitude: float
145
+ longitude: float
146
+
147
+ # Boundary and classification.
148
+ geofence: AddressGeofence
149
+ address_types: list[str] | None = None
@@ -0,0 +1,131 @@
1
+ # src/fleetpull/models/samsara/asset_location.py
2
+ """Samsara AssetLocation response model
3
+ (``GET /assets/location-and-speed/stream``).
4
+
5
+ Written from captured live responses (2026-07-20 probe session: a
6
+ 454-record page census with every nested block censused over 300
7
+ records; a 50-id one-hour walk of 2 pages / 701 records), never from
8
+ docs. The legacy hub called this surface ``location_stream``; the
9
+ catalog name is ``asset_locations`` per the name=plural-of-entity
10
+ invariant -- the stored entity is the asset-location reading, one row
11
+ per fix.
12
+
13
+ The record census (454/454 unless noted): ``happenedAtTime`` (RFC3339
14
+ str, recovered tz-aware UTC -- the event-time column; readings fall
15
+ strictly inside the requested half-open ``[startTime, endTime)``
16
+ window, probe-proven on a 12:00-13:00Z window returning min 12:00:03Z /
17
+ max 12:59:56Z), ``asset`` (an object whose ONLY observed key is ``id``,
18
+ a STRING on the wire, 300/300 -- note the contrast with idling_events'
19
+ bare-int ``asset.id``: per-endpoint wire truth, mirrored per endpoint),
20
+ and ``location`` (300/300 census within the block: ``accuracyMeters``
21
+ int on every censused record but FLOAT on the live walk -- the
22
+ 2026-07-20 full-day live proof failed validation on a float value at
23
+ record 351, so the field is float, the census sample proven narrower
24
+ than the wire; ``headingDegrees`` int, ``latitude`` float,
25
+ ``longitude`` float).
26
+
27
+ Requiredness posture: 300/300 on a 454-record page is NOT a
28
+ whole-population oath (the drivers conservative posture would leave
29
+ everything optional), but the location core is required anyway by
30
+ structural judgment -- a location record without coordinates mirrors
31
+ nothing and is structurally meaningless, so a future record omitting
32
+ them should fail loudly rather than land an all-null coordinate row.
33
+ The same judgment covers ``asset``/``asset.id`` (an unattributed
34
+ reading cannot be a reading of anything) and ``happenedAtTime`` (the
35
+ event-time column the watermark routes on). This is the one deliberate
36
+ departure from the pure conservative posture, recorded here and in
37
+ DESIGN section 8.
38
+
39
+ Not modeled, with different provenance:
40
+
41
+ - ``location.geofence`` -- OBSERVED-EMPTY, not excluded: present
42
+ 300/300 but an empty object with ZERO keys on every censused record,
43
+ so there is nothing to mirror (``extra='ignore'`` drops it). Revisit
44
+ on a capture showing content.
45
+ - Any speed key -- UNOBSERVED despite the surface's name
46
+ (``location-and-speed``): no speed key appeared anywhere in the
47
+ census. Unmodeled as unobserved (never excluded); revisit on a
48
+ capture that shows one.
49
+
50
+ Wire keys are camelCase; fields are snake_case via the ``to_camel``
51
+ alias generator.
52
+ """
53
+
54
+ from datetime import datetime
55
+
56
+ from pydantic import ConfigDict
57
+ from pydantic.alias_generators import to_camel
58
+
59
+ from fleetpull.model_contract import ResponseModel
60
+
61
+ __all__: list[str] = [
62
+ 'AssetLocation',
63
+ 'AssetLocationAssetRef',
64
+ 'AssetLocationFix',
65
+ ]
66
+
67
+
68
+ class AssetLocationAssetRef(ResponseModel):
69
+ """The ``asset`` block: the reading's asset reference.
70
+
71
+ Its ONLY observed key is ``id`` (300/300) -- a STRING on the wire,
72
+ unlike idling_events' bare-int ``asset.id`` (per-endpoint wire
73
+ truth, mirrored per endpoint).
74
+
75
+ Attributes:
76
+ id: Samsara's asset id -- a string, mirrored as string.
77
+ """
78
+
79
+ id: str
80
+
81
+
82
+ class AssetLocationFix(ResponseModel):
83
+ """The ``location`` block: one position fix.
84
+
85
+ All four modeled keys were 300/300 in the block census and are
86
+ required by structural judgment (module docstring): a fix without
87
+ coordinates mirrors nothing. The block's ``geofence`` key is
88
+ observed-empty (an empty object on every censused record) and is
89
+ not modeled -- there is nothing to mirror.
90
+
91
+ Attributes:
92
+ accuracy_meters: The fix's reported accuracy in meters --
93
+ float: the census saw only bare ints (300/300), but the
94
+ live full-day walk carried floats (proven 2026-07-20, the
95
+ validation failure that widened this field).
96
+ heading_degrees: Heading in degrees, a bare int.
97
+ latitude: Fix latitude, decimal degrees.
98
+ longitude: Fix longitude, decimal degrees.
99
+ """
100
+
101
+ model_config = ConfigDict(alias_generator=to_camel)
102
+
103
+ accuracy_meters: float
104
+ heading_degrees: int
105
+ latitude: float
106
+ longitude: float
107
+
108
+
109
+ class AssetLocation(ResponseModel):
110
+ """One location reading of one asset, at the reading grain.
111
+
112
+ A pure mirror of the captured record (``location.geofence``
113
+ observed-empty and any speed key unobserved -- module docstring).
114
+ Field semantics and units are Samsara's; no value is derived or
115
+ interpreted here.
116
+
117
+ Attributes:
118
+ happened_at_time: The reading instant (RFC3339, recovered
119
+ tz-aware UTC) -- the event-time column; readings fall
120
+ strictly inside the requested window.
121
+ asset: The asset reference (string id) -- the per-record
122
+ attribution that makes the batched fan-out pure transport
123
+ packing.
124
+ location: The position fix block.
125
+ """
126
+
127
+ model_config = ConfigDict(alias_generator=to_camel)
128
+
129
+ happened_at_time: datetime
130
+ asset: AssetLocationAssetRef
131
+ location: AssetLocationFix
@@ -0,0 +1,232 @@
1
+ # src/fleetpull/models/samsara/driver.py
2
+ """Samsara Driver response model (``GET /fleet/drivers``).
3
+
4
+ Written from captured live responses (2026-07-20 probe session: the
5
+ full two-sweep census over all 832 records -- 460 active plus 372
6
+ deactivated, fully disjoint -- plus captured record variants), never
7
+ from docs. The census observed no null value anywhere -- Samsara omits
8
+ absent keys rather than nulling them (the vehicles posture), while
9
+ also using empty strings (``homeTerminalName`` / ``homeTerminalAddress``
10
+ are ``""`` on 204 and 268 of the 460 active records), which mirror
11
+ verbatim and normalize to null at the DataFrame boundary.
12
+
13
+ Optionality is conservative: only ``id`` is required. The per-key
14
+ presence counts were fully enumerated on the active sweep only (the
15
+ attribute docs cite them, out of 460); the deactivated sweep matched
16
+ structurally but was not per-key sworn, so unlike the vehicles model
17
+ (whose one sweep was the whole population) the always-present-in-capture
18
+ keys stay optional here rather than required.
19
+
20
+ ``driverActivationStatus`` is a strict CLOSED enum, proven by the API
21
+ itself: any other value -- case variants, comma-joins, repeated keys,
22
+ bogus strings -- returns HTTP 400 naming the two admissible values
23
+ (captured 2026-07-20), so the two-member ``DriverActivationStatus``
24
+ mirror is closed by evidence, not assumption. ``dotNumber`` is a BARE
25
+ integer on the wire (not quoted). ``eldExemptReason`` is a free-text
26
+ reason string on the wire (282/460 presence, captured 2026-07-20).
27
+
28
+ Excluded fields (``extra='ignore'`` makes exclusion exactly "don't
29
+ model it"):
30
+
31
+ - ``tags`` -- a list of ``{id, name, parentTagId}`` objects (441/460);
32
+ the records layer's schema derivation supports scalars, enums,
33
+ ``list[scalar]``, and nested models only (the GeoTab Device/User
34
+ exclusion precedent) -- modeled when the list-of-structs derivation
35
+ vertical lands.
36
+ - ``eldSettings`` -- ``{rulesets: [{break, cycle, restart, shift}]}``,
37
+ a list-of-objects block (190/460); same exclusion precedent.
38
+
39
+ ``externalIds`` was NEVER observed on any of the 832 swept records --
40
+ deliberately not modeled (unobserved, not excluded); revisit only on a
41
+ capture that shows one, per the union-of-observed discipline.
42
+
43
+ Wire keys are camelCase; fields are snake_case via the ``to_camel``
44
+ alias generator.
45
+ """
46
+
47
+ from datetime import datetime
48
+ from enum import StrEnum
49
+
50
+ from pydantic import ConfigDict
51
+ from pydantic.alias_generators import to_camel
52
+
53
+ from fleetpull.model_contract import ResponseModel
54
+
55
+ __all__: list[str] = [
56
+ 'Driver',
57
+ 'DriverActivationStatus',
58
+ 'DriverCarrierSettings',
59
+ 'DriverHosSetting',
60
+ 'DriverStaticAssignedVehicleRef',
61
+ 'DriverTagRef',
62
+ ]
63
+
64
+
65
+ class DriverActivationStatus(StrEnum):
66
+ """Activation status for a Samsara driver.
67
+
68
+ A closed mirror whose closure is API-enforced: every probed variant
69
+ outside these two values returned HTTP 400 naming exactly this
70
+ vocabulary (captured 2026-07-20), so the enum is evidence-closed.
71
+ Kept as an enum (not downgraded to ``str``) because the wire values
72
+ match the member values exactly -- membership validation for free on
73
+ a faithful mirror (the Motive ``VehicleStatus`` precedent).
74
+ """
75
+
76
+ ACTIVE = 'active'
77
+ DEACTIVATED = 'deactivated'
78
+
79
+
80
+ class DriverCarrierSettings(ResponseModel):
81
+ """The ``carrierSettings`` block: carrier identity and home terminal.
82
+
83
+ Present on every captured record, with all five keys present in
84
+ every carrying block; the home-terminal pair frequently carries
85
+ empty strings (204/460 and 268/460 active), mirrored verbatim.
86
+
87
+ Attributes:
88
+ carrier_name: The carrier's display name.
89
+ dot_number: The carrier's USDOT number -- a BARE integer on the
90
+ wire (captured six-digit), never quoted.
91
+ main_office_address: The carrier's main office address.
92
+ home_terminal_name: The driver's home terminal name; ``""`` on
93
+ 204/460 active records.
94
+ home_terminal_address: The home terminal address; ``""`` on
95
+ 268/460 active records.
96
+ """
97
+
98
+ model_config = ConfigDict(alias_generator=to_camel)
99
+
100
+ carrier_name: str
101
+ dot_number: int
102
+ main_office_address: str
103
+ home_terminal_name: str
104
+ home_terminal_address: str
105
+
106
+
107
+ class DriverHosSetting(ResponseModel):
108
+ """The ``hosSetting`` block: hours-of-service configuration flags.
109
+
110
+ Attributes:
111
+ heavy_haul_exemption_toggle_enabled: Whether the heavy-haul
112
+ exemption toggle is enabled for the driver.
113
+ """
114
+
115
+ model_config = ConfigDict(alias_generator=to_camel)
116
+
117
+ heavy_haul_exemption_toggle_enabled: bool
118
+
119
+
120
+ class DriverStaticAssignedVehicleRef(ResponseModel):
121
+ """The statically assigned vehicle reference: id and display name."""
122
+
123
+ id: str
124
+ name: str
125
+
126
+
127
+ class DriverTagRef(ResponseModel):
128
+ """A tag reference: the shared ``{id, name, parentTagId}`` shape.
129
+
130
+ The one shape both singular tag references (``peerGroupTag``,
131
+ ``vehicleGroupTag``) carry -- all three keys present in every
132
+ captured block. Deliberately NOT reused for the ``tags`` list, which
133
+ stays excluded wholesale (module docstring).
134
+
135
+ Attributes:
136
+ id: The tag's id.
137
+ name: The tag's display name.
138
+ parent_tag_id: The parent tag's id.
139
+ """
140
+
141
+ model_config = ConfigDict(alias_generator=to_camel)
142
+
143
+ id: str
144
+ name: str
145
+ parent_tag_id: str
146
+
147
+
148
+ class Driver(ResponseModel):
149
+ """One Samsara fleet driver, active or deactivated.
150
+
151
+ A pure mirror of the captured fields (``tags`` and ``eldSettings``
152
+ excluded, ``externalIds`` unobserved -- module docstring). Field
153
+ semantics are Samsara's; no value is derived or interpreted here.
154
+ The one complete driver dataset is the union of the two activation
155
+ sweeps; ``driver_activation_status`` carries the split.
156
+
157
+ Presence counts below are out of the 460 active records; keys with
158
+ no count were present on every one (the deactivated sweep matched
159
+ structurally). Only ``id`` is required (module docstring).
160
+
161
+ Attributes:
162
+ id: Samsara's driver id -- a numeric string, mirrored as string.
163
+ name: The driver's display name.
164
+ username: The driver-app login name.
165
+ driver_activation_status: ``active`` / ``deactivated`` -- the
166
+ API-enforced closed enum, and the two-sweep split column.
167
+ timezone: The driver's IANA timezone (e.g.
168
+ ``America/Chicago``).
169
+ created_at_time: Record creation (UTC, millisecond ISO-8601).
170
+ updated_at_time: Last record update (UTC).
171
+ has_vehicle_unpinning_enabled: Vehicle-unpinning flag.
172
+ carrier_settings: Carrier identity and home terminal block.
173
+ hos_setting: Hours-of-service configuration block.
174
+ static_assigned_vehicle: Statically assigned vehicle reference
175
+ (102/460).
176
+ peer_group_tag: Peer-group tag reference (4/460).
177
+ vehicle_group_tag: Vehicle-group tag reference (8/460).
178
+ license_number: Driving license number (172/460).
179
+ license_state: License issuing state (269/460).
180
+ phone: Contact phone number (7/460).
181
+ locale: Display locale (1/460).
182
+ notes: Free-form notes (1/460).
183
+ profile_image_url: Profile image URL (1/460).
184
+ eld_exempt: ELD exemption flag (270/460).
185
+ eld_exempt_reason: Free-text exemption reason (282/460).
186
+ eld_adverse_weather_exemption_enabled: ELD adverse-weather
187
+ exemption flag (191/460).
188
+ eld_big_day_exemption_enabled: ELD big-day exemption flag
189
+ (186/460).
190
+ eld_pc_enabled: ELD personal-conveyance flag (77/460).
191
+ eld_ym_enabled: ELD yard-move flag (100/460).
192
+ waiting_time_duty_status_enabled: Waiting-time duty-status flag
193
+ (8/460).
194
+ """
195
+
196
+ model_config = ConfigDict(alias_generator=to_camel)
197
+
198
+ # Identity and lifecycle.
199
+ id: str
200
+ name: str | None = None
201
+ username: str | None = None
202
+ driver_activation_status: DriverActivationStatus | None = None
203
+ timezone: str | None = None
204
+ created_at_time: datetime | None = None
205
+ updated_at_time: datetime | None = None
206
+ has_vehicle_unpinning_enabled: bool | None = None
207
+
208
+ # Carrier and HOS configuration blocks.
209
+ carrier_settings: DriverCarrierSettings | None = None
210
+ hos_setting: DriverHosSetting | None = None
211
+
212
+ # Assignment and grouping references.
213
+ static_assigned_vehicle: DriverStaticAssignedVehicleRef | None = None
214
+ peer_group_tag: DriverTagRef | None = None
215
+ vehicle_group_tag: DriverTagRef | None = None
216
+
217
+ # Person and contact.
218
+ license_number: str | None = None
219
+ license_state: str | None = None
220
+ phone: str | None = None
221
+ locale: str | None = None
222
+ notes: str | None = None
223
+ profile_image_url: str | None = None
224
+
225
+ # ELD and duty-status flags.
226
+ eld_exempt: bool | None = None
227
+ eld_exempt_reason: str | None = None
228
+ eld_adverse_weather_exemption_enabled: bool | None = None
229
+ eld_big_day_exemption_enabled: bool | None = None
230
+ eld_pc_enabled: bool | None = None
231
+ eld_ym_enabled: bool | None = None
232
+ waiting_time_duty_status_enabled: bool | None = None