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,74 @@
1
+ # src/fleetpull/endpoints/geotab/log_records.py
2
+ """The GeoTab log_records binding: the first GeoTab feed vertical.
3
+
4
+ A ``GetFeed`` drive of the ``LogRecord`` entity — the ACTIVE GPS stream:
5
+ records are emitted once, never re-emitted, and carry no per-record
6
+ version, so the append-only cell is trivially complete and the consumer
7
+ reconciles by ``id`` (DESIGN §4). The run resumes from the stored
8
+ ``FeedToken`` (or a ``FeedSeed`` at the sync-wide cold-start anchor on
9
+ the tokenless first run), rides the shared ``GeotabGetFeedSpecBuilder``
10
+ and ``GeotabFeedPageDecoder``, spends from the ``geotab_feed``
11
+ method-class budget, and appends every emitted record into its event
12
+ date's partition as numbered part files — stored as emitted, nothing
13
+ ever deleted or replaced.
14
+
15
+ ``resultsLimit`` is the 50,000 protocol maximum: the probed tenant emits
16
+ >50,000 LogRecords/day (a 50,000-record page did not cover one day), so
17
+ anything smaller only multiplies pages against the ~60/min feed budget
18
+ (DESIGN §8, the 2026-07-21 feed wave block).
19
+
20
+ Every request here is a JSON-RPC POST whose ``params.credentials`` and
21
+ resolved host are the session strategy's injections (the devices-leaf
22
+ convention); the host this module writes is a pre-auth placeholder.
23
+ """
24
+
25
+ from typing import Final
26
+
27
+ from fleetpull.config import GeotabConfig
28
+ from fleetpull.endpoints.geotab._requests import GeotabGetFeedSpecBuilder, server_host
29
+ from fleetpull.endpoints.shared import EndpointDefinition, FeedMode, StorageKind
30
+ from fleetpull.models.geotab import LogRecord
31
+ from fleetpull.network.decoders import GeotabFeedPageDecoder
32
+ from fleetpull.vocabulary import Provider, QuotaScope
33
+
34
+ __all__: list[str] = ['build_endpoint']
35
+
36
+ # The GetFeed protocol maximum; the daily volume exceeds one page.
37
+ _RESULTS_LIMIT: Final[int] = 50000
38
+
39
+ _LOG_RECORD_TYPE_NAME: Final[str] = 'LogRecord'
40
+
41
+
42
+ def build_endpoint(config: GeotabConfig) -> EndpointDefinition[LogRecord]:
43
+ """Build the GeoTab log_records feed binding.
44
+
45
+ The GPS stream fetched incrementally as a version-token feed: the
46
+ run resumes from the stored token (seeded via ``search.fromDate``
47
+ on the tokenless first run only), each page appends durably before
48
+ its ``toVersion`` commits, and the fetched records land in
49
+ ``date=YYYY-MM-DD`` partitions as new numbered part files.
50
+
51
+ Args:
52
+ config: The validated GeoTab configuration; supplies the
53
+ authentication host the pre-auth spec URLs are built on.
54
+
55
+ Returns:
56
+ The frozen log_records ``EndpointDefinition``. Construction
57
+ validates the ``FeedMode`` / ``APPEND_LOG`` /
58
+ ``event_time_column`` triple against the response model.
59
+ """
60
+ return EndpointDefinition(
61
+ provider=Provider.GEOTAB,
62
+ name='log_records',
63
+ spec_builder=GeotabGetFeedSpecBuilder(
64
+ server=server_host(config),
65
+ type_name=_LOG_RECORD_TYPE_NAME,
66
+ results_limit=_RESULTS_LIMIT,
67
+ ),
68
+ page_decoder=GeotabFeedPageDecoder(),
69
+ response_model=LogRecord,
70
+ quota_scope=QuotaScope.GEOTAB_FEED,
71
+ storage_kind=StorageKind.APPEND_LOG,
72
+ sync_mode=FeedMode(),
73
+ event_time_column='date_time',
74
+ )
@@ -0,0 +1,74 @@
1
+ # src/fleetpull/endpoints/geotab/media_files.py
2
+ """The GeoTab media_files binding: the media-attachment feed.
3
+
4
+ A ``GetFeed`` drive of the ``MediaFile`` entity — media attachments
5
+ (images, video, ...) carrying a per-record ``version``; the consumer
6
+ reconciles by ``(id, max version)`` (DESIGN §4). MediaFile carries NO
7
+ ``dateTime`` key: the event-time identity is ``fromDate`` (the media
8
+ start, 55/55), so this leaf anchors ``event_time_column='from_date'``
9
+ rather than the ``date_time`` its feed siblings use
10
+ (``models/geotab/media_file.py``). Thin evidence at the probed tenant —
11
+ 55 records over 730 days — so the model is conservative.
12
+
13
+ ``resultsLimit`` is the 50,000 protocol maximum: the docs list no lower
14
+ per-type cap for this type (the SCALE census cannot probe a cap the
15
+ 55-record population never reaches — the FillUp dual-provenance lesson's
16
+ other arm, DESIGN §8), so the protocol maximum stands.
17
+
18
+ Every request here is a JSON-RPC POST whose ``params.credentials`` and
19
+ resolved host are the session strategy's injections (the devices-leaf
20
+ convention); the host this module writes is a pre-auth placeholder.
21
+ """
22
+
23
+ from typing import Final
24
+
25
+ from fleetpull.config import GeotabConfig
26
+ from fleetpull.endpoints.geotab._requests import GeotabGetFeedSpecBuilder, server_host
27
+ from fleetpull.endpoints.shared import EndpointDefinition, FeedMode, StorageKind
28
+ from fleetpull.models.geotab import MediaFile
29
+ from fleetpull.network.decoders import GeotabFeedPageDecoder
30
+ from fleetpull.vocabulary import Provider, QuotaScope
31
+
32
+ __all__: list[str] = ['build_endpoint']
33
+
34
+ # The GetFeed protocol maximum; no lower documented per-type cap.
35
+ _RESULTS_LIMIT: Final[int] = 50000
36
+
37
+ _MEDIA_FILE_TYPE_NAME: Final[str] = 'MediaFile'
38
+
39
+
40
+ def build_endpoint(config: GeotabConfig) -> EndpointDefinition[MediaFile]:
41
+ """Build the GeoTab media_files feed binding.
42
+
43
+ Media attachments fetched incrementally as a version-token feed: the
44
+ run resumes from the stored token (seeded via ``search.fromDate`` on
45
+ the tokenless first run only), each page appends durably before its
46
+ ``toVersion`` commits, and the fetched records land in
47
+ ``date=YYYY-MM-DD`` partitions (by ``fromDate``, the event time) as
48
+ new numbered part files — re-emitted versions accumulate for the
49
+ consumer's ``(id, max version)`` reconcile.
50
+
51
+ Args:
52
+ config: The validated GeoTab configuration; supplies the
53
+ authentication host the pre-auth spec URLs are built on.
54
+
55
+ Returns:
56
+ The frozen media_files ``EndpointDefinition``. Construction
57
+ validates the ``FeedMode`` / ``APPEND_LOG`` /
58
+ ``event_time_column`` triple against the response model.
59
+ """
60
+ return EndpointDefinition(
61
+ provider=Provider.GEOTAB,
62
+ name='media_files',
63
+ spec_builder=GeotabGetFeedSpecBuilder(
64
+ server=server_host(config),
65
+ type_name=_MEDIA_FILE_TYPE_NAME,
66
+ results_limit=_RESULTS_LIMIT,
67
+ ),
68
+ page_decoder=GeotabFeedPageDecoder(),
69
+ response_model=MediaFile,
70
+ quota_scope=QuotaScope.GEOTAB_FEED,
71
+ storage_kind=StorageKind.APPEND_LOG,
72
+ sync_mode=FeedMode(),
73
+ event_time_column='from_date',
74
+ )
@@ -0,0 +1,71 @@
1
+ # src/fleetpull/endpoints/geotab/shipment_logs.py
2
+ """The GeoTab shipment_logs binding: the shipment-manifest feed.
3
+
4
+ A ``GetFeed`` drive of the ``ShipmentLog`` entity — shipment-manifest
5
+ records attached to a driver and device, carrying a per-record
6
+ ``version``. Shipment logs are user-editable, so re-emission under newer
7
+ versions is expected, every emitted version is stored, and the consumer
8
+ reconciles by ``(id, max version)`` (DESIGN §4).
9
+
10
+ ``resultsLimit`` is the 50,000 protocol maximum: the docs list no lower
11
+ per-type cap for this type (the SCALE census cannot probe a cap the
12
+ population never reaches — the FillUp dual-provenance lesson's other
13
+ arm, DESIGN §8), so the protocol maximum stands.
14
+
15
+ Every request here is a JSON-RPC POST whose ``params.credentials`` and
16
+ resolved host are the session strategy's injections (the devices-leaf
17
+ convention); the host this module writes is a pre-auth placeholder.
18
+ """
19
+
20
+ from typing import Final
21
+
22
+ from fleetpull.config import GeotabConfig
23
+ from fleetpull.endpoints.geotab._requests import GeotabGetFeedSpecBuilder, server_host
24
+ from fleetpull.endpoints.shared import EndpointDefinition, FeedMode, StorageKind
25
+ from fleetpull.models.geotab import ShipmentLog
26
+ from fleetpull.network.decoders import GeotabFeedPageDecoder
27
+ from fleetpull.vocabulary import Provider, QuotaScope
28
+
29
+ __all__: list[str] = ['build_endpoint']
30
+
31
+ # The GetFeed protocol maximum; no lower documented per-type cap.
32
+ _RESULTS_LIMIT: Final[int] = 50000
33
+
34
+ _SHIPMENT_LOG_TYPE_NAME: Final[str] = 'ShipmentLog'
35
+
36
+
37
+ def build_endpoint(config: GeotabConfig) -> EndpointDefinition[ShipmentLog]:
38
+ """Build the GeoTab shipment_logs feed binding.
39
+
40
+ Shipment-manifest records fetched incrementally as a version-token
41
+ feed: the run resumes from the stored token (seeded via
42
+ ``search.fromDate`` on the tokenless first run only), each page
43
+ appends durably before its ``toVersion`` commits, and the fetched
44
+ records land in ``date=YYYY-MM-DD`` partitions as new numbered part
45
+ files — re-emitted versions accumulate for the consumer's
46
+ ``(id, max version)`` reconcile.
47
+
48
+ Args:
49
+ config: The validated GeoTab configuration; supplies the
50
+ authentication host the pre-auth spec URLs are built on.
51
+
52
+ Returns:
53
+ The frozen shipment_logs ``EndpointDefinition``. Construction
54
+ validates the ``FeedMode`` / ``APPEND_LOG`` /
55
+ ``event_time_column`` triple against the response model.
56
+ """
57
+ return EndpointDefinition(
58
+ provider=Provider.GEOTAB,
59
+ name='shipment_logs',
60
+ spec_builder=GeotabGetFeedSpecBuilder(
61
+ server=server_host(config),
62
+ type_name=_SHIPMENT_LOG_TYPE_NAME,
63
+ results_limit=_RESULTS_LIMIT,
64
+ ),
65
+ page_decoder=GeotabFeedPageDecoder(),
66
+ response_model=ShipmentLog,
67
+ quota_scope=QuotaScope.GEOTAB_FEED,
68
+ storage_kind=StorageKind.APPEND_LOG,
69
+ sync_mode=FeedMode(),
70
+ event_time_column='date_time',
71
+ )
@@ -0,0 +1,71 @@
1
+ # src/fleetpull/endpoints/geotab/status_data.py
2
+ """The GeoTab status_data binding: the active diagnostic feed.
3
+
4
+ A ``GetFeed`` drive of the ``StatusData`` entity — the log_records
5
+ binding with the entity swapped: an ACTIVE feed (records emitted once,
6
+ reconciled by ``id``) that, unlike LogRecord, carries a per-record
7
+ ``version`` — mirrored as wire truth (DESIGN §4/§8). The name is the
8
+ wire's own uncountable vocabulary (``StatusData`` → ``status_data``);
9
+ there is no plural to form.
10
+
11
+ ``resultsLimit`` is the 50,000 protocol maximum: the probed tenant emits
12
+ ~24,500 StatusData records/hour, so anything smaller only multiplies
13
+ pages against the ~60/min feed budget (DESIGN §8, the 2026-07-21 feed
14
+ wave block).
15
+
16
+ Every request here is a JSON-RPC POST whose ``params.credentials`` and
17
+ resolved host are the session strategy's injections (the devices-leaf
18
+ convention); the host this module writes is a pre-auth placeholder.
19
+ """
20
+
21
+ from typing import Final
22
+
23
+ from fleetpull.config import GeotabConfig
24
+ from fleetpull.endpoints.geotab._requests import GeotabGetFeedSpecBuilder, server_host
25
+ from fleetpull.endpoints.shared import EndpointDefinition, FeedMode, StorageKind
26
+ from fleetpull.models.geotab import StatusData
27
+ from fleetpull.network.decoders import GeotabFeedPageDecoder
28
+ from fleetpull.vocabulary import Provider, QuotaScope
29
+
30
+ __all__: list[str] = ['build_endpoint']
31
+
32
+ # The GetFeed protocol maximum; the hourly volume makes pages precious.
33
+ _RESULTS_LIMIT: Final[int] = 50000
34
+
35
+ _STATUS_DATA_TYPE_NAME: Final[str] = 'StatusData'
36
+
37
+
38
+ def build_endpoint(config: GeotabConfig) -> EndpointDefinition[StatusData]:
39
+ """Build the GeoTab status_data feed binding.
40
+
41
+ The diagnostic stream fetched incrementally as a version-token
42
+ feed: the run resumes from the stored token (seeded via
43
+ ``search.fromDate`` on the tokenless first run only), each page
44
+ appends durably before its ``toVersion`` commits, and the fetched
45
+ records land in ``date=YYYY-MM-DD`` partitions as new numbered part
46
+ files.
47
+
48
+ Args:
49
+ config: The validated GeoTab configuration; supplies the
50
+ authentication host the pre-auth spec URLs are built on.
51
+
52
+ Returns:
53
+ The frozen status_data ``EndpointDefinition``. Construction
54
+ validates the ``FeedMode`` / ``APPEND_LOG`` /
55
+ ``event_time_column`` triple against the response model.
56
+ """
57
+ return EndpointDefinition(
58
+ provider=Provider.GEOTAB,
59
+ name='status_data',
60
+ spec_builder=GeotabGetFeedSpecBuilder(
61
+ server=server_host(config),
62
+ type_name=_STATUS_DATA_TYPE_NAME,
63
+ results_limit=_RESULTS_LIMIT,
64
+ ),
65
+ page_decoder=GeotabFeedPageDecoder(),
66
+ response_model=StatusData,
67
+ quota_scope=QuotaScope.GEOTAB_FEED,
68
+ storage_kind=StorageKind.APPEND_LOG,
69
+ sync_mode=FeedMode(),
70
+ event_time_column='date_time',
71
+ )
@@ -0,0 +1,77 @@
1
+ # src/fleetpull/endpoints/geotab/text_messages.py
2
+ """The GeoTab text_messages binding: the dispatch-message feed.
3
+
4
+ A ``GetFeed`` drive of the ``TextMessage`` entity — office↔vehicle
5
+ dispatch messages. TextMessage carries NO per-record ``version`` (the
6
+ FaultData/LogRecord asymmetry, DESIGN §8): the append-only cell is
7
+ trivially complete and the consumer reconciles by ``id`` alone
8
+ (DESIGN §4). Delivered/read receipts re-emit a message under newer FEED
9
+ ``toVersion`` tokens and are stored-as-emitted — the feed's own
10
+ versioning, not a per-record ``version`` key.
11
+
12
+ TextMessage also carries NO ``dateTime`` key: the event-time identity is
13
+ ``sent`` (the send instant, 25,000/25,000), so this leaf anchors
14
+ ``event_time_column='sent'`` rather than the ``date_time`` its feed
15
+ siblings use (``models/geotab/text_message.py``).
16
+
17
+ ``resultsLimit`` is the 50,000 protocol maximum: the docs list no lower
18
+ per-type cap for this type (the SCALE census cannot probe a cap the
19
+ population never reaches — the FillUp dual-provenance lesson's other
20
+ arm, DESIGN §8), so the protocol maximum stands.
21
+
22
+ Every request here is a JSON-RPC POST whose ``params.credentials`` and
23
+ resolved host are the session strategy's injections (the devices-leaf
24
+ convention); the host this module writes is a pre-auth placeholder.
25
+ """
26
+
27
+ from typing import Final
28
+
29
+ from fleetpull.config import GeotabConfig
30
+ from fleetpull.endpoints.geotab._requests import GeotabGetFeedSpecBuilder, server_host
31
+ from fleetpull.endpoints.shared import EndpointDefinition, FeedMode, StorageKind
32
+ from fleetpull.models.geotab import TextMessage
33
+ from fleetpull.network.decoders import GeotabFeedPageDecoder
34
+ from fleetpull.vocabulary import Provider, QuotaScope
35
+
36
+ __all__: list[str] = ['build_endpoint']
37
+
38
+ # The GetFeed protocol maximum; no lower documented per-type cap.
39
+ _RESULTS_LIMIT: Final[int] = 50000
40
+
41
+ _TEXT_MESSAGE_TYPE_NAME: Final[str] = 'TextMessage'
42
+
43
+
44
+ def build_endpoint(config: GeotabConfig) -> EndpointDefinition[TextMessage]:
45
+ """Build the GeoTab text_messages feed binding.
46
+
47
+ Dispatch messages fetched incrementally as a version-token feed: the
48
+ run resumes from the stored token (seeded via ``search.fromDate`` on
49
+ the tokenless first run only), each page appends durably before its
50
+ ``toVersion`` commits, and the fetched records land in
51
+ ``date=YYYY-MM-DD`` partitions (by ``sent``, the event time) as new
52
+ numbered part files.
53
+
54
+ Args:
55
+ config: The validated GeoTab configuration; supplies the
56
+ authentication host the pre-auth spec URLs are built on.
57
+
58
+ Returns:
59
+ The frozen text_messages ``EndpointDefinition``. Construction
60
+ validates the ``FeedMode`` / ``APPEND_LOG`` /
61
+ ``event_time_column`` triple against the response model.
62
+ """
63
+ return EndpointDefinition(
64
+ provider=Provider.GEOTAB,
65
+ name='text_messages',
66
+ spec_builder=GeotabGetFeedSpecBuilder(
67
+ server=server_host(config),
68
+ type_name=_TEXT_MESSAGE_TYPE_NAME,
69
+ results_limit=_RESULTS_LIMIT,
70
+ ),
71
+ page_decoder=GeotabFeedPageDecoder(),
72
+ response_model=TextMessage,
73
+ quota_scope=QuotaScope.GEOTAB_FEED,
74
+ storage_kind=StorageKind.APPEND_LOG,
75
+ sync_mode=FeedMode(),
76
+ event_time_column='sent',
77
+ )
@@ -0,0 +1,102 @@
1
+ # src/fleetpull/endpoints/geotab/trips.py
2
+ """The GeoTab trips binding: the first windowed (watermark) GeoTab endpoint.
3
+
4
+ A date-windowed, seek-paged pull of the ``Trip`` entity: the run resumes
5
+ from a ``DateWindow`` (watermark with the provider's late-arrival
6
+ lookback from config -- for trips, the same margin absorbs GeoTab's Trip
7
+ recalculation), the request shape is the shared
8
+ ``GeotabWindowedGetSpecBuilder`` (``_requests``) with
9
+ ``id_sort=True`` -- the window rides a ``TripSearch``
10
+ (``search.fromDate`` / ``search.toDate``) beside the id-ascending
11
+ ``sort`` of the seek walk -- and the fetched days land in
12
+ ``date=YYYY-MM-DD`` partitions replaced wholesale. The decoder is the
13
+ existing ``GeotabGetPageDecoder`` unchanged: its advance spreads the
14
+ sent params when rewriting ``sort.offset``, so ``search`` survives
15
+ every page (live-verified 2026-07-13 -- a windowed, sorted, seeked page
16
+ pair returned strictly-ascending ids across the boundary with every
17
+ record inside the window).
18
+
19
+ ``TripSearch`` matches trips by their STOP time (captured 2026-07-06
20
+ via a discriminating window pair; prediction-confirmed 2026-07-15,
21
+ DESIGN §8): a trip whose start precedes ``fromDate`` returns when its
22
+ stop falls inside, and a trip stopping past ``toDate`` never returns.
23
+ The event-time column is therefore ``stop`` — retrieval (stop in
24
+ window) and routing (the runner's per-batch window filter over the
25
+ event-time column) coincide, so every completed trip belongs to exactly
26
+ one chunk: its stop's day. The watermark advances on stop, which
27
+ matches record-materialization order — a Trip exists only once it has
28
+ stopped. Anchoring on ``start`` instead would drop every
29
+ chunk-boundary-crossing trip: the chunk owning its start never receives
30
+ it, and the chunk that fetches it filters it out.
31
+
32
+ Every request here is a JSON-RPC POST whose ``params.credentials`` and
33
+ resolved host are the session strategy's injections (the devices-leaf
34
+ convention); the host this module writes is a pre-auth placeholder.
35
+ """
36
+
37
+ from datetime import timedelta
38
+ from typing import Final
39
+
40
+ from fleetpull.config import GeotabConfig
41
+ from fleetpull.endpoints.geotab._requests import (
42
+ GeotabWindowedGetSpecBuilder,
43
+ server_host,
44
+ )
45
+ from fleetpull.endpoints.shared import (
46
+ EndpointDefinition,
47
+ StorageKind,
48
+ WatermarkMode,
49
+ )
50
+ from fleetpull.models.geotab import Trip
51
+ from fleetpull.network.decoders import GeotabGetPageDecoder
52
+ from fleetpull.vocabulary import Provider, QuotaScope
53
+
54
+ __all__: list[str] = ['build_endpoint']
55
+
56
+ # The largest sound page under Get's silent 5,000-record cap.
57
+ _RESULTS_LIMIT: Final[int] = 5000
58
+
59
+ _TRIP_TYPE_NAME: Final[str] = 'Trip'
60
+
61
+
62
+ def build_endpoint(config: GeotabConfig) -> EndpointDefinition[Trip]:
63
+ """Build the GeoTab trips watermark binding.
64
+
65
+ Movement-interval history fetched incrementally: the run resumes
66
+ from a ``DateWindow``, each window is walked in id-ascending seek
67
+ pages under the silent 5,000-record ``Get`` cap with the window
68
+ filter riding ``search``, and the fetched days are written to
69
+ ``date=YYYY-MM-DD`` partitions replaced wholesale. No
70
+ ``completeness_check``: the guard is snapshot-only by construction
71
+ -- a ``GetCountOf`` compares only against a complete listing, and a
72
+ date window is not one.
73
+
74
+ Args:
75
+ config: The validated GeoTab configuration; supplies the
76
+ authentication host the pre-auth spec URLs are built on and
77
+ the lookback and cutoff the watermark mode carries.
78
+
79
+ Returns:
80
+ The frozen trips ``EndpointDefinition``. Construction validates
81
+ the ``WatermarkMode`` / ``DATE_PARTITIONED`` /
82
+ ``event_time_column`` triple against the response model.
83
+ """
84
+ return EndpointDefinition(
85
+ provider=Provider.GEOTAB,
86
+ name='trips',
87
+ spec_builder=GeotabWindowedGetSpecBuilder(
88
+ server=server_host(config),
89
+ type_name=_TRIP_TYPE_NAME,
90
+ results_limit=_RESULTS_LIMIT,
91
+ id_sort=True,
92
+ ),
93
+ page_decoder=GeotabGetPageDecoder(),
94
+ response_model=Trip,
95
+ quota_scope=QuotaScope.GEOTAB_GET,
96
+ storage_kind=StorageKind.DATE_PARTITIONED,
97
+ sync_mode=WatermarkMode(
98
+ lookback=timedelta(days=config.lookback_days),
99
+ cutoff=timedelta(days=config.cutoff_days),
100
+ ),
101
+ event_time_column='stop',
102
+ )
@@ -0,0 +1,81 @@
1
+ # src/fleetpull/endpoints/geotab/users.py
2
+ """The GeoTab users binding: a factory composing the users snapshot
3
+ EndpointDefinition from resolved GeoTab configuration.
4
+
5
+ The devices pattern verbatim, bound to ``User``: id-sort seek paging is
6
+ supported for this type (proven live 2026-07-16 -- first page ascending,
7
+ the offset advance continuing past the boundary with no overlap; never
8
+ assumed from Device, since ExceptionEvent rejects id-sort outright), so
9
+ the leaf composes the shared ``_requests`` machinery with a
10
+ ``GetCountOfCheck`` on ``User``. The captured population (157 accounts,
11
+ count == sweep) sits far under the 5,000 silent cap, but the walk is
12
+ cap-agnostic by construction: the seek advance terminates on the empty
13
+ page whatever the per-type cap turns out to be, and the completeness
14
+ check proves the harvest whole either way.
15
+
16
+ Every request here is a JSON-RPC POST whose ``params.credentials`` and
17
+ resolved host are the session strategy's injections (the devices-leaf
18
+ convention); the host this module writes is a pre-auth placeholder.
19
+ """
20
+
21
+ from typing import Final
22
+
23
+ from fleetpull.config import GeotabConfig
24
+ from fleetpull.endpoints.geotab._requests import (
25
+ GeotabGetSpecBuilder,
26
+ GetCountOfCheck,
27
+ server_host,
28
+ )
29
+ from fleetpull.endpoints.shared import (
30
+ EndpointDefinition,
31
+ SnapshotMode,
32
+ StorageKind,
33
+ )
34
+ from fleetpull.models.geotab import User
35
+ from fleetpull.network.decoders import GeotabGetPageDecoder
36
+ from fleetpull.vocabulary import Provider, QuotaScope
37
+
38
+ __all__: list[str] = ['build_endpoint']
39
+
40
+ # The seek-walk page size. The 5,000 silent Get cap is per-type
41
+ # provenance and was never grazed for User (157 captured accounts);
42
+ # the walk is correct under any per-type cap (see module docstring),
43
+ # so 5000 is a page-size choice here, not a proven ceiling. A strong
44
+ # candidate for a user config knob.
45
+ _RESULTS_LIMIT: Final[int] = 5000
46
+
47
+ _USER_TYPE_NAME: Final[str] = 'User'
48
+
49
+
50
+ def build_endpoint(config: GeotabConfig) -> EndpointDefinition[User]:
51
+ """Build the GeoTab users snapshot binding.
52
+
53
+ A full-listing snapshot of the account's User entities (drivers and
54
+ console accounts alike): no resume, a single parquet file, full
55
+ replacement each run. Records arrive as a plain list under
56
+ ``result``, walked by id-ascending seek pages, and every harvest is
57
+ verified against ``GetCountOf`` before anything flows downstream.
58
+
59
+ Args:
60
+ config: The validated GeoTab configuration; supplies the
61
+ authentication host the pre-auth spec URLs are built on.
62
+
63
+ Returns:
64
+ The frozen users ``EndpointDefinition``.
65
+ """
66
+ server = server_host(config)
67
+ return EndpointDefinition(
68
+ provider=Provider.GEOTAB,
69
+ name='users',
70
+ spec_builder=GeotabGetSpecBuilder(
71
+ server=server,
72
+ type_name=_USER_TYPE_NAME,
73
+ results_limit=_RESULTS_LIMIT,
74
+ ),
75
+ page_decoder=GeotabGetPageDecoder(),
76
+ response_model=User,
77
+ quota_scope=QuotaScope.GEOTAB_GET,
78
+ storage_kind=StorageKind.SINGLE,
79
+ sync_mode=SnapshotMode(),
80
+ completeness_check=GetCountOfCheck(server=server, type_name=_USER_TYPE_NAME),
81
+ )
@@ -0,0 +1,12 @@
1
+ # src/fleetpull/endpoints/motive/__init__.py
2
+ """The Motive endpoints package.
3
+
4
+ A provider package under the endpoints layer. It exposes no factory gather:
5
+ endpoint leaves are discovered by ``build_endpoint_registry`` walking this
6
+ package for modules exposing ``build_endpoint``, so a new Motive endpoint is
7
+ a new leaf module here with nothing to register. Import a specific factory
8
+ from its leaf module (``fleetpull.endpoints.motive.vehicles``) when one is
9
+ needed directly.
10
+ """
11
+
12
+ __all__: list[str] = []
@@ -0,0 +1,94 @@
1
+ # src/fleetpull/endpoints/motive/_spec_builders.py
2
+ """The shared Motive fleet-wide date-range spec-builder.
3
+
4
+ The builder for Motive endpoints that take day-granular ``start_date`` /
5
+ ``end_date`` query parameters with no per-vehicle fan-out
6
+ (driving_periods, idle_events, and the utilization rollup pair
7
+ vehicle_utilizations / driver_idle_rollups) — the promotion the
8
+ vehicle_locations module anticipated once a real second user arrived.
9
+ vehicle_locations keeps its own builder because it renders a
10
+ per-vehicle path; this one serves the fleet-wide leaves and any future
11
+ sibling. The rendered label pair is INCLUSIVE on both ends; how the
12
+ provider interprets it (UTC vs company-local days) is a per-endpoint
13
+ fact each leaf documents.
14
+
15
+ ``window_pad_days`` exists for endpoints whose server-side window
16
+ matching is not UTC: idle_events interprets its date range on
17
+ company-local day boundaries and matches by overlap (DESIGN §8, captured
18
+ 2026-07-15), so its leaf pads the wire window one day on each side and
19
+ the true UTC window — the post-fetch window filter and the writer's
20
+ partition tripwire — does the trimming. A pad only ever widens what is
21
+ fetched, never what is written.
22
+ """
23
+
24
+ from collections.abc import Mapping
25
+ from dataclasses import dataclass
26
+ from datetime import timedelta
27
+
28
+ from fleetpull.endpoints.shared import ResumeValue, require_date_window
29
+ from fleetpull.network.contract import HttpMethod, RequestSpec
30
+ from fleetpull.timing import to_utc_date_string
31
+
32
+ __all__: list[str] = ['MotiveFleetDateRangeSpecBuilder']
33
+
34
+
35
+ @dataclass(frozen=True, slots=True)
36
+ class MotiveFleetDateRangeSpecBuilder:
37
+ """Build the fleet-wide, date-windowed first request.
38
+
39
+ The ``SpecBuilder`` for Motive watermark endpoints with no fan-out:
40
+ injects the resume window as ``start_date`` / ``end_date``, mapped
41
+ exactly as the vehicle_locations builder maps them — ``start_date``
42
+ is the UTC date of ``window.start`` and ``end_date`` the window's
43
+ ``last_covered_date`` (the derivation lives on ``DateWindow``) —
44
+ then widened by ``window_pad_days`` whole days on each side.
45
+
46
+ Attributes:
47
+ base_url: Root of the Motive API, trailing-slash-normalized by
48
+ the provider config so the leading-slash path joins directly.
49
+ path: The endpoint's leading-slash request path.
50
+ window_pad_days: Whole days added to each side of the wire
51
+ window. Zero for UTC-matched endpoints; idle_events sets one
52
+ to cover its company-local overlap matching under any account
53
+ timezone.
54
+ """
55
+
56
+ base_url: str
57
+ path: str
58
+ window_pad_days: int = 0
59
+
60
+ def build_spec(
61
+ self, resume: ResumeValue, member_values: Mapping[str, str]
62
+ ) -> RequestSpec:
63
+ """Build the fleet-wide, date-windowed GET.
64
+
65
+ Args:
66
+ resume: The run's resume window. Must be a ``DateWindow`` — a
67
+ watermark endpoint always resumes from one; any other
68
+ value is a wiring bug.
69
+ member_values: Accepted to satisfy the protocol; unused — a
70
+ fleet-wide single chain binds no member.
71
+
72
+ Returns:
73
+ A credential-less ``GET`` for ``base_url + path`` carrying
74
+ the padded ``start_date`` / ``end_date``. Auth headers are
75
+ layered on by the client's ``ProviderProfile``; pagination
76
+ parameters are injected by the page decoder's
77
+ ``first_request``.
78
+
79
+ Raises:
80
+ TypeError: ``resume`` is not a ``DateWindow``.
81
+
82
+ Side Effects:
83
+ None.
84
+ """
85
+ resume_window = require_date_window(resume, type(self).__name__)
86
+ pad = timedelta(days=self.window_pad_days)
87
+ start_date = to_utc_date_string(resume_window.start - pad)
88
+ end_date = (resume_window.last_covered_date + pad).isoformat()
89
+ params = {'start_date': start_date, 'end_date': end_date}
90
+ return RequestSpec(
91
+ method=HttpMethod.GET,
92
+ url=f'{self.base_url}{self.path}',
93
+ params=params,
94
+ )