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,94 @@
|
|
|
1
|
+
# src/fleetpull/incremental/window.py
|
|
2
|
+
"""The half-open watermark resume window — the canonical internal fetch window.
|
|
3
|
+
|
|
4
|
+
A pure, stdlib-only leaf beside the cursors (DESIGN §4). ``DateWindow`` is the
|
|
5
|
+
*resume value* a watermark fetch is built from, distinct from the stored
|
|
6
|
+
``DateWatermark`` cursor: the cursor records the maximum event timestamp seen, and
|
|
7
|
+
the window is what the resume resolver derives from it (``watermark - lookback`` up
|
|
8
|
+
to the trailing edge). A fetch is built from the window, never from the watermark
|
|
9
|
+
directly.
|
|
10
|
+
|
|
11
|
+
The window is half-open, ``[start, end)`` — start inclusive, end exclusive — and
|
|
12
|
+
that is the canonical internal form: the half-open boundary is what lets storage's
|
|
13
|
+
delete-by-window predicate (``>= start & < end``) and the start-anchored
|
|
14
|
+
append-filter share one rule, so a cross-boundary event is never double-counted at
|
|
15
|
+
a window edge (DESIGN §4). Unlike the pure ``cursor.py`` carriers, ``DateWindow``
|
|
16
|
+
enforces one structural invariant on construction — ``start < end`` — because that
|
|
17
|
+
ordering has no downstream enforcement; UTC validity, which does (the codec
|
|
18
|
+
boundary), is deferred there exactly as ``DateWatermark`` defers it.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from datetime import date, datetime, timedelta
|
|
23
|
+
|
|
24
|
+
__all__: list[str] = ['DateWindow']
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True, slots=True)
|
|
28
|
+
class DateWindow:
|
|
29
|
+
"""
|
|
30
|
+
The half-open ``[start, end)`` watermark resume window.
|
|
31
|
+
|
|
32
|
+
The resume value the spec-builder turns into a request, distinct from the
|
|
33
|
+
stored ``DateWatermark`` cursor — a fetch is built from this window, not from
|
|
34
|
+
the watermark. Half-open by definition: ``start`` inclusive, ``end`` exclusive.
|
|
35
|
+
The half-open boundary is the canonical internal convention that lets the
|
|
36
|
+
delete-by-window predicate and the start-anchored append-filter share one rule
|
|
37
|
+
(DESIGN §4); there is deliberately no ``contains`` / ``__contains__`` —
|
|
38
|
+
membership is storage's vectorized Polars ``>= start & < end``, never scalar
|
|
39
|
+
row-level.
|
|
40
|
+
|
|
41
|
+
Construction enforces the one structural invariant, ``start < end`` (strict,
|
|
42
|
+
well-ordered, mirroring the run ledger's window). It lives here on the type,
|
|
43
|
+
not deferred like ``DateWatermark``'s UTC check, because the ordering has no
|
|
44
|
+
downstream boundary to catch it — an inverted or empty window is a loud-failure
|
|
45
|
+
bug, not a value to carry; the resume resolver returns ``None`` for a caught-up
|
|
46
|
+
``start >= end`` rather than constructing one, so this invariant now backstops a
|
|
47
|
+
direct construction bug. UTC validity is not checked here; it crosses the codec
|
|
48
|
+
boundary when a spec-builder serializes the bounds, and that boundary raises on
|
|
49
|
+
naive/non-UTC, exactly as for ``DateWatermark``.
|
|
50
|
+
|
|
51
|
+
Attributes:
|
|
52
|
+
start: The window's inclusive start, timezone-aware UTC.
|
|
53
|
+
end: The window's exclusive end, timezone-aware UTC; must be after
|
|
54
|
+
``start``.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
start: datetime
|
|
58
|
+
end: datetime
|
|
59
|
+
|
|
60
|
+
def __post_init__(self) -> None:
|
|
61
|
+
"""
|
|
62
|
+
Enforce the ``start < end`` ordering invariant on construction.
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
ValueError: ``start`` is not strictly before ``end`` (an inverted or
|
|
66
|
+
empty window); the message names both bounds.
|
|
67
|
+
TypeError: ``start`` and ``end`` mix naive and aware datetimes —
|
|
68
|
+
surfaced from the stdlib comparison, a loud failure (UTC validity
|
|
69
|
+
otherwise defers to the codec boundary).
|
|
70
|
+
|
|
71
|
+
Side Effects:
|
|
72
|
+
None — reads ``start`` / ``end`` and may raise.
|
|
73
|
+
"""
|
|
74
|
+
if self.start >= self.end:
|
|
75
|
+
raise ValueError(
|
|
76
|
+
f'DateWindow requires start < end; got start={self.start!r}, '
|
|
77
|
+
f'end={self.end!r}'
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def last_covered_date(self) -> date:
|
|
82
|
+
"""
|
|
83
|
+
The last calendar date the half-open window covers.
|
|
84
|
+
|
|
85
|
+
The ``(end - 1µs).date()`` derivation, stated once: the exclusive
|
|
86
|
+
``end`` itself is not covered, so a window ending exactly at UTC
|
|
87
|
+
midnight does not cover that date while a mid-day ``end`` does. The
|
|
88
|
+
one-microsecond step back is exact because every datetime in
|
|
89
|
+
fleetpull is microsecond-precision UTC end to end (DESIGN §3/§4).
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
The UTC calendar date of the last covered instant.
|
|
93
|
+
"""
|
|
94
|
+
return (self.end - timedelta(microseconds=1)).date()
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# src/fleetpull/logger/setup.py
|
|
2
|
+
"""Logging configuration for the fleetpull package.
|
|
3
|
+
|
|
4
|
+
Provides centralized logging setup so every module that calls
|
|
5
|
+
``logging.getLogger(__name__)`` inherits the same level, format, and
|
|
6
|
+
handler configuration. Users drive the setup from a validated
|
|
7
|
+
``LoggerConfig`` — no ad-hoc level parsing or path expansion happens
|
|
8
|
+
here; that is the configuration layer's job.
|
|
9
|
+
|
|
10
|
+
Library convention:
|
|
11
|
+
fleetpull attaches no handlers at import time and registers no
|
|
12
|
+
``NullHandler``. When a consuming application never configures
|
|
13
|
+
logging, Python's ``lastResort`` handler surfaces WARNING-and-above
|
|
14
|
+
records on stderr — for a data-fetching library, warnings (rate
|
|
15
|
+
limit penalties, session invalidations) should be visible by
|
|
16
|
+
default, not swallowed. Applications that want full control
|
|
17
|
+
configure the ``'fleetpull'`` logger with stdlib tools or call
|
|
18
|
+
:func:`setup_logger`.
|
|
19
|
+
|
|
20
|
+
Timestamps:
|
|
21
|
+
All log timestamps are UTC with a ``Z`` suffix. fleetpull's data
|
|
22
|
+
semantics are UTC end to end; log time matches data time so
|
|
23
|
+
incident correlation never crosses a timezone boundary.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import logging
|
|
27
|
+
import sys
|
|
28
|
+
import time
|
|
29
|
+
from typing import Final, TextIO
|
|
30
|
+
|
|
31
|
+
from fleetpull.config import LoggerConfig
|
|
32
|
+
|
|
33
|
+
__all__: list[str] = ['setup_logger']
|
|
34
|
+
|
|
35
|
+
# Name of the package's root logger; every fleetpull module logger
|
|
36
|
+
# (logging.getLogger(__name__)) is a descendant and inherits this
|
|
37
|
+
# configuration.
|
|
38
|
+
_PACKAGE_LOGGER_NAME: Final[str] = 'fleetpull'
|
|
39
|
+
|
|
40
|
+
# Shared format and date-format for every handler this module attaches.
|
|
41
|
+
# Module-level constants so tests can assert against them by identity
|
|
42
|
+
# rather than re-typing the literals.
|
|
43
|
+
_LOG_FORMAT: Final[str] = (
|
|
44
|
+
'%(asctime)s - %(levelname)-8s - [%(threadName)s] - [%(name)s] - %(message)s'
|
|
45
|
+
)
|
|
46
|
+
_DATE_FORMAT: Final[str] = '%Y-%m-%dT%H:%M:%SZ'
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def setup_logger(config: LoggerConfig) -> None:
|
|
50
|
+
"""
|
|
51
|
+
Configure the fleetpull package logger from a ``LoggerConfig``.
|
|
52
|
+
|
|
53
|
+
Clears any handlers already attached to the ``fleetpull`` logger,
|
|
54
|
+
installs a console handler (always) and an optional file handler,
|
|
55
|
+
and sets the package logger's level so that neither handler is
|
|
56
|
+
starved by an ancestor filter. All module loggers created with
|
|
57
|
+
``logging.getLogger(__name__)`` inside fleetpull inherit the result
|
|
58
|
+
automatically.
|
|
59
|
+
|
|
60
|
+
The function is idempotent: calling it twice produces the same
|
|
61
|
+
handler count as one call. The second call's ``LoggerConfig`` fully
|
|
62
|
+
supersedes the first.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
config: Validated ``LoggerConfig``. ``console_level`` drives
|
|
66
|
+
stderr output; ``file_path``, when set, enables file
|
|
67
|
+
logging at ``file_level``.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
None. Matches the stdlib convention (``logging.basicConfig``
|
|
71
|
+
and friends all return None); module loggers inherit the
|
|
72
|
+
configuration automatically, so no handle is useful.
|
|
73
|
+
|
|
74
|
+
Side Effects:
|
|
75
|
+
- Closes and replaces the ``fleetpull`` logger's existing
|
|
76
|
+
handlers (closing releases the previous configuration's file
|
|
77
|
+
descriptors).
|
|
78
|
+
- Sets the ``fleetpull`` logger's level (the minimum of the
|
|
79
|
+
active handler levels, so no record is filtered at the
|
|
80
|
+
logger before reaching a handler).
|
|
81
|
+
- Sets ``propagate = False`` on the ``fleetpull`` logger so a
|
|
82
|
+
hosting application that configures the root logger does not
|
|
83
|
+
see duplicate records.
|
|
84
|
+
- Creates the parent directory of ``file_path`` if file
|
|
85
|
+
logging is enabled and the directory does not exist.
|
|
86
|
+
- Emits a single INFO-level record confirming the
|
|
87
|
+
configuration, through the just-installed handlers.
|
|
88
|
+
"""
|
|
89
|
+
package_logger: logging.Logger = logging.getLogger(_PACKAGE_LOGGER_NAME)
|
|
90
|
+
|
|
91
|
+
# Close, then clear, existing handlers: closing releases the file
|
|
92
|
+
# descriptors (and Windows file locks) a previous configuration
|
|
93
|
+
# holds; clearing makes the call idempotent so a caller can
|
|
94
|
+
# reconfigure at runtime without accumulating duplicates. Closing
|
|
95
|
+
# a StreamHandler never closes its underlying stream, so stderr is
|
|
96
|
+
# safe.
|
|
97
|
+
for previous_handler in package_logger.handlers:
|
|
98
|
+
previous_handler.close()
|
|
99
|
+
package_logger.handlers.clear()
|
|
100
|
+
|
|
101
|
+
# A hosting application may configure the root logger; disabling
|
|
102
|
+
# propagation prevents every fleetpull record from being emitted
|
|
103
|
+
# twice.
|
|
104
|
+
package_logger.propagate = False
|
|
105
|
+
|
|
106
|
+
log_formatter: logging.Formatter = logging.Formatter(
|
|
107
|
+
fmt=_LOG_FORMAT,
|
|
108
|
+
datefmt=_DATE_FORMAT,
|
|
109
|
+
)
|
|
110
|
+
# UTC timestamps: see the module docstring. The converter applies
|
|
111
|
+
# to asctime only; record.created remains an epoch float.
|
|
112
|
+
log_formatter.converter = time.gmtime
|
|
113
|
+
|
|
114
|
+
console_handler: logging.StreamHandler[TextIO] = logging.StreamHandler(sys.stderr)
|
|
115
|
+
console_handler.setFormatter(log_formatter)
|
|
116
|
+
console_handler.setLevel(config.console_level)
|
|
117
|
+
package_logger.addHandler(console_handler)
|
|
118
|
+
|
|
119
|
+
if config.file_path is not None:
|
|
120
|
+
config.file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
121
|
+
|
|
122
|
+
file_handler: logging.FileHandler = logging.FileHandler(
|
|
123
|
+
filename=config.file_path,
|
|
124
|
+
mode='a',
|
|
125
|
+
encoding='utf-8',
|
|
126
|
+
)
|
|
127
|
+
file_handler.setFormatter(log_formatter)
|
|
128
|
+
file_handler.setLevel(config.file_level)
|
|
129
|
+
package_logger.addHandler(file_handler)
|
|
130
|
+
|
|
131
|
+
# The logger's own level must not filter records before they reach
|
|
132
|
+
# any handler. file_level participates only when a file handler is
|
|
133
|
+
# actually attached; otherwise it is inert by design.
|
|
134
|
+
effective_level: int = (
|
|
135
|
+
min(config.console_level, config.file_level)
|
|
136
|
+
if config.file_path is not None
|
|
137
|
+
else config.console_level
|
|
138
|
+
)
|
|
139
|
+
package_logger.setLevel(effective_level)
|
|
140
|
+
|
|
141
|
+
package_logger.info(
|
|
142
|
+
'Logging configured: console_level=%s%s',
|
|
143
|
+
logging.getLevelName(config.console_level),
|
|
144
|
+
f', file={config.file_path}' if config.file_path is not None else '',
|
|
145
|
+
)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""The response-model contract: the config-policy base and type-recovery coercion."""
|
|
2
|
+
|
|
3
|
+
from fleetpull.model_contract.coercions import empty_str_to_none
|
|
4
|
+
from fleetpull.model_contract.response import ResponseModel
|
|
5
|
+
|
|
6
|
+
__all__: list[str] = ['ResponseModel', 'empty_str_to_none']
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# src/fleetpull/model_contract/coercions.py
|
|
2
|
+
"""Type-recovery coercion for stringly wire values.
|
|
3
|
+
|
|
4
|
+
Value-level wire-cleaning on a model is allowed only where recovering
|
|
5
|
+
the declared type is structural (DESIGN section 9): a field typed
|
|
6
|
+
``int`` receiving ``""`` cannot validate at all, so a before-validator
|
|
7
|
+
lifts the empty string ahead of parsing (Motive ``VehicleSummary.year``
|
|
8
|
+
is the shipped case). String fields never use this — models preserve
|
|
9
|
+
``""`` faithfully from the wire, and empty strings normalize to null
|
|
10
|
+
once, at the DataFrame boundary (``records.normalize_empty_strings``).
|
|
11
|
+
A formatted value someone chose (``"22.3 mi"``) is never touched
|
|
12
|
+
anywhere: parsing it would presume a use case.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
__all__: list[str] = ['empty_str_to_none']
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# A BeforeValidator receives the raw wire value, and the layering
|
|
19
|
+
# contract bars model_contract from vocabulary's JsonValue.
|
|
20
|
+
# typing-justified: object is the strictest annotation available here.
|
|
21
|
+
def empty_str_to_none(value: object) -> object:
|
|
22
|
+
"""Lift a bare empty string to ``None``; everything else passes through.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
value: The raw wire value, ahead of field validation.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
``None`` when ``value`` is exactly ``''``; otherwise ``value``
|
|
29
|
+
unchanged.
|
|
30
|
+
"""
|
|
31
|
+
if value == '':
|
|
32
|
+
return None
|
|
33
|
+
return value
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# src/fleetpull/model_contract/response.py
|
|
2
|
+
"""The shared config-policy base every per-record response model extends.
|
|
3
|
+
|
|
4
|
+
Carries only configuration — no fields, no methods, no schema-derivation helpers
|
|
5
|
+
(those are the records layer's concern, §9, and attach when it is built). Models
|
|
6
|
+
that extend ``ResponseModel`` stay pure API mirrors.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, ConfigDict
|
|
10
|
+
|
|
11
|
+
__all__: list[str] = ['ResponseModel']
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ResponseModel(BaseModel):
|
|
15
|
+
"""
|
|
16
|
+
Config-policy base for response models; subclasses add only fields.
|
|
17
|
+
|
|
18
|
+
Each ``model_config`` setting is deliberate:
|
|
19
|
+
|
|
20
|
+
- ``frozen=True`` — verified inbound data is immutable once validated.
|
|
21
|
+
- ``extra='ignore'`` — response models mirror evolving provider APIs, so a
|
|
22
|
+
field a provider adds is dropped, not a crash (records derives the Polars
|
|
23
|
+
schema from declared fields, so an unknown field has nowhere to land
|
|
24
|
+
anyway). A deliberate departure from the house ``extra='forbid'`` default,
|
|
25
|
+
justified for inbound mirrors specifically.
|
|
26
|
+
- non-strict (the absence of ``strict=True``) — Pydantic's lax coercion turns
|
|
27
|
+
Motive's stringly-typed numerics into typed values; ``strict=True`` would
|
|
28
|
+
reject them, and the §9 coercion overrides only handle what lax cannot. (The
|
|
29
|
+
GeoTab auth slice models use ``strict=True`` because auth responses are
|
|
30
|
+
well-typed — the opposite case.)
|
|
31
|
+
- ``populate_by_name=True`` — models alias camelCase wire keys to snake_case
|
|
32
|
+
fields; this lets construction by either the alias or the field name
|
|
33
|
+
succeed.
|
|
34
|
+
- ``str_strip_whitespace=True`` — trims incoming strings as structural
|
|
35
|
+
hygiene, never a semantic transform.
|
|
36
|
+
- ``validate_default=True`` — house standard.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
model_config = ConfigDict(
|
|
40
|
+
frozen=True,
|
|
41
|
+
extra='ignore',
|
|
42
|
+
populate_by_name=True,
|
|
43
|
+
str_strip_whitespace=True,
|
|
44
|
+
validate_default=True,
|
|
45
|
+
)
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# src/fleetpull/models/geotab/__init__.py
|
|
2
|
+
"""GeoTab response models; the face re-exports each endpoint module's models."""
|
|
3
|
+
|
|
4
|
+
from fleetpull.models.geotab.annotation_log import (
|
|
5
|
+
AnnotationLog,
|
|
6
|
+
AnnotationLogDriverRef,
|
|
7
|
+
AnnotationLogDutyStatusLogRef,
|
|
8
|
+
)
|
|
9
|
+
from fleetpull.models.geotab.audit import Audit
|
|
10
|
+
from fleetpull.models.geotab.device import CustomFeatures, Device, DeviceFlags
|
|
11
|
+
from fleetpull.models.geotab.driver_change import (
|
|
12
|
+
DriverChange,
|
|
13
|
+
DriverChangeDeviceRef,
|
|
14
|
+
DriverChangeDriverRef,
|
|
15
|
+
)
|
|
16
|
+
from fleetpull.models.geotab.duty_status_log import (
|
|
17
|
+
DutyStatusLog,
|
|
18
|
+
DutyStatusLogDeviceRef,
|
|
19
|
+
DutyStatusLogDriverRef,
|
|
20
|
+
)
|
|
21
|
+
from fleetpull.models.geotab.dvir_log import (
|
|
22
|
+
DvirLog,
|
|
23
|
+
DvirLogDefectList,
|
|
24
|
+
DvirLogDeviceRef,
|
|
25
|
+
DvirLogDriverRef,
|
|
26
|
+
DvirLogTrailerRef,
|
|
27
|
+
)
|
|
28
|
+
from fleetpull.models.geotab.exception_event import (
|
|
29
|
+
ExceptionEvent,
|
|
30
|
+
ExceptionEventDeviceRef,
|
|
31
|
+
ExceptionEventDiagnosticRef,
|
|
32
|
+
ExceptionEventDriverRef,
|
|
33
|
+
ExceptionEventRuleRef,
|
|
34
|
+
)
|
|
35
|
+
from fleetpull.models.geotab.fault_data import (
|
|
36
|
+
FaultData,
|
|
37
|
+
FaultDataControllerRef,
|
|
38
|
+
FaultDataDeviceRef,
|
|
39
|
+
FaultDataDiagnosticRef,
|
|
40
|
+
FaultDataFailureModeRef,
|
|
41
|
+
FaultDataFaultStates,
|
|
42
|
+
)
|
|
43
|
+
from fleetpull.models.geotab.fill_up import (
|
|
44
|
+
FillUp,
|
|
45
|
+
FillUpDeviceRef,
|
|
46
|
+
FillUpDriverRef,
|
|
47
|
+
FillUpLocation,
|
|
48
|
+
FillUpTankCapacity,
|
|
49
|
+
FillUpTankLevelExtrema,
|
|
50
|
+
FillUpTankLevelPoint,
|
|
51
|
+
)
|
|
52
|
+
from fleetpull.models.geotab.fuel_and_energy_used import (
|
|
53
|
+
FuelAndEnergyUsed,
|
|
54
|
+
FuelAndEnergyUsedDeviceRef,
|
|
55
|
+
)
|
|
56
|
+
from fleetpull.models.geotab.fuel_tax_detail import (
|
|
57
|
+
FuelTaxDetail,
|
|
58
|
+
FuelTaxDetailDeviceRef,
|
|
59
|
+
FuelTaxDetailDriverRef,
|
|
60
|
+
)
|
|
61
|
+
from fleetpull.models.geotab.log_record import LogRecord, LogRecordDeviceRef
|
|
62
|
+
from fleetpull.models.geotab.media_file import (
|
|
63
|
+
MediaFile,
|
|
64
|
+
MediaFileDeviceRef,
|
|
65
|
+
MediaFileDriverRef,
|
|
66
|
+
)
|
|
67
|
+
from fleetpull.models.geotab.shared import (
|
|
68
|
+
GeotabAddressedLocation,
|
|
69
|
+
GeotabCoordinate,
|
|
70
|
+
GeotabPostalAddress,
|
|
71
|
+
GeotabTimeSpan,
|
|
72
|
+
bare_id_to_reference,
|
|
73
|
+
parse_timespan,
|
|
74
|
+
)
|
|
75
|
+
from fleetpull.models.geotab.shipment_log import (
|
|
76
|
+
ShipmentLog,
|
|
77
|
+
ShipmentLogDeviceRef,
|
|
78
|
+
ShipmentLogDriverRef,
|
|
79
|
+
)
|
|
80
|
+
from fleetpull.models.geotab.status_data import (
|
|
81
|
+
StatusData,
|
|
82
|
+
StatusDataDeviceRef,
|
|
83
|
+
StatusDataDiagnosticRef,
|
|
84
|
+
)
|
|
85
|
+
from fleetpull.models.geotab.text_message import (
|
|
86
|
+
TextMessage,
|
|
87
|
+
TextMessageContent,
|
|
88
|
+
TextMessageDeviceRef,
|
|
89
|
+
)
|
|
90
|
+
from fleetpull.models.geotab.trip import (
|
|
91
|
+
Trip,
|
|
92
|
+
TripDeviceRef,
|
|
93
|
+
TripDriverRef,
|
|
94
|
+
TripStopPoint,
|
|
95
|
+
)
|
|
96
|
+
from fleetpull.models.geotab.user import User, UserAccessGroupFilterRef
|
|
97
|
+
|
|
98
|
+
__all__: list[str] = [
|
|
99
|
+
'AnnotationLog',
|
|
100
|
+
'AnnotationLogDriverRef',
|
|
101
|
+
'AnnotationLogDutyStatusLogRef',
|
|
102
|
+
'Audit',
|
|
103
|
+
'CustomFeatures',
|
|
104
|
+
'Device',
|
|
105
|
+
'DeviceFlags',
|
|
106
|
+
'DriverChange',
|
|
107
|
+
'DriverChangeDeviceRef',
|
|
108
|
+
'DriverChangeDriverRef',
|
|
109
|
+
'DutyStatusLog',
|
|
110
|
+
'DutyStatusLogDeviceRef',
|
|
111
|
+
'DutyStatusLogDriverRef',
|
|
112
|
+
'DvirLog',
|
|
113
|
+
'DvirLogDefectList',
|
|
114
|
+
'DvirLogDeviceRef',
|
|
115
|
+
'DvirLogDriverRef',
|
|
116
|
+
'DvirLogTrailerRef',
|
|
117
|
+
'ExceptionEvent',
|
|
118
|
+
'ExceptionEventDeviceRef',
|
|
119
|
+
'ExceptionEventDiagnosticRef',
|
|
120
|
+
'ExceptionEventDriverRef',
|
|
121
|
+
'ExceptionEventRuleRef',
|
|
122
|
+
'FaultData',
|
|
123
|
+
'FaultDataControllerRef',
|
|
124
|
+
'FaultDataDeviceRef',
|
|
125
|
+
'FaultDataDiagnosticRef',
|
|
126
|
+
'FaultDataFailureModeRef',
|
|
127
|
+
'FaultDataFaultStates',
|
|
128
|
+
'FillUp',
|
|
129
|
+
'FillUpDeviceRef',
|
|
130
|
+
'FillUpDriverRef',
|
|
131
|
+
'FillUpLocation',
|
|
132
|
+
'FillUpTankCapacity',
|
|
133
|
+
'FillUpTankLevelExtrema',
|
|
134
|
+
'FillUpTankLevelPoint',
|
|
135
|
+
'FuelAndEnergyUsed',
|
|
136
|
+
'FuelAndEnergyUsedDeviceRef',
|
|
137
|
+
'FuelTaxDetail',
|
|
138
|
+
'FuelTaxDetailDeviceRef',
|
|
139
|
+
'FuelTaxDetailDriverRef',
|
|
140
|
+
'GeotabAddressedLocation',
|
|
141
|
+
'GeotabCoordinate',
|
|
142
|
+
'GeotabPostalAddress',
|
|
143
|
+
'GeotabTimeSpan',
|
|
144
|
+
'LogRecord',
|
|
145
|
+
'LogRecordDeviceRef',
|
|
146
|
+
'MediaFile',
|
|
147
|
+
'MediaFileDeviceRef',
|
|
148
|
+
'MediaFileDriverRef',
|
|
149
|
+
'ShipmentLog',
|
|
150
|
+
'ShipmentLogDeviceRef',
|
|
151
|
+
'ShipmentLogDriverRef',
|
|
152
|
+
'StatusData',
|
|
153
|
+
'StatusDataDeviceRef',
|
|
154
|
+
'StatusDataDiagnosticRef',
|
|
155
|
+
'TextMessage',
|
|
156
|
+
'TextMessageContent',
|
|
157
|
+
'TextMessageDeviceRef',
|
|
158
|
+
'Trip',
|
|
159
|
+
'TripDeviceRef',
|
|
160
|
+
'TripDriverRef',
|
|
161
|
+
'TripStopPoint',
|
|
162
|
+
'User',
|
|
163
|
+
'UserAccessGroupFilterRef',
|
|
164
|
+
'bare_id_to_reference',
|
|
165
|
+
'parse_timespan',
|
|
166
|
+
]
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# src/fleetpull/models/geotab/annotation_log.py
|
|
2
|
+
"""GeoTab AnnotationLog response model (``GetFeed`` on ``typeName: AnnotationLog``).
|
|
3
|
+
|
|
4
|
+
Written from the 2026-07-21 feed wave three SCALE census (walked at
|
|
5
|
+
scale at the probed tenant), never from docs. An AnnotationLog is one
|
|
6
|
+
free-text annotation attached to an HOS duty-status log — a versioned
|
|
7
|
+
feed (annotations are user-editable), so re-emission under newer
|
|
8
|
+
``version`` tokens is expected and the consumer reconciles by
|
|
9
|
+
``(id, max version)`` (DESIGN §4).
|
|
10
|
+
|
|
11
|
+
This vertical COMPLETES the wave-two ``duty_status_logs`` loop: a
|
|
12
|
+
DutyStatusLog carries an ``annotations`` id-list (the strict
|
|
13
|
+
``list[str]`` reduction), and each AnnotationLog's ``dutyStatusLog.id``
|
|
14
|
+
points BACK to the DutyStatusLog it annotates — a bidirectional join
|
|
15
|
+
across the two verticals (``annotation_logs.duty_status_log__id`` ↔
|
|
16
|
+
``duty_status_logs.annotations``).
|
|
17
|
+
|
|
18
|
+
Requiredness posture (the wave-two conservative stance, DESIGN §8): the
|
|
19
|
+
census is a TENANT-SCOPED observation (8,857 records), so structural
|
|
20
|
+
requiredness is limited to the record identity — ``id``, ``dateTime``
|
|
21
|
+
(the event time), ``version``, and the primary entity ref
|
|
22
|
+
(``dutyStatusLog``, the annotated log — the annotation's subject) — and
|
|
23
|
+
every other field is optional EVEN where the census was total. The
|
|
24
|
+
observed arms:
|
|
25
|
+
|
|
26
|
+
- ``dutyStatusLog`` and ``driver`` were object-only (``{id}``) at
|
|
27
|
+
scale; both ride the shared ``bare_id_to_reference`` lift regardless
|
|
28
|
+
(the census-scope lesson, DESIGN §8: a tenant census cannot prove
|
|
29
|
+
the string arm absent, and the lift is structural and
|
|
30
|
+
sentinel-agnostic).
|
|
31
|
+
|
|
32
|
+
``dateTime`` is recovered tz-aware by validation, the GeoTab sibling
|
|
33
|
+
idiom. ``comment`` is a census-open free-text string.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from datetime import datetime
|
|
37
|
+
from typing import Annotated
|
|
38
|
+
|
|
39
|
+
from pydantic import BeforeValidator, ConfigDict
|
|
40
|
+
from pydantic.alias_generators import to_camel
|
|
41
|
+
|
|
42
|
+
from fleetpull.model_contract import ResponseModel
|
|
43
|
+
from fleetpull.models.geotab.shared import bare_id_to_reference
|
|
44
|
+
|
|
45
|
+
__all__: list[str] = [
|
|
46
|
+
'AnnotationLog',
|
|
47
|
+
'AnnotationLogDriverRef',
|
|
48
|
+
'AnnotationLogDutyStatusLogRef',
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class AnnotationLogDriverRef(ResponseModel):
|
|
53
|
+
"""The annotating driver's reference.
|
|
54
|
+
|
|
55
|
+
Census-observed as an ``{id}`` object at scale; the shared coercion
|
|
56
|
+
lifts a bare string defensively (the census-scope lesson).
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
model_config = ConfigDict(alias_generator=to_camel)
|
|
60
|
+
|
|
61
|
+
id: str
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class AnnotationLogDutyStatusLogRef(ResponseModel):
|
|
65
|
+
"""The annotated duty-status log's reference — the annotation's subject.
|
|
66
|
+
|
|
67
|
+
The BACK-REFERENCE completing the wave-two loop: ``id`` points to
|
|
68
|
+
the ``duty_status_logs`` record this annotation belongs to.
|
|
69
|
+
Census-observed as an ``{id}`` object at scale; the shared coercion
|
|
70
|
+
lifts a bare string defensively (the census-scope lesson).
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
model_config = ConfigDict(alias_generator=to_camel)
|
|
74
|
+
|
|
75
|
+
id: str
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class AnnotationLog(ResponseModel):
|
|
79
|
+
"""One GeoTab duty-status-log annotation from the AnnotationLog feed.
|
|
80
|
+
|
|
81
|
+
The wave-two conservative mirror (the module docstring's posture):
|
|
82
|
+
``id`` / ``date_time`` / ``version`` / ``duty_status_log`` required,
|
|
83
|
+
everything else optional even where census-total.
|
|
84
|
+
|
|
85
|
+
Attributes:
|
|
86
|
+
comment: The annotation's free text (census-open str).
|
|
87
|
+
date_time: The annotation's UTC instant — the endpoint's event
|
|
88
|
+
time.
|
|
89
|
+
driver: The annotating driver's reference (object-only at scale;
|
|
90
|
+
defensively lifted).
|
|
91
|
+
duty_status_log: The annotated duty-status log's reference — the
|
|
92
|
+
back-reference to the ``duty_status_logs`` vertical.
|
|
93
|
+
id: GeoTab's record id.
|
|
94
|
+
version: The record's version token — the reconcile key beside
|
|
95
|
+
``id``.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
model_config = ConfigDict(alias_generator=to_camel)
|
|
99
|
+
|
|
100
|
+
comment: str | None = None
|
|
101
|
+
date_time: datetime
|
|
102
|
+
driver: Annotated[
|
|
103
|
+
AnnotationLogDriverRef | None, BeforeValidator(bare_id_to_reference)
|
|
104
|
+
] = None
|
|
105
|
+
duty_status_log: Annotated[
|
|
106
|
+
AnnotationLogDutyStatusLogRef, BeforeValidator(bare_id_to_reference)
|
|
107
|
+
]
|
|
108
|
+
id: str
|
|
109
|
+
version: str
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# src/fleetpull/models/geotab/audit.py
|
|
2
|
+
"""GeoTab Audit response model (``GetFeed`` on ``typeName: Audit``).
|
|
3
|
+
|
|
4
|
+
Written from the 2026-07-21 feed wave three SCALE census (walked at
|
|
5
|
+
scale at the probed tenant), never from docs. An Audit is one
|
|
6
|
+
configuration-change audit-trail entry — a versioned feed, so
|
|
7
|
+
re-emission under newer ``version`` tokens is expected and the consumer
|
|
8
|
+
reconciles by ``(id, max version)`` (DESIGN §4).
|
|
9
|
+
|
|
10
|
+
The SIMPLEST feed vertical: NO reference fields at all (no ref models),
|
|
11
|
+
six flat keys. Requiredness posture (the wave-two conservative stance,
|
|
12
|
+
DESIGN §8): the census is a TENANT-SCOPED observation (20,000 records),
|
|
13
|
+
so structural requiredness is limited to the record identity — ``id``,
|
|
14
|
+
``dateTime`` (the event time), and ``version`` — and every other field
|
|
15
|
+
is optional EVEN where the census was total.
|
|
16
|
+
|
|
17
|
+
``dateTime`` is recovered tz-aware by validation, the GeoTab sibling
|
|
18
|
+
idiom. ``comment``, ``name``, and ``userName`` are census-open
|
|
19
|
+
free-text strings.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from datetime import datetime
|
|
23
|
+
|
|
24
|
+
from pydantic import ConfigDict
|
|
25
|
+
from pydantic.alias_generators import to_camel
|
|
26
|
+
|
|
27
|
+
from fleetpull.model_contract import ResponseModel
|
|
28
|
+
|
|
29
|
+
__all__: list[str] = ['Audit']
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Audit(ResponseModel):
|
|
33
|
+
"""One GeoTab audit-trail entry from the Audit feed.
|
|
34
|
+
|
|
35
|
+
The wave-two conservative mirror (the module docstring's posture):
|
|
36
|
+
``id`` / ``date_time`` / ``version`` required, everything else
|
|
37
|
+
optional even where census-total.
|
|
38
|
+
|
|
39
|
+
Attributes:
|
|
40
|
+
comment: The audit entry's free text (census-open str).
|
|
41
|
+
date_time: The entry's UTC instant — the endpoint's event time.
|
|
42
|
+
id: GeoTab's record id.
|
|
43
|
+
name: The audited object's name (census-open str).
|
|
44
|
+
user_name: The acting user's name (census-open str).
|
|
45
|
+
version: The record's version token — the reconcile key beside
|
|
46
|
+
``id``.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
model_config = ConfigDict(alias_generator=to_camel)
|
|
50
|
+
|
|
51
|
+
comment: str | None = None
|
|
52
|
+
date_time: datetime
|
|
53
|
+
id: str
|
|
54
|
+
name: str | None = None
|
|
55
|
+
user_name: str | None = None
|
|
56
|
+
version: str
|