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,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
|
+
)
|