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.
- fleetpull/__init__.py +36 -0
- fleetpull/api/__init__.py +32 -0
- fleetpull/api/auth_ingress.py +216 -0
- fleetpull/api/catalog.py +139 -0
- fleetpull/api/fetch.py +229 -0
- fleetpull/api/identity.py +90 -0
- fleetpull/api/sync.py +735 -0
- fleetpull/cli.py +146 -0
- fleetpull/config/__init__.py +47 -0
- fleetpull/config/base.py +18 -0
- fleetpull/config/example.py +90 -0
- fleetpull/config/geotab.py +78 -0
- fleetpull/config/http.py +32 -0
- fleetpull/config/loading.py +195 -0
- fleetpull/config/logger.py +139 -0
- fleetpull/config/providers.py +520 -0
- fleetpull/config/rate_limit.py +50 -0
- fleetpull/config/resolution.py +156 -0
- fleetpull/config/retry.py +69 -0
- fleetpull/config/root.py +157 -0
- fleetpull/config/sections.py +146 -0
- fleetpull/endpoints/__init__.py +24 -0
- fleetpull/endpoints/geotab/__init__.py +12 -0
- fleetpull/endpoints/geotab/_requests.py +427 -0
- fleetpull/endpoints/geotab/annotation_logs.py +73 -0
- fleetpull/endpoints/geotab/audits.py +70 -0
- fleetpull/endpoints/geotab/devices.py +79 -0
- fleetpull/endpoints/geotab/driver_changes.py +73 -0
- fleetpull/endpoints/geotab/duty_status_logs.py +75 -0
- fleetpull/endpoints/geotab/dvir_logs.py +76 -0
- fleetpull/endpoints/geotab/exception_events.py +119 -0
- fleetpull/endpoints/geotab/fault_data.py +72 -0
- fleetpull/endpoints/geotab/fill_ups.py +79 -0
- fleetpull/endpoints/geotab/fuel_and_energy_used.py +74 -0
- fleetpull/endpoints/geotab/fuel_tax_details.py +75 -0
- fleetpull/endpoints/geotab/log_records.py +74 -0
- fleetpull/endpoints/geotab/media_files.py +74 -0
- fleetpull/endpoints/geotab/shipment_logs.py +71 -0
- fleetpull/endpoints/geotab/status_data.py +71 -0
- fleetpull/endpoints/geotab/text_messages.py +77 -0
- fleetpull/endpoints/geotab/trips.py +102 -0
- fleetpull/endpoints/geotab/users.py +81 -0
- fleetpull/endpoints/motive/__init__.py +12 -0
- fleetpull/endpoints/motive/_spec_builders.py +94 -0
- fleetpull/endpoints/motive/driver_idle_rollups.py +113 -0
- fleetpull/endpoints/motive/driving_periods.py +85 -0
- fleetpull/endpoints/motive/groups.py +61 -0
- fleetpull/endpoints/motive/idle_events.py +94 -0
- fleetpull/endpoints/motive/users.py +64 -0
- fleetpull/endpoints/motive/vehicle_locations.py +170 -0
- fleetpull/endpoints/motive/vehicle_utilizations.py +122 -0
- fleetpull/endpoints/motive/vehicles.py +106 -0
- fleetpull/endpoints/registry.py +280 -0
- fleetpull/endpoints/samsara/__init__.py +12 -0
- fleetpull/endpoints/samsara/_spec_builders.py +202 -0
- fleetpull/endpoints/samsara/addresses.py +77 -0
- fleetpull/endpoints/samsara/asset_locations.py +217 -0
- fleetpull/endpoints/samsara/driver_fuel_energy_reports.py +124 -0
- fleetpull/endpoints/samsara/driver_vehicle_assignments.py +211 -0
- fleetpull/endpoints/samsara/drivers.py +169 -0
- fleetpull/endpoints/samsara/engine_states.py +114 -0
- fleetpull/endpoints/samsara/gps_readings.py +113 -0
- fleetpull/endpoints/samsara/idling_events.py +195 -0
- fleetpull/endpoints/samsara/odometer_readings.py +116 -0
- fleetpull/endpoints/samsara/trips.py +217 -0
- fleetpull/endpoints/samsara/vehicle_fuel_energy_reports.py +139 -0
- fleetpull/endpoints/samsara/vehicles.py +121 -0
- fleetpull/endpoints/shared/__init__.py +57 -0
- fleetpull/endpoints/shared/base.py +529 -0
- fleetpull/endpoints/shared/request_shape.py +227 -0
- fleetpull/endpoints/shared/resume.py +74 -0
- fleetpull/endpoints/shared/spec_builders.py +59 -0
- fleetpull/endpoints/shared/sync_mode.py +159 -0
- fleetpull/endpoints/shared/url_paths.py +149 -0
- fleetpull/exceptions.py +320 -0
- fleetpull/incremental/__init__.py +27 -0
- fleetpull/incremental/cursor.py +66 -0
- fleetpull/incremental/resolution.py +152 -0
- fleetpull/incremental/seed.py +53 -0
- fleetpull/incremental/window.py +94 -0
- fleetpull/logger/__init__.py +5 -0
- fleetpull/logger/setup.py +145 -0
- fleetpull/model_contract/__init__.py +6 -0
- fleetpull/model_contract/coercions.py +33 -0
- fleetpull/model_contract/response.py +45 -0
- fleetpull/models/__init__.py +7 -0
- fleetpull/models/geotab/__init__.py +166 -0
- fleetpull/models/geotab/annotation_log.py +109 -0
- fleetpull/models/geotab/audit.py +56 -0
- fleetpull/models/geotab/device.py +217 -0
- fleetpull/models/geotab/driver_change.py +102 -0
- fleetpull/models/geotab/duty_status_log.py +200 -0
- fleetpull/models/geotab/dvir_log.py +184 -0
- fleetpull/models/geotab/exception_event.py +135 -0
- fleetpull/models/geotab/fault_data.py +168 -0
- fleetpull/models/geotab/fill_up.py +189 -0
- fleetpull/models/geotab/fuel_and_energy_used.py +81 -0
- fleetpull/models/geotab/fuel_tax_detail.py +150 -0
- fleetpull/models/geotab/log_record.py +68 -0
- fleetpull/models/geotab/media_file.py +133 -0
- fleetpull/models/geotab/shared.py +221 -0
- fleetpull/models/geotab/shipment_log.py +105 -0
- fleetpull/models/geotab/status_data.py +108 -0
- fleetpull/models/geotab/text_message.py +125 -0
- fleetpull/models/geotab/trip.py +162 -0
- fleetpull/models/geotab/user.py +187 -0
- fleetpull/models/motive/__init__.py +43 -0
- fleetpull/models/motive/driver_idle_rollup.py +109 -0
- fleetpull/models/motive/driving_period.py +82 -0
- fleetpull/models/motive/group.py +49 -0
- fleetpull/models/motive/idle_event.py +77 -0
- fleetpull/models/motive/shared.py +192 -0
- fleetpull/models/motive/user.py +217 -0
- fleetpull/models/motive/vehicle.py +162 -0
- fleetpull/models/motive/vehicle_location.py +127 -0
- fleetpull/models/motive/vehicle_utilization.py +128 -0
- fleetpull/models/samsara/__init__.py +108 -0
- fleetpull/models/samsara/address.py +149 -0
- fleetpull/models/samsara/asset_location.py +131 -0
- fleetpull/models/samsara/driver.py +232 -0
- fleetpull/models/samsara/driver_fuel_energy_report.py +153 -0
- fleetpull/models/samsara/driver_vehicle_assignment.py +168 -0
- fleetpull/models/samsara/engine_state.py +92 -0
- fleetpull/models/samsara/gps_reading.py +146 -0
- fleetpull/models/samsara/idling_event.py +174 -0
- fleetpull/models/samsara/odometer_reading.py +90 -0
- fleetpull/models/samsara/trip.py +199 -0
- fleetpull/models/samsara/vehicle.py +167 -0
- fleetpull/models/samsara/vehicle_fuel_energy_report.py +191 -0
- fleetpull/network/__init__.py +9 -0
- fleetpull/network/auth/__init__.py +15 -0
- fleetpull/network/auth/authenticate.py +271 -0
- fleetpull/network/auth/manager.py +223 -0
- fleetpull/network/auth/models.py +57 -0
- fleetpull/network/auth/strategies.py +169 -0
- fleetpull/network/classifiers/__init__.py +11 -0
- fleetpull/network/classifiers/geotab.py +145 -0
- fleetpull/network/classifiers/motive.py +79 -0
- fleetpull/network/classifiers/samsara.py +80 -0
- fleetpull/network/client/__init__.py +25 -0
- fleetpull/network/client/page.py +32 -0
- fleetpull/network/client/profile.py +27 -0
- fleetpull/network/client/registry.py +103 -0
- fleetpull/network/client/registry_base.py +150 -0
- fleetpull/network/client/runtime.py +44 -0
- fleetpull/network/client/transport.py +381 -0
- fleetpull/network/contract/__init__.py +50 -0
- fleetpull/network/contract/auth.py +48 -0
- fleetpull/network/contract/classifier.py +189 -0
- fleetpull/network/contract/envelope_fetcher.py +33 -0
- fleetpull/network/contract/envelopes.py +189 -0
- fleetpull/network/contract/outcome.py +43 -0
- fleetpull/network/contract/page_decoder.py +101 -0
- fleetpull/network/contract/request.py +97 -0
- fleetpull/network/decoders/__init__.py +40 -0
- fleetpull/network/decoders/_window_stamp.py +78 -0
- fleetpull/network/decoders/geotab.py +309 -0
- fleetpull/network/decoders/motive.py +204 -0
- fleetpull/network/decoders/motive_reports.py +114 -0
- fleetpull/network/decoders/samsara.py +343 -0
- fleetpull/network/decoders/samsara_reports.py +119 -0
- fleetpull/network/decoders/single_page.py +55 -0
- fleetpull/network/limits/__init__.py +13 -0
- fleetpull/network/limits/bucket_math.py +58 -0
- fleetpull/network/limits/limiter.py +153 -0
- fleetpull/network/limits/registry.py +96 -0
- fleetpull/network/posture/__init__.py +6 -0
- fleetpull/network/posture/client_options.py +96 -0
- fleetpull/network/retry/__init__.py +13 -0
- fleetpull/network/retry/decision.py +155 -0
- fleetpull/network/tls/__init__.py +5 -0
- fleetpull/network/tls/truststore_context.py +35 -0
- fleetpull/orchestrator/__init__.py +30 -0
- fleetpull/orchestrator/backfill.py +127 -0
- fleetpull/orchestrator/batch.py +155 -0
- fleetpull/orchestrator/bisection.py +271 -0
- fleetpull/orchestrator/drivers.py +350 -0
- fleetpull/orchestrator/entry.py +347 -0
- fleetpull/orchestrator/executors.py +128 -0
- fleetpull/orchestrator/fanout.py +163 -0
- fleetpull/orchestrator/feed_drive.py +266 -0
- fleetpull/orchestrator/metadata_projection.py +195 -0
- fleetpull/orchestrator/outcome.py +53 -0
- fleetpull/orchestrator/recording.py +85 -0
- fleetpull/orchestrator/resume.py +143 -0
- fleetpull/orchestrator/roster_harvest.py +108 -0
- fleetpull/orchestrator/roster_refresh.py +363 -0
- fleetpull/orchestrator/runner.py +262 -0
- fleetpull/orchestrator/shape_resolution.py +221 -0
- fleetpull/orchestrator/spine.py +166 -0
- fleetpull/orchestrator/streaming.py +153 -0
- fleetpull/orchestrator/unit_loop.py +273 -0
- fleetpull/orchestrator/watermark_drive.py +455 -0
- fleetpull/paths/__init__.py +13 -0
- fleetpull/paths/datasets.py +38 -0
- fleetpull/paths/partitions.py +41 -0
- fleetpull/paths/resolution.py +98 -0
- fleetpull/polars_typing/__init__.py +20 -0
- fleetpull/py.typed +0 -0
- fleetpull/records/__init__.py +19 -0
- fleetpull/records/convert.py +53 -0
- fleetpull/records/dataframe.py +63 -0
- fleetpull/records/event_time.py +70 -0
- fleetpull/records/fields.py +190 -0
- fleetpull/records/flatten.py +54 -0
- fleetpull/records/roster_members.py +74 -0
- fleetpull/records/schema.py +73 -0
- fleetpull/records/validation.py +62 -0
- fleetpull/resources/__init__.py +10 -0
- fleetpull/resources/config.example.yaml +222 -0
- fleetpull/roster/__init__.py +16 -0
- fleetpull/roster/definition.py +46 -0
- fleetpull/roster/key.py +40 -0
- fleetpull/roster/registry.py +93 -0
- fleetpull/state/__init__.py +36 -0
- fleetpull/state/cursors.py +417 -0
- fleetpull/state/database.py +463 -0
- fleetpull/state/migrations.py +430 -0
- fleetpull/state/reconcile.py +127 -0
- fleetpull/state/rosters.py +159 -0
- fleetpull/state/run_ledger.py +572 -0
- fleetpull/state/work_units.py +648 -0
- fleetpull/storage/__init__.py +30 -0
- fleetpull/storage/append.py +187 -0
- fleetpull/storage/atomic.py +161 -0
- fleetpull/storage/files.py +176 -0
- fleetpull/storage/frames.py +98 -0
- fleetpull/storage/metadata.py +161 -0
- fleetpull/storage/partitioned.py +205 -0
- fleetpull/storage/pruning.py +169 -0
- fleetpull/storage/read.py +35 -0
- fleetpull/storage/result.py +34 -0
- fleetpull/storage/single_file.py +130 -0
- fleetpull/storage/splitting.py +88 -0
- fleetpull/storage/staging.py +178 -0
- fleetpull/storage/writers.py +145 -0
- fleetpull/timing/__init__.py +21 -0
- fleetpull/timing/canon.py +92 -0
- fleetpull/timing/clock.py +192 -0
- fleetpull/timing/codec.py +122 -0
- fleetpull/timing/sleeper.py +65 -0
- fleetpull/vocabulary/__init__.py +16 -0
- fleetpull/vocabulary/json_types.py +20 -0
- fleetpull/vocabulary/provider.py +29 -0
- fleetpull/vocabulary/quota_scope.py +47 -0
- fleetpull/vocabulary/response_category.py +32 -0
- fleetpull-0.1.0.dist-info/METADATA +248 -0
- fleetpull-0.1.0.dist-info/RECORD +252 -0
- fleetpull-0.1.0.dist-info/WHEEL +5 -0
- fleetpull-0.1.0.dist-info/entry_points.txt +2 -0
- fleetpull-0.1.0.dist-info/licenses/LICENSE +202 -0
- fleetpull-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Motive response models; the face re-exports each endpoint module's models."""
|
|
2
|
+
|
|
3
|
+
from fleetpull.models.motive.driver_idle_rollup import DriverIdleRollup
|
|
4
|
+
from fleetpull.models.motive.driving_period import DrivingPeriod
|
|
5
|
+
from fleetpull.models.motive.group import Group
|
|
6
|
+
from fleetpull.models.motive.idle_event import IdleEvent
|
|
7
|
+
from fleetpull.models.motive.shared import (
|
|
8
|
+
EldDeviceInfo,
|
|
9
|
+
MotiveWindowStamp,
|
|
10
|
+
UserSummary,
|
|
11
|
+
VehicleSummary,
|
|
12
|
+
)
|
|
13
|
+
from fleetpull.models.motive.user import User
|
|
14
|
+
from fleetpull.models.motive.vehicle import (
|
|
15
|
+
AvailabilityDetails,
|
|
16
|
+
AvailabilityStatus,
|
|
17
|
+
Vehicle,
|
|
18
|
+
VehicleStatus,
|
|
19
|
+
)
|
|
20
|
+
from fleetpull.models.motive.vehicle_location import (
|
|
21
|
+
VehicleLocation,
|
|
22
|
+
VehicleLocationType,
|
|
23
|
+
)
|
|
24
|
+
from fleetpull.models.motive.vehicle_utilization import VehicleUtilization
|
|
25
|
+
|
|
26
|
+
__all__: list[str] = [
|
|
27
|
+
'AvailabilityDetails',
|
|
28
|
+
'AvailabilityStatus',
|
|
29
|
+
'DriverIdleRollup',
|
|
30
|
+
'DrivingPeriod',
|
|
31
|
+
'EldDeviceInfo',
|
|
32
|
+
'Group',
|
|
33
|
+
'IdleEvent',
|
|
34
|
+
'MotiveWindowStamp',
|
|
35
|
+
'User',
|
|
36
|
+
'UserSummary',
|
|
37
|
+
'Vehicle',
|
|
38
|
+
'VehicleLocation',
|
|
39
|
+
'VehicleLocationType',
|
|
40
|
+
'VehicleStatus',
|
|
41
|
+
'VehicleSummary',
|
|
42
|
+
'VehicleUtilization',
|
|
43
|
+
]
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# src/fleetpull/models/motive/driver_idle_rollup.py
|
|
2
|
+
"""Motive DriverIdleRollup response model
|
|
3
|
+
(``GET /v2/driver_utilization``, post-decoder window-stamped grain).
|
|
4
|
+
|
|
5
|
+
Written from captured live responses (2026-07-21 probe session: 100
|
|
6
|
+
records sampled, structurally uniform), never from docs. NOTE THE
|
|
7
|
+
NAMING: the wire's OWN envelope vocabulary is
|
|
8
|
+
``driver_idle_rollups``/``driver_idle_rollup`` -- different from its
|
|
9
|
+
``/v2/driver_utilization`` path -- and the model mirrors the wire's
|
|
10
|
+
vocabulary (the endpoint records the legacy-name mapping in
|
|
11
|
+
``ENDPOINTS.md``).
|
|
12
|
+
|
|
13
|
+
The model mirrors the record ``MotiveWindowReportPageDecoder`` emits:
|
|
14
|
+
``window_start`` / ``window_end`` are DECODER-SYNTHESIZED
|
|
15
|
+
(``windowStartDate``/``windowEndDate``) -- rollup rows carry NO date or
|
|
16
|
+
time identity of any kind, so the decoder stamps each row with the
|
|
17
|
+
window the SENT spec asked for, copied verbatim from its
|
|
18
|
+
``start_date``/``end_date`` date labels, and ``MotiveWindowStamp``
|
|
19
|
+
lifts each label to its UTC-midnight instant (``shared.py``);
|
|
20
|
+
everything else is wire-verbatim.
|
|
21
|
+
|
|
22
|
+
**THE COMPANY-LOCAL CAVEAT (the documented obligation on this
|
|
23
|
+
mirror).** The date labels are interpreted in COMPANY-LOCAL days -- the
|
|
24
|
+
account's ``/v1/companies`` capture carries a company-local zone at a
|
|
25
|
+
UTC-5 offset (DESIGN section 8) -- so each row is the provider's rollup
|
|
26
|
+
over its company-local day(s), mirrored verbatim. The window stamps
|
|
27
|
+
carry the day LABELS, not UTC day boundaries; nothing here converts
|
|
28
|
+
anything.
|
|
29
|
+
|
|
30
|
+
**THE ROLLUP GRAIN IS THE REQUEST WINDOW -- proven on this surface;
|
|
31
|
+
do not sum day rows (precedent-based).** A quiet single day returned 13
|
|
32
|
+
rows and a six-day window 653 -- one rollup row per driver with
|
|
33
|
+
activity in the window, per window (contrast the vehicle arm's
|
|
34
|
+
whole-fleet-every-window population). The grain forces the binding's
|
|
35
|
+
``fixed_unit_days=1``. Additivity was NOT probed here; on the provider
|
|
36
|
+
family's only probed rollup surfaces (the Samsara fuel-energy pair) day
|
|
37
|
+
rollups were NON-ADDITIVE into wider windows (89/267 mismatched), so
|
|
38
|
+
the same posture applies as precedent: day rows MUST NOT be summed to
|
|
39
|
+
reproduce a wider window's rollup.
|
|
40
|
+
|
|
41
|
+
``driver`` is the shared 8-key ``UserSummary`` (its fourth carrying
|
|
42
|
+
surface) and NULLABLE: the census found it populated on 99 of 100
|
|
43
|
+
sampled rows and NULL on one -- an UNATTRIBUTED rollup bucket the
|
|
44
|
+
provider emits alongside the per-driver rows, mirrored as a null ref,
|
|
45
|
+
never dropped. The metric core is REQUIRED (every key on every sampled
|
|
46
|
+
record; the rollup-surface reasoning -- metrics are computed per
|
|
47
|
+
window, with no absence mechanism to be conservative about), and the
|
|
48
|
+
window stamps are required STRUCTURALLY. ``idle_time`` and
|
|
49
|
+
``driving_time`` are bare INTS on this arm -- floats on the vehicle
|
|
50
|
+
arm -- and each arm mirrors its own wire.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
from pydantic import Field
|
|
54
|
+
|
|
55
|
+
from fleetpull.model_contract import ResponseModel
|
|
56
|
+
from fleetpull.models.motive.shared import MotiveWindowStamp, UserSummary
|
|
57
|
+
|
|
58
|
+
__all__: list[str] = ['DriverIdleRollup']
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class DriverIdleRollup(ResponseModel):
|
|
62
|
+
"""One driver's idle rollup over exactly one request window.
|
|
63
|
+
|
|
64
|
+
A pure mirror of the window-stamped post-decoder record (module
|
|
65
|
+
docstring: the window stamps are decoder-synthesized from the sent
|
|
66
|
+
spec's date labels; everything else is wire-verbatim). Field
|
|
67
|
+
semantics and units are Motive's; no value is derived or
|
|
68
|
+
interpreted here. Day rows MUST NOT be summed to reproduce a wider
|
|
69
|
+
window's rollup (module docstring -- the provider-family
|
|
70
|
+
precedent).
|
|
71
|
+
|
|
72
|
+
Attributes:
|
|
73
|
+
window_start: The request window's start-date label
|
|
74
|
+
(decoder-synthesized ``windowStartDate``, verbatim from the
|
|
75
|
+
sent ``start_date``) at UTC midnight -- the event-time
|
|
76
|
+
column: the row's time identity is the window that produced
|
|
77
|
+
it. A COMPANY-LOCAL day label (module docstring).
|
|
78
|
+
window_end: The request window's end-date label
|
|
79
|
+
(decoder-synthesized ``windowEndDate``, verbatim from the
|
|
80
|
+
sent ``end_date``) at UTC midnight. INCLUSIVE: at the fixed
|
|
81
|
+
1-day unit it equals ``window_start``.
|
|
82
|
+
driver: The rollup's driver entity (the shared ``UserSummary``);
|
|
83
|
+
NULL on the unattributed rollup bucket row (module
|
|
84
|
+
docstring), populated with the full 8-key shape otherwise.
|
|
85
|
+
utilization: The provider's utilization figure for the window,
|
|
86
|
+
mirrored uninterpreted.
|
|
87
|
+
driving_time: Time spent driving in the window, a bare int
|
|
88
|
+
(contrast the vehicle arm's float durations -- each arm
|
|
89
|
+
mirrors its own wire).
|
|
90
|
+
idle_time: Time spent idling in the window, a bare int.
|
|
91
|
+
driving_fuel: Fuel used while driving in the window, provider
|
|
92
|
+
units mirrored verbatim.
|
|
93
|
+
idle_fuel: Fuel used while idling in the window.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
# Decoder-synthesized window identity (the sent spec's own labels).
|
|
97
|
+
window_start: MotiveWindowStamp = Field(alias='windowStartDate')
|
|
98
|
+
window_end: MotiveWindowStamp = Field(alias='windowEndDate')
|
|
99
|
+
|
|
100
|
+
# The rollup's entity; null on the unattributed bucket row.
|
|
101
|
+
driver: UserSummary | None = None
|
|
102
|
+
|
|
103
|
+
# The wire-verbatim metric core (required -- rollup surfaces have no
|
|
104
|
+
# absence arm; int durations on THIS arm, provider units verbatim).
|
|
105
|
+
utilization: float
|
|
106
|
+
driving_time: int
|
|
107
|
+
idle_time: int
|
|
108
|
+
driving_fuel: float
|
|
109
|
+
idle_fuel: float
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# src/fleetpull/models/motive/driving_period.py
|
|
2
|
+
"""The Motive driving-period response model (captured 2026-07-15).
|
|
3
|
+
|
|
4
|
+
One record per contiguous driving span from ``GET /v1/driving_periods``.
|
|
5
|
+
Completed records reproduce ``duration = end_time - start_time`` exactly
|
|
6
|
+
(float seconds); in-progress records (``status: "in_progress"``) null
|
|
7
|
+
every end-side field — ``end_time``, ``end_kilometers``, ``distance``,
|
|
8
|
+
the destination and its coordinates — while ``duration`` keeps counting
|
|
9
|
+
as a fractional elapsed value. ``distance`` is a provider-formatted
|
|
10
|
+
string (``"42.2 mi"``) mirrored verbatim — the merely-ugly side of the
|
|
11
|
+
coercion boundary; real distance arithmetic uses the kilometer odometer
|
|
12
|
+
fields. ``start_time`` was never observed null and is the endpoint's
|
|
13
|
+
retrieval anchor (start-anchored UTC window matching, DESIGN §8).
|
|
14
|
+
|
|
15
|
+
Excluded fields, per capture discipline: ``source`` and the four
|
|
16
|
+
``*_hvb_*`` battery fields were never observed non-null (a diesel
|
|
17
|
+
fleet's capture), so no honest dtype exists; they join the model when a
|
|
18
|
+
capture pins their types.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from datetime import datetime
|
|
22
|
+
|
|
23
|
+
from pydantic import Field
|
|
24
|
+
|
|
25
|
+
from fleetpull.model_contract import ResponseModel
|
|
26
|
+
from fleetpull.models.motive.shared import UserSummary, VehicleSummary
|
|
27
|
+
|
|
28
|
+
__all__: list[str] = ['DrivingPeriod']
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DrivingPeriod(ResponseModel):
|
|
32
|
+
"""One contiguous driving span for one vehicle.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
period_id: Motive's internal period identifier (wire key ``id``).
|
|
36
|
+
start_time: UTC start of the span; the retrieval anchor.
|
|
37
|
+
end_time: UTC end of the span; null while the span is in progress.
|
|
38
|
+
status: Free-form lifecycle string (``"complete"`` /
|
|
39
|
+
``"in_progress"`` observed), mirrored, never interpreted.
|
|
40
|
+
period_type: Free-form span-type string (wire key ``type``;
|
|
41
|
+
``"driving"`` observed), mirrored, never interpreted.
|
|
42
|
+
annotation_status: Provider annotation state; null when absent.
|
|
43
|
+
notes: Free-form annotation text; null when absent.
|
|
44
|
+
duration: Span length in float seconds; on an in-progress record,
|
|
45
|
+
the elapsed value so far.
|
|
46
|
+
start_kilometers: Odometer at span start, kilometers.
|
|
47
|
+
end_kilometers: Odometer at span end; null while in progress.
|
|
48
|
+
driver: Attributed driver; null when the span is unattributed.
|
|
49
|
+
vehicle: The vehicle the span belongs to.
|
|
50
|
+
origin: Provider-formatted start address, mirrored verbatim
|
|
51
|
+
(empty strings normalize to null at the DataFrame boundary);
|
|
52
|
+
null when absent.
|
|
53
|
+
origin_lat: Start latitude; null when the provider has none.
|
|
54
|
+
origin_lon: Start longitude; null when the provider has none.
|
|
55
|
+
destination_lat: End latitude; null while in progress.
|
|
56
|
+
destination_lon: End longitude; null while in progress.
|
|
57
|
+
destination: Provider-formatted end address, mirrored verbatim —
|
|
58
|
+
the captured in-progress record carries ``""`` here (nulled
|
|
59
|
+
at the DataFrame boundary); null when absent.
|
|
60
|
+
distance: Provider-formatted distance string (``"42.2 mi"``),
|
|
61
|
+
mirrored verbatim; null while in progress.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
period_id: int = Field(alias='id')
|
|
65
|
+
start_time: datetime
|
|
66
|
+
end_time: datetime | None = None
|
|
67
|
+
status: str
|
|
68
|
+
period_type: str = Field(alias='type')
|
|
69
|
+
annotation_status: int | None = None
|
|
70
|
+
notes: str | None = None
|
|
71
|
+
duration: float
|
|
72
|
+
start_kilometers: float
|
|
73
|
+
end_kilometers: float | None = None
|
|
74
|
+
driver: UserSummary | None = None
|
|
75
|
+
vehicle: VehicleSummary
|
|
76
|
+
origin: str | None = None
|
|
77
|
+
origin_lat: float | None = None
|
|
78
|
+
origin_lon: float | None = None
|
|
79
|
+
destination_lat: float | None = None
|
|
80
|
+
destination_lon: float | None = None
|
|
81
|
+
destination: str | None = None
|
|
82
|
+
distance: str | None = None
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# src/fleetpull/models/motive/group.py
|
|
2
|
+
"""The Motive group response model (``GET /v1/groups``, captured 2026-07-21).
|
|
3
|
+
|
|
4
|
+
One record per organizational group. Written from a whole-population walk
|
|
5
|
+
(152 records, four pages at ``per_page`` 50): every modeled key was present
|
|
6
|
+
on all 152 records, so everything here is required -- nullability is the
|
|
7
|
+
only optionality, and it follows the census exactly. ``parent_id`` is null
|
|
8
|
+
on root groups and an existing group id otherwise (the groups form a
|
|
9
|
+
tree).
|
|
10
|
+
|
|
11
|
+
``user`` is the group's owner/creator reference -- the compact
|
|
12
|
+
users-endpoint account shape, carried by the shared ``UserSummary`` (the
|
|
13
|
+
``shared.py`` promotion rule: the identical wire shape already rides the
|
|
14
|
+
driving-period and idle-event driver references). On THIS surface the
|
|
15
|
+
census observed ``username`` and ``driver_company_id`` null on all 152
|
|
16
|
+
records and ``role``/``status``/names populated on all 152;
|
|
17
|
+
``UserSummary``'s union-lax optionality absorbs both facts, and the
|
|
18
|
+
sibling surfaces are where the null-here keys carry values.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from pydantic import Field
|
|
22
|
+
|
|
23
|
+
from fleetpull.model_contract import ResponseModel
|
|
24
|
+
from fleetpull.models.motive.shared import UserSummary
|
|
25
|
+
|
|
26
|
+
__all__: list[str] = ['Group']
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Group(ResponseModel):
|
|
30
|
+
"""One Motive organizational group.
|
|
31
|
+
|
|
32
|
+
A pure mirror of the whole-population census: five keys, all present
|
|
33
|
+
on every record.
|
|
34
|
+
|
|
35
|
+
Attributes:
|
|
36
|
+
group_id: Motive's internal group identifier (wire key ``id``).
|
|
37
|
+
company_id: Parent company identifier.
|
|
38
|
+
name: The group's display name.
|
|
39
|
+
parent_id: The parent group's id; null on root groups (the
|
|
40
|
+
groups form a tree).
|
|
41
|
+
user: The group's owner/creator reference (the shared compact
|
|
42
|
+
users-endpoint account shape).
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
group_id: int = Field(alias='id')
|
|
46
|
+
company_id: int
|
|
47
|
+
name: str
|
|
48
|
+
parent_id: int | None
|
|
49
|
+
user: UserSummary
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# src/fleetpull/models/motive/idle_event.py
|
|
2
|
+
"""The Motive idle-event response model (captured 2026-07-15).
|
|
3
|
+
|
|
4
|
+
One record per engine-idle interval from ``GET /v1/idle_events``. Both
|
|
5
|
+
timestamps were always present in capture — the endpoint has no
|
|
6
|
+
in-progress shape analogue. ``veh_fuel_start`` / ``veh_fuel_end`` are the
|
|
7
|
+
ELD's cumulative fuel counters, mirrored as-is. The ``rg_*`` fields are
|
|
8
|
+
the provider's reverse-geocode metadata; when ``rg_match`` is false,
|
|
9
|
+
``location`` carries a distance-direction prefix (``"2.6 mi NW of …"``)
|
|
10
|
+
instead of a bare place name — both formats mirror verbatim.
|
|
11
|
+
|
|
12
|
+
The endpoint's date window is interpreted on company-local day boundaries
|
|
13
|
+
and matched by overlap (DESIGN §8, captured 2026-07-15) — the reason its
|
|
14
|
+
endpoint leaf pads the wire window; nothing about that reaches this
|
|
15
|
+
model, whose ``start_time`` remains the routing anchor.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
|
|
20
|
+
from pydantic import Field
|
|
21
|
+
|
|
22
|
+
from fleetpull.model_contract import ResponseModel
|
|
23
|
+
from fleetpull.models.motive.shared import (
|
|
24
|
+
EldDeviceInfo,
|
|
25
|
+
UserSummary,
|
|
26
|
+
VehicleSummary,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
__all__: list[str] = ['IdleEvent']
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class IdleEvent(ResponseModel):
|
|
33
|
+
"""One engine-idle interval for one vehicle.
|
|
34
|
+
|
|
35
|
+
Attributes:
|
|
36
|
+
event_id: Motive's internal event identifier (wire key ``id``).
|
|
37
|
+
start_time: UTC start of the idle interval; the routing anchor.
|
|
38
|
+
end_time: UTC end of the idle interval.
|
|
39
|
+
veh_fuel_start: ELD cumulative fuel counter at interval start;
|
|
40
|
+
null when the vehicle reports no fuel counters
|
|
41
|
+
(live-observed 2026-07-16).
|
|
42
|
+
veh_fuel_end: ELD cumulative fuel counter at interval end; null
|
|
43
|
+
when the vehicle reports no fuel counters.
|
|
44
|
+
lat: Event latitude.
|
|
45
|
+
lon: Event longitude.
|
|
46
|
+
city: Reverse-geocoded place name.
|
|
47
|
+
state: Reverse-geocoded state / province code.
|
|
48
|
+
rg_brg: Reverse-geocode bearing from the matched place, degrees.
|
|
49
|
+
rg_km: Reverse-geocode distance from the matched place,
|
|
50
|
+
kilometers.
|
|
51
|
+
rg_match: Whether the reverse geocoder matched a place directly.
|
|
52
|
+
end_type: Free-form interval-end reason (``"engine_stop"`` /
|
|
53
|
+
``"vehicle_moving"`` observed), mirrored, never interpreted.
|
|
54
|
+
driver: Attributed driver; null when the idle is unattributed.
|
|
55
|
+
vehicle: The vehicle the interval belongs to.
|
|
56
|
+
eld_device: The ELD hardware that reported the interval.
|
|
57
|
+
location: Provider-formatted place string; carries a
|
|
58
|
+
distance-direction prefix when ``rg_match`` is false.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
event_id: int = Field(alias='id')
|
|
62
|
+
start_time: datetime
|
|
63
|
+
end_time: datetime
|
|
64
|
+
veh_fuel_start: float | None = None
|
|
65
|
+
veh_fuel_end: float | None = None
|
|
66
|
+
lat: float
|
|
67
|
+
lon: float
|
|
68
|
+
city: str
|
|
69
|
+
state: str
|
|
70
|
+
rg_brg: float
|
|
71
|
+
rg_km: float
|
|
72
|
+
rg_match: bool
|
|
73
|
+
end_type: str
|
|
74
|
+
driver: UserSummary | None = None
|
|
75
|
+
vehicle: VehicleSummary
|
|
76
|
+
eld_device: EldDeviceInfo
|
|
77
|
+
location: str
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# src/fleetpull/models/motive/shared.py
|
|
2
|
+
"""Motive embedded shapes shared across more than one endpoint.
|
|
3
|
+
|
|
4
|
+
This module holds the per-record building blocks that appear on multiple
|
|
5
|
+
Motive responses — ``UserSummary`` and ``EldDeviceInfo`` on the vehicle
|
|
6
|
+
and vehicle-location records (and the driving-period and idle-event
|
|
7
|
+
records), ``VehicleSummary`` on the driving-period, idle-event, and
|
|
8
|
+
vehicle-utilization records — plus ``MotiveWindowStamp``, the
|
|
9
|
+
decoder-synthesized window-identity type the utilization rollup pair's
|
|
10
|
+
models share. Endpoint-private sub-shapes live in their endpoint module,
|
|
11
|
+
not here; a shape is promoted into this module only once a second
|
|
12
|
+
endpoint actually uses it.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
from datetime import UTC, date, datetime, time
|
|
17
|
+
from typing import Annotated, Final
|
|
18
|
+
|
|
19
|
+
from pydantic import BeforeValidator, Field
|
|
20
|
+
|
|
21
|
+
from fleetpull.model_contract import ResponseModel, empty_str_to_none
|
|
22
|
+
from fleetpull.vocabulary import JsonValue
|
|
23
|
+
|
|
24
|
+
__all__: list[str] = [
|
|
25
|
+
'EldDeviceInfo',
|
|
26
|
+
'MotiveWindowStamp',
|
|
27
|
+
'UserSummary',
|
|
28
|
+
'VehicleSummary',
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Exactly the dashed calendar-date label the builders render; fullmatch
|
|
33
|
+
# keeps date.fromisoformat's laxer forms (compact YYYYMMDD, ISO week
|
|
34
|
+
# dates) failing loudly as the wiring drift they would be.
|
|
35
|
+
_DATE_LABEL_PATTERN: Final[re.Pattern[str]] = re.compile(r'\d{4}-\d{2}-\d{2}')
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _date_label_to_utc_midnight(value: JsonValue | datetime) -> datetime:
|
|
39
|
+
"""The ``MotiveWindowStamp`` ingress: lift a date label to an instant.
|
|
40
|
+
|
|
41
|
+
The Motive window-report decoder stamps each rollup row with the
|
|
42
|
+
sent spec's ``start_date``/``end_date`` values VERBATIM — day-only
|
|
43
|
+
``YYYY-MM-DD`` labels, never instants. A date label cannot validate
|
|
44
|
+
into the timezone-aware datetime the event-time machinery requires
|
|
45
|
+
(an unzoned event time is never assumed), so this lift is the
|
|
46
|
+
structural type recovery DESIGN section 9 allows on a mirror: the
|
|
47
|
+
calendar-day label is preserved exactly (the result's ``.date()`` IS
|
|
48
|
+
the label) and UTC midnight is attached as the label's canonical
|
|
49
|
+
instant representation — a labeling convention for partition
|
|
50
|
+
routing, never a timezone conversion of the data. Strict by design:
|
|
51
|
+
the builder only ever renders date labels, so any other string —
|
|
52
|
+
including a full RFC3339 datetime — is a wiring drift that should
|
|
53
|
+
fail validation loudly, not pass mangled.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
value: The raw stamp value, or an already-recovered datetime on
|
|
57
|
+
a Pydantic revalidation path.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
The label's UTC-midnight instant (passthrough for an
|
|
61
|
+
already-recovered tz-aware value).
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
ValueError: ``value`` is not exactly a dashed ``YYYY-MM-DD``
|
|
65
|
+
label (the laxer ``date.fromisoformat`` forms -- compact
|
|
66
|
+
digits, week dates -- reject too), or is a NAIVE datetime.
|
|
67
|
+
"""
|
|
68
|
+
if isinstance(value, datetime):
|
|
69
|
+
if value.tzinfo is None:
|
|
70
|
+
raise ValueError(
|
|
71
|
+
'window stamp datetime is naive -- an unzoned event time '
|
|
72
|
+
'is never assumed'
|
|
73
|
+
)
|
|
74
|
+
return value
|
|
75
|
+
if isinstance(value, str) and _DATE_LABEL_PATTERN.fullmatch(value):
|
|
76
|
+
return datetime.combine(date.fromisoformat(value), time.min, tzinfo=UTC)
|
|
77
|
+
raise ValueError(f'expected a YYYY-MM-DD window-stamp label, got {value!r}')
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# The window-stamp field type the utilization rollup models use. Plain
|
|
81
|
+
# assignment, not a `type` statement: Pydantic must evaluate the
|
|
82
|
+
# Annotated form eagerly for the metadata lift (the GeotabTimeSpan
|
|
83
|
+
# precedent).
|
|
84
|
+
MotiveWindowStamp = Annotated[datetime, BeforeValidator(_date_label_to_utc_midnight)]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class UserSummary(ResponseModel):
|
|
88
|
+
"""Abbreviated user reference embedded in other Motive records.
|
|
89
|
+
|
|
90
|
+
The compact user-account shape that appears when a user is referenced
|
|
91
|
+
from another entity: the vehicle record's ``permanent_driver`` /
|
|
92
|
+
``current_driver``, the driving-period and idle-event ``driver``
|
|
93
|
+
references, the group record's owner ``user``, and the
|
|
94
|
+
driver-idle-rollup ``driver`` reference (the fourth carrying
|
|
95
|
+
surface, captured 2026-07-21: the exact 8-key shape, populated on
|
|
96
|
+
every attributed rollup and null on the unattributed bucket row).
|
|
97
|
+
The full record comes from the users endpoint. Optionality is
|
|
98
|
+
union-lax across the carrying surfaces (a key populated on one
|
|
99
|
+
surface may be null on another -- e.g. ``username`` carries values
|
|
100
|
+
on driver references and was null on all 152 group owners); each
|
|
101
|
+
consumer's docstring pins its own surface's census.
|
|
102
|
+
|
|
103
|
+
``status`` and ``role`` are modeled as free-form ``str`` rather than
|
|
104
|
+
constrained enums: Motive documents both as plain strings, fleetpull
|
|
105
|
+
does not interpret them, and mirroring them as strings keeps the model
|
|
106
|
+
faithful and evolution-safe.
|
|
107
|
+
|
|
108
|
+
Attributes:
|
|
109
|
+
user_id: Motive's internal user identifier (wire key ``id``).
|
|
110
|
+
first_name: Driver's first name.
|
|
111
|
+
last_name: Driver's last name.
|
|
112
|
+
username: Login username; null when unset.
|
|
113
|
+
email: Driver's email address; null when unset.
|
|
114
|
+
driver_company_id: Company-assigned driver identifier; null when
|
|
115
|
+
unset.
|
|
116
|
+
status: Free-form account-status string; null when absent.
|
|
117
|
+
role: Free-form user-role string; null when absent.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
user_id: int = Field(alias='id')
|
|
121
|
+
first_name: str
|
|
122
|
+
last_name: str
|
|
123
|
+
username: str | None = None
|
|
124
|
+
email: str | None = None
|
|
125
|
+
driver_company_id: str | None = None
|
|
126
|
+
status: str | None = None
|
|
127
|
+
role: str | None = None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class VehicleSummary(ResponseModel):
|
|
131
|
+
"""Abbreviated vehicle reference embedded in other Motive records.
|
|
132
|
+
|
|
133
|
+
The compact vehicle shape that appears when a vehicle is referenced
|
|
134
|
+
from another record: the driving-period and idle-event records
|
|
135
|
+
(captured 2026-07-15) and the vehicle-utilization rollup record
|
|
136
|
+
(captured 2026-07-21 -- the same seven wire keys exactly, its third
|
|
137
|
+
carrying surface). The full vehicle record comes from the vehicles
|
|
138
|
+
endpoint. Optionality is union-lax across the carrying surfaces
|
|
139
|
+
(the UserSummary posture): ``vin`` carried values on every event
|
|
140
|
+
record but is null on some utilization rows, so it is nullable
|
|
141
|
+
here; each consumer's docstring pins its own surface's census.
|
|
142
|
+
|
|
143
|
+
``year`` arrives as a quoted integer (``"2022"``); lax coercion types
|
|
144
|
+
it, and the captured ``"0"`` not-configured sentinel mirrors as
|
|
145
|
+
``0``, never interpreted. The empty-string wire shape (live-observed
|
|
146
|
+
2026-07-16, failing ``int_parsing``) is lifted by a before-validator
|
|
147
|
+
— the type-recovery case DESIGN section 9 allows on a mirror, since
|
|
148
|
+
``""`` cannot validate as an integer at all. ``make`` and ``model``
|
|
149
|
+
arrive as empty strings where the provider has no value and mirror
|
|
150
|
+
verbatim: empty strings normalize to null at the DataFrame boundary,
|
|
151
|
+
never on a string field of the model.
|
|
152
|
+
|
|
153
|
+
Attributes:
|
|
154
|
+
vehicle_id: Motive's internal vehicle identifier (wire key ``id``).
|
|
155
|
+
number: Company-assigned unit number.
|
|
156
|
+
year: Model year; ``0`` is the provider's not-configured
|
|
157
|
+
sentinel; null when the provider sends an empty string or
|
|
158
|
+
nothing.
|
|
159
|
+
make: Manufacturer; the captured empty string mirrors verbatim;
|
|
160
|
+
null when absent.
|
|
161
|
+
model: Model name; the captured empty string mirrors verbatim;
|
|
162
|
+
null when absent.
|
|
163
|
+
vin: Vehicle identification number; null where the provider has
|
|
164
|
+
none (observed on the vehicle-utilization surface).
|
|
165
|
+
metric_units: Whether the vehicle's Motive profile reports metric.
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
vehicle_id: int = Field(alias='id')
|
|
169
|
+
number: str
|
|
170
|
+
year: Annotated[int | None, BeforeValidator(empty_str_to_none)] = None
|
|
171
|
+
make: str | None = None
|
|
172
|
+
model: str | None = None
|
|
173
|
+
vin: str | None = None
|
|
174
|
+
metric_units: bool
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class EldDeviceInfo(ResponseModel):
|
|
178
|
+
"""ELD hardware embedded in other Motive records.
|
|
179
|
+
|
|
180
|
+
Identifies the physical telematics device installed in a vehicle. The
|
|
181
|
+
source documents this shape on both the vehicle record and the
|
|
182
|
+
vehicle-location record.
|
|
183
|
+
|
|
184
|
+
Attributes:
|
|
185
|
+
device_id: Motive's internal device identifier (wire key ``id``).
|
|
186
|
+
identifier: Device serial / hardware identifier.
|
|
187
|
+
model: Device model name (e.g. the Motive LBB designation).
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
device_id: int = Field(alias='id')
|
|
191
|
+
identifier: str
|
|
192
|
+
model: str
|