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,50 @@
|
|
|
1
|
+
# src/fleetpull/config/rate_limit.py
|
|
2
|
+
"""Rate-limit configuration for a single quota scope.
|
|
3
|
+
|
|
4
|
+
One ``RateLimitConfig`` describes the request budget for one quota scope
|
|
5
|
+
(default: one provider). The limiter registry (``network/limits/``) maps
|
|
6
|
+
quota-scope strings to these configs; the limiter reads its refill rate and
|
|
7
|
+
capacity from here. Homed in ``config/`` with the other YAML-facing models;
|
|
8
|
+
each provider config carries its scope's values with a documented default.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from pydantic import Field
|
|
12
|
+
|
|
13
|
+
from fleetpull.config.base import ConfigModel
|
|
14
|
+
|
|
15
|
+
__all__: list[str] = ['RateLimitConfig']
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RateLimitConfig(ConfigModel):
|
|
19
|
+
"""Token-bucket and concurrency settings for one quota scope.
|
|
20
|
+
|
|
21
|
+
``burst`` is the bucket CAPACITY: the maximum number of tokens the
|
|
22
|
+
bucket holds, and therefore the number of requests a cold-started
|
|
23
|
+
limiter can fire immediately before settling to the steady rate of
|
|
24
|
+
``requests_per_period / period_seconds``. This semantics is a settled
|
|
25
|
+
design decision (DESIGN.md §7).
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
requests_per_period: Requests allowed per period (>= 1).
|
|
29
|
+
period_seconds: Length of the rate period in seconds (> 0).
|
|
30
|
+
burst: Bucket capacity in tokens (>= 1).
|
|
31
|
+
max_concurrency: Maximum requests in flight at once (>= 1) --
|
|
32
|
+
enforced by the limiter's semaphore, and also the size of the
|
|
33
|
+
per-provider fetch executor a fan-out run's workers come from
|
|
34
|
+
(``max_workers``, DESIGN section 7), so the pool can never
|
|
35
|
+
outrun the semaphore.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
requests_per_period: int = Field(ge=1)
|
|
39
|
+
period_seconds: float = Field(gt=0)
|
|
40
|
+
burst: int = Field(ge=1)
|
|
41
|
+
max_concurrency: int = Field(ge=1)
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def refill_rate_per_second(self) -> float:
|
|
45
|
+
"""Token refill rate: ``requests_per_period / period_seconds``.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Tokens added to the bucket per second.
|
|
49
|
+
"""
|
|
50
|
+
return self.requests_per_period / self.period_seconds
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# src/fleetpull/config/resolution.py
|
|
2
|
+
"""Pure cross-section resolution over raw YAML documents.
|
|
3
|
+
|
|
4
|
+
The single-concern functions behind ``FleetpullConfig``'s
|
|
5
|
+
``mode='before'`` validators (house style: validator bodies delegate to
|
|
6
|
+
functions). Each takes a raw document mapping and returns one -- no
|
|
7
|
+
environment access, no I/O, no logging -- and each acts only where the
|
|
8
|
+
relevant sections are raw mappings with the relevant keys present:
|
|
9
|
+
already-constructed section models and malformed sections pass through
|
|
10
|
+
unchanged, so shape errors surface from validation with their real key
|
|
11
|
+
paths.
|
|
12
|
+
|
|
13
|
+
The three settled rules (DESIGN section 10):
|
|
14
|
+
- Window-knob precedence per provider: a provider's own
|
|
15
|
+
``lookback_days`` / ``cutoff_days`` key stands; else a declared
|
|
16
|
+
``sync`` value fans in; else the provider model's default
|
|
17
|
+
validates in naturally.
|
|
18
|
+
- ``state.database_path`` defaults to
|
|
19
|
+
``<dataset_root>/.fleetpull/state.sqlite3`` when absent.
|
|
20
|
+
- Either logging file key enables file logging: ``file_level``
|
|
21
|
+
without ``file_path`` injects the default path
|
|
22
|
+
(``<dataset_root>/.fleetpull/fleetpull.log``); ``file_path`` alone
|
|
23
|
+
already works via the model's DEBUG default.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from collections.abc import Mapping
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
__all__: list[str] = [
|
|
30
|
+
'with_log_path_defaulted',
|
|
31
|
+
'with_provider_knobs_applied',
|
|
32
|
+
'with_state_path_defaulted',
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
# The internal directory convention under dataset_root (DESIGN section 5).
|
|
36
|
+
_INTERNAL_SUBDIRECTORY: str = '.fleetpull'
|
|
37
|
+
_STATE_FILENAME: str = 'state.sqlite3'
|
|
38
|
+
_LOG_FILENAME: str = 'fleetpull.log'
|
|
39
|
+
|
|
40
|
+
# The knobs the sync section can declare package-wide.
|
|
41
|
+
_WINDOW_KNOB_KEYS: tuple[str, ...] = ('lookback_days', 'cutoff_days')
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# typing-justified: raw YAML documents are arbitrary until validated
|
|
45
|
+
def with_provider_knobs_applied(document: Mapping[str, object]) -> dict[str, object]:
|
|
46
|
+
"""Fan declared ``sync`` window knobs into providers lacking their own key.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
document: The raw document mapping.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
The document with each provider mapping gaining any ``sync``-declared
|
|
53
|
+
knob it does not set itself; everything else unchanged. A provider key
|
|
54
|
+
always wins over the ``sync`` value (proven by the precedence tests).
|
|
55
|
+
"""
|
|
56
|
+
sync_section = document.get('sync')
|
|
57
|
+
providers_section = document.get('providers')
|
|
58
|
+
if not isinstance(sync_section, Mapping) or not isinstance(
|
|
59
|
+
providers_section, Mapping
|
|
60
|
+
):
|
|
61
|
+
return dict(document)
|
|
62
|
+
declared = {
|
|
63
|
+
key: sync_section[key] for key in _WINDOW_KNOB_KEYS if key in sync_section
|
|
64
|
+
}
|
|
65
|
+
if not declared:
|
|
66
|
+
return dict(document)
|
|
67
|
+
# typing-justified: raw provider entries, arbitrary until validated
|
|
68
|
+
fanned_providers: dict[object, object] = {}
|
|
69
|
+
for provider_name, provider_section in providers_section.items():
|
|
70
|
+
# typing-justified: a raw provider section, arbitrary until validated
|
|
71
|
+
fanned_section: object = provider_section
|
|
72
|
+
if isinstance(provider_section, Mapping):
|
|
73
|
+
missing = {
|
|
74
|
+
key: value
|
|
75
|
+
for key, value in declared.items()
|
|
76
|
+
if key not in provider_section
|
|
77
|
+
}
|
|
78
|
+
if missing:
|
|
79
|
+
fanned_section = {**provider_section, **missing}
|
|
80
|
+
fanned_providers[provider_name] = fanned_section
|
|
81
|
+
return {**document, 'providers': fanned_providers}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# typing-justified: raw YAML documents are arbitrary until validated
|
|
85
|
+
def with_state_path_defaulted(document: Mapping[str, object]) -> dict[str, object]:
|
|
86
|
+
"""Default ``state.database_path`` under ``dataset_root`` when absent.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
document: The raw document mapping.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
The document with ``state.database_path`` set to the DESIGN
|
|
93
|
+
section 5 convention when the key is absent and ``dataset_root``
|
|
94
|
+
is readable; otherwise unchanged.
|
|
95
|
+
"""
|
|
96
|
+
dataset_root = _raw_dataset_root(document)
|
|
97
|
+
if dataset_root is None:
|
|
98
|
+
return dict(document)
|
|
99
|
+
state_section = document.get('state', {})
|
|
100
|
+
if not isinstance(state_section, Mapping) or 'database_path' in state_section:
|
|
101
|
+
return dict(document)
|
|
102
|
+
defaulted = {
|
|
103
|
+
**state_section,
|
|
104
|
+
'database_path': dataset_root / _INTERNAL_SUBDIRECTORY / _STATE_FILENAME,
|
|
105
|
+
}
|
|
106
|
+
return {**document, 'state': defaulted}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# typing-justified: raw YAML documents are arbitrary until validated
|
|
110
|
+
def with_log_path_defaulted(document: Mapping[str, object]) -> dict[str, object]:
|
|
111
|
+
"""Inject the default log path when ``file_level`` is set without a path.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
document: The raw document mapping.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
The document with ``logging.file_path`` defaulted when
|
|
118
|
+
``file_level`` is present without it and ``dataset_root`` is
|
|
119
|
+
readable; otherwise unchanged. Neither file key present means
|
|
120
|
+
file logging stays off; ``file_path`` alone needs no help (the
|
|
121
|
+
model's DEBUG default covers the level).
|
|
122
|
+
"""
|
|
123
|
+
logging_section = document.get('logging')
|
|
124
|
+
if not isinstance(logging_section, Mapping):
|
|
125
|
+
return dict(document)
|
|
126
|
+
if 'file_level' not in logging_section or 'file_path' in logging_section:
|
|
127
|
+
return dict(document)
|
|
128
|
+
dataset_root = _raw_dataset_root(document)
|
|
129
|
+
if dataset_root is None:
|
|
130
|
+
return dict(document)
|
|
131
|
+
defaulted = {
|
|
132
|
+
**logging_section,
|
|
133
|
+
'file_path': dataset_root / _INTERNAL_SUBDIRECTORY / _LOG_FILENAME,
|
|
134
|
+
}
|
|
135
|
+
return {**document, 'logging': defaulted}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# typing-justified: reads a raw YAML value, arbitrary until validated
|
|
139
|
+
def _raw_dataset_root(document: Mapping[str, object]) -> Path | None:
|
|
140
|
+
"""The raw ``storage.dataset_root`` as a Path, or None when unreadable.
|
|
141
|
+
|
|
142
|
+
Unreadable (missing section, missing key, non-path type) returns
|
|
143
|
+
``None`` so the caller passes the document through and validation
|
|
144
|
+
names the real problem at its real key path. The value may still be
|
|
145
|
+
unnormalized here; the derived defaults normalize with everything
|
|
146
|
+
else at field validation (``resolve_path``).
|
|
147
|
+
"""
|
|
148
|
+
storage_section = document.get('storage')
|
|
149
|
+
if not isinstance(storage_section, Mapping):
|
|
150
|
+
return None
|
|
151
|
+
dataset_root = storage_section.get('dataset_root')
|
|
152
|
+
if isinstance(dataset_root, str) and dataset_root.strip():
|
|
153
|
+
return Path(dataset_root)
|
|
154
|
+
if isinstance(dataset_root, Path):
|
|
155
|
+
return dataset_root
|
|
156
|
+
return None
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# src/fleetpull/config/retry.py
|
|
2
|
+
"""Retry policy configuration: attempt budgets and backoff shape.
|
|
3
|
+
|
|
4
|
+
Vocabulary (shared with ``network/retry/decision.py``): a *failure
|
|
5
|
+
count* is one-based within the current retryable category — the first
|
|
6
|
+
TRANSIENT failure of an attempt sequence is 1. ``*_max_failures = N``
|
|
7
|
+
means failures 1..N are each answered with a retry and the (N+1)th
|
|
8
|
+
exhausts the budget: at most N + 1 requests. Counters are independent
|
|
9
|
+
per category within an attempt sequence; a RATE_LIMITED failure
|
|
10
|
+
neither resets nor advances the TRANSIENT count.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from typing import Self
|
|
14
|
+
|
|
15
|
+
from pydantic import Field, model_validator
|
|
16
|
+
|
|
17
|
+
from fleetpull.config.base import ConfigModel
|
|
18
|
+
|
|
19
|
+
__all__: list[str] = ['RetryConfig']
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RetryConfig(ConfigModel):
|
|
23
|
+
"""
|
|
24
|
+
User-facing retry policy, one instance per run.
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
transient_max_failures: Highest TRANSIENT failure count still
|
|
28
|
+
retried. 0 means TRANSIENT failures are never retried.
|
|
29
|
+
transient_backoff_base_seconds: Full-jitter base; the delay
|
|
30
|
+
envelope for failure ``n`` is
|
|
31
|
+
``min(cap, base * 2 ** (n - 1))``.
|
|
32
|
+
transient_backoff_cap_seconds: Ceiling on the delay envelope.
|
|
33
|
+
Only reachable when an operator raises the failure budget.
|
|
34
|
+
rate_limited_max_failures: Highest RATE_LIMITED failure count
|
|
35
|
+
still retried. A circuit breaker against 429 storms, not a
|
|
36
|
+
pacer — pacing is the limiter's job, and RATE_LIMITED
|
|
37
|
+
retries never sleep locally.
|
|
38
|
+
fallback_penalty_seconds: Quota-scope penalty the client
|
|
39
|
+
applies when a rate-limited response carries no usable
|
|
40
|
+
Retry-After. Fires only when a provider is already
|
|
41
|
+
misbehaving, so it errs long; the penalty log line carries
|
|
42
|
+
the raw header value to keep the case diagnosable.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
transient_max_failures: int = Field(default=3, ge=0)
|
|
46
|
+
transient_backoff_base_seconds: float = Field(default=1.0, gt=0)
|
|
47
|
+
transient_backoff_cap_seconds: float = Field(default=30.0, gt=0)
|
|
48
|
+
rate_limited_max_failures: int = Field(default=10, ge=0)
|
|
49
|
+
fallback_penalty_seconds: float = Field(default=60.0, gt=0)
|
|
50
|
+
|
|
51
|
+
@model_validator(mode='after')
|
|
52
|
+
def _cap_not_below_base(self) -> Self:
|
|
53
|
+
"""
|
|
54
|
+
Reject a cap below the base: the envelope would be constant at
|
|
55
|
+
the cap from the first failure, which is never what a config
|
|
56
|
+
author meant.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
The validated model.
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
ValueError: When the cap is below the base.
|
|
63
|
+
"""
|
|
64
|
+
if self.transient_backoff_cap_seconds < self.transient_backoff_base_seconds:
|
|
65
|
+
raise ValueError(
|
|
66
|
+
'transient_backoff_cap_seconds must be >= '
|
|
67
|
+
'transient_backoff_base_seconds'
|
|
68
|
+
)
|
|
69
|
+
return self
|
fleetpull/config/root.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# src/fleetpull/config/root.py
|
|
2
|
+
"""The whole-document configuration root: ``FleetpullConfig`` and ``from_yaml``.
|
|
3
|
+
|
|
4
|
+
The one model family that IS the schema (DESIGN section 10 -- the YAML
|
|
5
|
+
schema is sync's API): the root composes the section models exactly as
|
|
6
|
+
the YAML reads, and cross-section resolution runs as ``mode='before'``
|
|
7
|
+
validation on the root, where key presence is still visible. Each
|
|
8
|
+
validator body is a thin wrapper over a single-concern pure function in
|
|
9
|
+
``config/resolution.py``; the loading steps behind ``from_yaml`` live in
|
|
10
|
+
``config/loading.py``. No masks, no injections, no post-validation
|
|
11
|
+
rewriting -- the sections and the schema agree, so nothing needs
|
|
12
|
+
compensating for.
|
|
13
|
+
|
|
14
|
+
The invariant, precisely: any ``FleetpullConfig`` validated from a raw
|
|
15
|
+
document is fully resolved -- every path field normalized through
|
|
16
|
+
``paths.resolve_path``, ``state.database_path`` and the log-file default
|
|
17
|
+
composed against ``storage.dataset_root``, and every provider's window
|
|
18
|
+
knobs settled by precedence (provider key > ``sync`` key > provider
|
|
19
|
+
default). Direct construction from already-built section models skips
|
|
20
|
+
raw-document resolution but never the enablement invariant: endpoints
|
|
21
|
+
listed with no credential raise ``ConfigurationError`` at validation
|
|
22
|
+
either way.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from collections.abc import Mapping
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Self
|
|
28
|
+
|
|
29
|
+
from pydantic import Field, ValidationError, model_validator
|
|
30
|
+
|
|
31
|
+
from fleetpull.config.base import ConfigModel
|
|
32
|
+
from fleetpull.config.http import HttpConfig
|
|
33
|
+
from fleetpull.config.loading import (
|
|
34
|
+
read_yaml_document,
|
|
35
|
+
validation_detail,
|
|
36
|
+
warn_disabled_providers,
|
|
37
|
+
with_environment_credentials,
|
|
38
|
+
)
|
|
39
|
+
from fleetpull.config.logger import LoggerConfig
|
|
40
|
+
from fleetpull.config.providers import ProvidersConfig, require_provider_credentials
|
|
41
|
+
from fleetpull.config.resolution import (
|
|
42
|
+
with_log_path_defaulted,
|
|
43
|
+
with_provider_knobs_applied,
|
|
44
|
+
with_state_path_defaulted,
|
|
45
|
+
)
|
|
46
|
+
from fleetpull.config.retry import RetryConfig
|
|
47
|
+
from fleetpull.config.sections import StateConfig, StorageConfig, SyncConfig
|
|
48
|
+
from fleetpull.exceptions import ConfigurationError
|
|
49
|
+
|
|
50
|
+
__all__: list[str] = ['FleetpullConfig']
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class FleetpullConfig(ConfigModel):
|
|
54
|
+
"""
|
|
55
|
+
The validated whole-document configuration, one instance per run.
|
|
56
|
+
|
|
57
|
+
Attributes:
|
|
58
|
+
sync: Sync-wide settings: the cold-start anchor and the optional
|
|
59
|
+
package-wide window knobs.
|
|
60
|
+
storage: Where the parquet dataset lives (``dataset_root``'s one
|
|
61
|
+
and only home).
|
|
62
|
+
state: Where operational SQLite state lives; defaulted under
|
|
63
|
+
``dataset_root`` by raw-document resolution.
|
|
64
|
+
logging: Console and file logging; the file-path default composes
|
|
65
|
+
under ``dataset_root`` when only ``file_level`` is given.
|
|
66
|
+
http: Transport timeouts and TLS posture.
|
|
67
|
+
retry: Attempt budgets and backoff shape.
|
|
68
|
+
providers: The per-provider sections; a provider listing
|
|
69
|
+
endpoints without a credential fails validation.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
sync: SyncConfig
|
|
73
|
+
storage: StorageConfig
|
|
74
|
+
state: StateConfig = Field(default_factory=StateConfig)
|
|
75
|
+
logging: LoggerConfig = Field(default_factory=LoggerConfig)
|
|
76
|
+
http: HttpConfig = Field(default_factory=HttpConfig)
|
|
77
|
+
retry: RetryConfig = Field(default_factory=RetryConfig)
|
|
78
|
+
providers: ProvidersConfig
|
|
79
|
+
|
|
80
|
+
@model_validator(mode='before')
|
|
81
|
+
@classmethod
|
|
82
|
+
# typing-justified: mode='before' input is the arbitrary raw document
|
|
83
|
+
def _apply_provider_knob_precedence(cls, document: object) -> object:
|
|
84
|
+
"""Fan declared ``sync`` knobs into providers lacking their own key."""
|
|
85
|
+
if not isinstance(document, Mapping):
|
|
86
|
+
return document
|
|
87
|
+
return with_provider_knobs_applied(document)
|
|
88
|
+
|
|
89
|
+
@model_validator(mode='before')
|
|
90
|
+
@classmethod
|
|
91
|
+
# typing-justified: mode='before' input is the arbitrary raw document
|
|
92
|
+
def _apply_state_path_default(cls, document: object) -> object:
|
|
93
|
+
"""Default ``state.database_path`` under ``dataset_root``."""
|
|
94
|
+
if not isinstance(document, Mapping):
|
|
95
|
+
return document
|
|
96
|
+
return with_state_path_defaulted(document)
|
|
97
|
+
|
|
98
|
+
@model_validator(mode='before')
|
|
99
|
+
@classmethod
|
|
100
|
+
# typing-justified: mode='before' input is the arbitrary raw document
|
|
101
|
+
def _apply_log_path_default(cls, document: object) -> object:
|
|
102
|
+
"""Inject the default log path when ``file_level`` is set alone."""
|
|
103
|
+
if not isinstance(document, Mapping):
|
|
104
|
+
return document
|
|
105
|
+
return with_log_path_defaulted(document)
|
|
106
|
+
|
|
107
|
+
@model_validator(mode='after')
|
|
108
|
+
def _require_credentialed_providers(self) -> Self:
|
|
109
|
+
"""Enforce enablement's credential half; see the providers family.
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
ConfigurationError: A provider lists endpoints with no
|
|
113
|
+
credential. Raised (not a ``ValueError``) so it reaches
|
|
114
|
+
the caller as itself -- Pydantic wraps only
|
|
115
|
+
``ValueError``/``AssertionError`` into validation errors.
|
|
116
|
+
"""
|
|
117
|
+
require_provider_credentials(self.providers)
|
|
118
|
+
return self
|
|
119
|
+
|
|
120
|
+
@classmethod
|
|
121
|
+
def from_yaml(cls, path: Path | str) -> Self:
|
|
122
|
+
"""Load, validate, and resolve one fleetpull YAML configuration file.
|
|
123
|
+
|
|
124
|
+
The loading API: read the file, merge conventional credential
|
|
125
|
+
environment variables (a YAML literal wins; empty counts as
|
|
126
|
+
unset), validate -- which resolves every cross-section default --
|
|
127
|
+
and warn about credentialed providers with no endpoints.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
path: The configuration file to read; a string coerces.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
The fully resolved configuration (the class docstring's
|
|
134
|
+
invariant).
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
ConfigurationError: The file is missing (naming the path), is
|
|
138
|
+
not valid YAML (naming the line), violates the schema
|
|
139
|
+
(naming each offending key path, never a raw value), or
|
|
140
|
+
lists endpoints for a provider with no resolvable
|
|
141
|
+
credential (naming the YAML field and the environment
|
|
142
|
+
variable).
|
|
143
|
+
|
|
144
|
+
Side Effects:
|
|
145
|
+
Reads the file and the process environment; logs one WARNING
|
|
146
|
+
per credentialed provider whose endpoint list is empty.
|
|
147
|
+
"""
|
|
148
|
+
document = read_yaml_document(Path(path))
|
|
149
|
+
merged = with_environment_credentials(document)
|
|
150
|
+
try:
|
|
151
|
+
config = cls.model_validate(merged)
|
|
152
|
+
except ValidationError as validation_error:
|
|
153
|
+
raise ConfigurationError(
|
|
154
|
+
'invalid configuration', detail=validation_detail(validation_error)
|
|
155
|
+
) from None
|
|
156
|
+
warn_disabled_providers(config.providers)
|
|
157
|
+
return config
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# src/fleetpull/config/sections.py
|
|
2
|
+
"""The run-scoped standalone sections: ``sync``, ``storage``, ``state``.
|
|
3
|
+
|
|
4
|
+
One model family per file (house rule): these three sections describe
|
|
5
|
+
run-wide facts -- when history starts, where the dataset lands, where
|
|
6
|
+
operational state lives -- and change together as the run vocabulary
|
|
7
|
+
grows. Path fields normalize through ``paths.resolve_path`` at
|
|
8
|
+
validation, so a validated section is fully resolved by construction and
|
|
9
|
+
downstream code never normalizes.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from datetime import UTC, date, datetime, time
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from pydantic import Field, field_validator
|
|
16
|
+
|
|
17
|
+
from fleetpull.config.base import ConfigModel
|
|
18
|
+
from fleetpull.paths import resolve_path
|
|
19
|
+
|
|
20
|
+
__all__: list[str] = ['StateConfig', 'StorageConfig', 'SyncConfig']
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SyncConfig(ConfigModel):
|
|
24
|
+
"""
|
|
25
|
+
User-facing sync-wide settings, one instance per run.
|
|
26
|
+
|
|
27
|
+
``dataset_root`` deliberately does not live here. It did once; the
|
|
28
|
+
config rebuild moved it to ``StorageConfig`` for real, so the YAML
|
|
29
|
+
section and this model are the same shape and no loader machinery
|
|
30
|
+
bridges them. Consumers that need both the anchor and the dataset
|
|
31
|
+
root (the runner) take the root ``FleetpullConfig`` and read each
|
|
32
|
+
from its own section.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
default_start_date: The cold-start backfill anchor -- the UTC calendar date
|
|
36
|
+
a watermark endpoint's history begins from on its very first run, before
|
|
37
|
+
any committed watermark or completed coverage exists (DESIGN section 4/5
|
|
38
|
+
resume precedence arm 3). Required: there is no safe default for "where
|
|
39
|
+
our history begins", so it must be declared. It goes inert the moment
|
|
40
|
+
observed data or completed coverage exists; thereafter the start is
|
|
41
|
+
derived from those, never from this.
|
|
42
|
+
lookback_days: Package-wide late-arrival re-fetch margin in whole days.
|
|
43
|
+
Optional; ``None`` means no package-wide value is declared. Root-level
|
|
44
|
+
resolution fans a declared value into every provider section that does
|
|
45
|
+
not set its own key (provider key > this > provider default); the
|
|
46
|
+
field itself is never read as a runtime knob.
|
|
47
|
+
cutoff_days: Package-wide trailing-edge holdback in whole days, the
|
|
48
|
+
complement of ``lookback_days``; same precedence and ``None``
|
|
49
|
+
semantics.
|
|
50
|
+
backfill_chunk_days: The width, in whole days, of the work units a
|
|
51
|
+
windowed run's plan tiles its window into (DESIGN sections 4/5).
|
|
52
|
+
Every windowed pull is planned as units and each unit commits
|
|
53
|
+
independently, so smaller chunks mean finer crash-resume
|
|
54
|
+
granularity at the cost of more per-unit commits; a window
|
|
55
|
+
smaller than one chunk degenerates to a single unit (the daily
|
|
56
|
+
run). Sync-level only, deliberately without a per-provider
|
|
57
|
+
override: chunk size is fleetpull's transactional knob, not a
|
|
58
|
+
provider-latency fact. Applies to newly planned units only --
|
|
59
|
+
already-persisted unit boundaries are honored on resume. An
|
|
60
|
+
endpoint whose ``WatermarkMode`` declares ``fixed_unit_days``
|
|
61
|
+
(a window-grain rollup surface, where the unit width is part
|
|
62
|
+
of the row's meaning) tiles at exactly that width instead;
|
|
63
|
+
this knob remains the default for every other endpoint.
|
|
64
|
+
backfill_unit_workers: How many of a windowed endpoint's work units
|
|
65
|
+
drive concurrently (DESIGN section 5's prefix-advance rule keeps
|
|
66
|
+
the watermark truthful under any completion order). ``1`` is the
|
|
67
|
+
serial path. Sync-level like ``backfill_chunk_days`` -- unit
|
|
68
|
+
concurrency is fleetpull's execution knob; the per-provider rate
|
|
69
|
+
budget stays the limiter's job at the transport boundary, so
|
|
70
|
+
workers beyond a provider's budget simply pace on tokens.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
default_start_date: date
|
|
74
|
+
lookback_days: int | None = Field(default=None, ge=0)
|
|
75
|
+
cutoff_days: int | None = Field(default=None, ge=0)
|
|
76
|
+
backfill_chunk_days: int = Field(default=7, ge=1)
|
|
77
|
+
backfill_unit_workers: int = Field(default=4, ge=1)
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def default_start_datetime(self) -> datetime:
|
|
81
|
+
"""The cold-start anchor as a timezone-aware UTC midnight instant.
|
|
82
|
+
|
|
83
|
+
``default_start_date`` is the human-authored calendar date history
|
|
84
|
+
begins from; the resume resolver (``resolve_resume_start``) composes it
|
|
85
|
+
with watermark and frontier datetimes, so it is lifted to the start of
|
|
86
|
+
that UTC day here. Deriving it keeps the stored field a ``date`` (a
|
|
87
|
+
time-of-day on a backfill anchor is meaningless) while handing the
|
|
88
|
+
orchestrator the ``datetime`` it needs, the conversion defined in one
|
|
89
|
+
place.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
``default_start_date`` at 00:00:00 with ``tzinfo`` ``datetime.UTC``.
|
|
93
|
+
"""
|
|
94
|
+
return datetime.combine(self.default_start_date, time.min, tzinfo=UTC)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class StorageConfig(ConfigModel):
|
|
98
|
+
"""
|
|
99
|
+
User-facing storage settings, one instance per run.
|
|
100
|
+
|
|
101
|
+
Attributes:
|
|
102
|
+
dataset_root: The root directory the dataset is written under
|
|
103
|
+
(``{root}/{provider}/{endpoint}/``, DESIGN section 3).
|
|
104
|
+
Required: there is no safe default for where output lands.
|
|
105
|
+
Use a real local path -- never a cloud-synced folder
|
|
106
|
+
(OneDrive and kin), whose sync clients fight the writer's
|
|
107
|
+
atomic renames. Normalized through ``resolve_path`` at
|
|
108
|
+
validation.
|
|
109
|
+
drop_exact_duplicates: Whether write-time compaction drops
|
|
110
|
+
exact-duplicate rows (DESIGN section 6 -- the default-on
|
|
111
|
+
dedup with a config flag off; semantic dedup stays out of
|
|
112
|
+
scope). ``False`` preserves duplicates byte-for-byte.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
dataset_root: Path
|
|
116
|
+
drop_exact_duplicates: bool = True
|
|
117
|
+
|
|
118
|
+
@field_validator('dataset_root')
|
|
119
|
+
@classmethod
|
|
120
|
+
def _resolve(cls, value: Path) -> Path:
|
|
121
|
+
"""Normalize the path lexically; see ``paths.resolve_path``."""
|
|
122
|
+
return resolve_path(value)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class StateConfig(ConfigModel):
|
|
126
|
+
"""
|
|
127
|
+
User-facing operational-state settings, one instance per run.
|
|
128
|
+
|
|
129
|
+
Attributes:
|
|
130
|
+
database_path: Where the SQLite state database lives -- separable
|
|
131
|
+
from ``dataset_root`` so SQLite stays on a real local disk
|
|
132
|
+
even when the parquet dataset sits on a network filesystem
|
|
133
|
+
(DESIGN section 5). Optional in YAML: root-level resolution
|
|
134
|
+
defaults it to ``<dataset_root>/.fleetpull/state.sqlite3``,
|
|
135
|
+
so any ``FleetpullConfig`` validated from a raw document
|
|
136
|
+
carries a real, normalized path. ``None`` survives only
|
|
137
|
+
direct section construction.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
database_path: Path | None = None
|
|
141
|
+
|
|
142
|
+
@field_validator('database_path')
|
|
143
|
+
@classmethod
|
|
144
|
+
def _resolve(cls, value: Path | None) -> Path | None:
|
|
145
|
+
"""Normalize the path lexically when present; ``None`` passes through."""
|
|
146
|
+
return None if value is None else resolve_path(value)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# src/fleetpull/endpoints/__init__.py
|
|
2
|
+
"""The endpoints layer: per-endpoint bindings and the discovery catalogs.
|
|
3
|
+
|
|
4
|
+
Provider bindings live in subpackage faces consumers import directly --
|
|
5
|
+
``fleetpull.endpoints.shared`` for the ``EndpointDefinition`` binding and the
|
|
6
|
+
shared spec-builders, ``fleetpull.endpoints.motive`` (and future provider
|
|
7
|
+
packages) for the binding factories. The catalogs over those bindings --
|
|
8
|
+
``EndpointRegistry`` / ``build_endpoint_registry`` and the roster sibling
|
|
9
|
+
``build_roster_registry`` -- are re-exported here as the layer's public
|
|
10
|
+
lookup surface, so a consumer routes through this face rather than reaching
|
|
11
|
+
the ``registry`` submodule directly.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from fleetpull.endpoints.registry import (
|
|
15
|
+
EndpointRegistry,
|
|
16
|
+
build_endpoint_registry,
|
|
17
|
+
build_roster_registry,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__: list[str] = [
|
|
21
|
+
'EndpointRegistry',
|
|
22
|
+
'build_endpoint_registry',
|
|
23
|
+
'build_roster_registry',
|
|
24
|
+
]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# src/fleetpull/endpoints/geotab/__init__.py
|
|
2
|
+
"""The GeoTab 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 GeoTab endpoint is
|
|
7
|
+
a new leaf module here with nothing to register. Import a specific factory
|
|
8
|
+
from its leaf module (``fleetpull.endpoints.geotab.devices``) when one is
|
|
9
|
+
needed directly.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
__all__: list[str] = []
|