API reference¶
Rendered from the package docstrings so that each Implements :need:`REQ_…`
reference resolves against Requirements. An unresolved :need: here
fails the build under -W, which catches a typo’d requirement ID.
trackiwi¶
Read-only client for a personal trackiwi GPS account.
Implements Package imports only the st... (REQ_ZERO_DEPS) (standard library only, no runtime
dependencies), Read-only by construction (REQ_READONLY) (against the trackiwi API the only
state-changing request is DELETE /api/v2/session; InfluxDB writes are
bounded by InfluxDB writes are bounded... (REQ_INFLUX_WRITE_SCOPE)) and No private data in the repo... (REQ_NO_PRIVATE_DATA)
(no private data is ever committed). Requirements live as sphinx-needs
objects under docs/; see docs/requirements.rst.
- exception TrackiwiError(message: str, status: int | None = None)¶
Any failure talking to trackiwi.
- exception AuthError(message: str, status: int | None = None)¶
Credentials were rejected, or the session expired.
- COLUMNS = ('id', 'tracker_id', 'fix_at', 'fix_timezone', 'latitude', 'longitude', 'altitude', 'speed', 'course', 'distance', 'rssi', 'sat', 'battery', 'voltage')¶
Column order of the sync CSV and of the local positions table.
- class Row(*args, **kwargs)¶
A position row addressed by column name: a sqlite3.Row or a dict.
Values are Any because the cache stores heterogeneous columns (ints, floats, None).
trackiwi.client¶
Network access to the trackiwi API.
This is the only module that performs network I/O. It knows nothing about SQLite or output formats.
- normalize_epoch(value: int) int¶
Return value as epoch seconds, accepting seconds or milliseconds.
Implements fix_at is normalised to epo... (REQ_FIX_AT_SECONDS).
The sync CSV’s form only; see
epoch_from_iso()for the ISO form.>>> normalize_epoch(1700000000) 1700000000 >>> normalize_epoch(1700000000000) 1700000000
- epoch_from_iso(value: str) int¶
Return an ISO 8601 timestamp as epoch seconds.
Implements fix_at is normalised to epo... (REQ_FIX_AT_SECONDS).
The ISO form of GET /api/v2/trackers (latest_positionlog.fix_at, received_at). Kept apart from
normalize_epoch()because a digit string would be ambiguous and the error handling differs: a bad CSV field raises ValueError (the row is skipped), a bad JSON field has no row to skip and raises TrackiwiError. A zone-less value is read as UTC, not the machine’s zone. The result is range-checked like the CSV path’s.>>> epoch_from_iso("2023-11-14T22:13:20Z") 1700000000 >>> epoch_from_iso("2023-11-14T22:13:20+00:00") 1700000000 >>> epoch_from_iso("2023-11-14T22:13:20") # naive value read as UTC 1700000000
- _coerce(column: str, raw: str) float | int | None¶
Coerce one CSV field, rejecting a non-finite numeric value.
Implements A malformed row is skipped ... (REQ_MALFORMED_SKIP): a ValueError here lets
parse_positions()skip and count the row.
- parse_positions(text: str) tuple[list[tuple[Any, ...]], int, int | None]¶
Parse the sync endpoint’s CSV body.
Implements A malformed row is skipped ... (REQ_MALFORMED_SKIP), fix_at is normalised to epo... (REQ_FIX_AT_SECONDS) and Sync moves past malformed rows (REQ_SYNC_PAST_MALFORMED).
Returns (rows, skipped, last_id): tuples in
trackiwi.COLUMNSorder with fix_at in epoch seconds. Malformed rows are skipped and counted. last_id is the highest integer id on any line, skipped ones included, orNone.>>> body = "1,7,1700000000,1,31.0,-41.0,12,0.0,0,0,-71,9,98,4120\n" >>> rows, skipped, last_id = parse_positions(body) >>> skipped, last_id (0, 1) >>> rows[0][:3] (1, 7, 1700000000) >>> parse_positions("2,7,broken\n") ([], 1, 2) >>> parse_positions("broken,row\n") ([], 1, None)
- default_config_path() Path¶
Return the credentials file path, honouring
XDG_CONFIG_HOME.
- _check(status: int, body: bytes, *secrets: str | None) None¶
Raise the right error for a non-2xx status, per the app’s own handling.
Implements The token is never logged o... (REQ_TOKEN_NEVER_LOGGED): the interpolated body is redacted. Implements Redirects are never followed (REQ_NO_REDIRECTS): a 3xx raises.
secrets are values to scrub from the body — the session token, and the password on login, since a gateway can echo the request body into a 4xx.
- _require_https(api_base: str) str¶
Return api_base without its trailing slash, insisting on https.
Implements The API base must be https (REQ_API_BASE_HTTPS).
The value comes from the login response, –api-base or the config file; http:// would send the bearer token in cleartext. The message names the way out because a bad stored value fails every authenticated command (see Client.load, cmd_logout).
- class Client(api_base: str | None = None, token: str | None = None, user_id: int | None = None, opener: Callable[[...], Any] | None = None)¶
Read-only access to a trackiwi account.
The API base is never hardcoded: it comes from the login response’s server field. opener exists so tests can inject a fake transport.
- property authenticated: bool¶
True when both a token and an API base are present.
- classmethod load(opener: Callable[[...], Any] | None = None) Client¶
Load the stored session, self-healing a widened config file.
Implements The token file is 0600 and ... (REQ_CONFIG_MODE_0600): a config file whose mode was widened after login is narrowed back to 0600 here.
- classmethod forget_local_session() bool¶
Delete the stored credentials, returning whether there were any.
Does not need a loadable config, which is the point: the token grants live vehicle location, so it must stay removable even when the config file holds a value that Client.load refuses (an http:// API base, say). cmd_logout uses this as its fallback.
Implements A stored token is always re... (REQ_TOKEN_REMOVABLE).
- save() None¶
Persist the session owner-only. The password is never stored.
Implements The token file is 0600 and ... (REQ_CONFIG_MODE_0600).
Directory and file are created at their final mode, never widened and then narrowed, so no other process can hold an open fd on a wider file.
- login(email: str, password: str) dict[str, Any]¶
Authenticate and store the session.
Implements The API base comes from the... (REQ_API_BASE_FROM_LOGIN): the API base is taken from the login response’s
serverfield, and The API base must be https (REQ_API_BASE_HTTPS) via_require_https()before it is stored.
- logout() bool¶
Revoke server-side first; a local delete alone leaves a live token.
Implements logout revokes server-side ... (REQ_LOGOUT_REVOKES) and Read-only by construction (REQ_READONLY) (the one state-changing request this client makes,
DELETE /api/v2/session).Returns True if server-side revocation succeeded; False if the attempt failed (network error, 5xx, etc.). The local state (token, api_base, user_id, config file) is always cleared regardless, since the caller must decide whether to warn the user that the token may remain valid server-side.
- trackers() list[dict[str, Any]]¶
List the account’s trackers.
This output contains location data, not only device metadata. Each record carries an alarm_configuration whose geofence alarm holds lat/long/radius — i.e. a place the owner cares about, typically where the vehicle is kept — and a latest_positionlog with the current fix. Treat the return value with the same care as the position cache (design spec, section 7.1), not as an inventory listing.
Records also carry installed_at, membership_valid, membership_ends_at and prunable_at; the last of these reads as the vendor’s own data-retention horizon for the account’s history.
Timestamps here are ISO 8601 strings, unlike the epoch integers in the sync CSV — latest_positionlog.fix_at and received_at both are. Use
epoch_from_iso()on them.
- tours() list[dict[str, Any]]¶
List the account’s tours.
Verified live: a bare JSON list whose records carry color, ended_at, id, name, started_at and tracker_id.
- markers() list[dict[str, Any]]¶
List the account’s markers.
The element shape is unverified. The endpoint answered 200 with an empty list on the account it was checked against, so nothing is known about a marker record beyond the fact that the response is a bare list. Records are returned exactly as parsed; no field is promised.
- marker_categories() list[dict[str, Any]]¶
List the account’s marker categories.
Verified live: a bare JSON list whose records carry color, id and name.
- alarms() list[dict[str, Any]]¶
List the account’s alarms.
Verified live: a bare JSON list whose records carry acknowledged, alarm_type, event, id, inserted_at and tracker_id.
This output is a location history: each record’s event embeds latitude/longitude.
Nothing here acknowledges, clears or tests an alarm: this client is read-only by construction (design spec, section 7.5).
List the account’s active shares.
The element shape is unverified, for the same reason as
markers(): the endpoint answered 200 with an empty list on the account it was checked against. Records are returned exactly as parsed.Read-only: this lists shares, it cannot create or revoke one. share create was considered and rejected (design spec, section 7.5), because anyone holding a share link can see the vehicle’s position.
- sync(offset: int | None = None) Iterator[tuple[list[tuple[Any, ...]], int, int | None]]¶
Yield (rows, skipped, total) batches until the server runs dry.
Implements Sync is resumable (REQ_SYNC_RESUME), The sync offset is exclusive (REQ_SYNC_OFFSET_EXCLUSIVE), Sync fails loudly on a stuc... (REQ_SYNC_FAIL_LOUD) and Sync moves past malformed rows (REQ_SYNC_PAST_MALFORMED).
Offset-based, hence resumable: re-running continues from the highest stored id (there is no retry logic). The offset advances to the highest id on the page, malformed rows included, so a batch’s rows can be empty. Raises when a page has skipped rows but no readable id, or when the highest id does not pass the requested offset (it would loop).
trackiwi.store¶
Local SQLite cache of positions.
This is the only module that touches the database. It performs no network I/O. Open failures surface as TrackiwiError, never raw sqlite3 exceptions.
The cache is a complete movement history, so the file is created owner-only inside an owner-only directory.
- SIDECAR_SUFFIXES = ('-journal', '-wal', '-shm')¶
Suffixes SQLite appends for its rollback journal and WAL; they can hold rows.
- default_db_path() Path¶
Return the cache path, honouring
XDG_DATA_HOME.
- cache_files(path: Path) tuple[Path, ...]¶
Every file that can hold cached positions for the database at path.
Shared by trackiwi purge and Store.purge().
- class Store(path: Path | str | None = None, *, create: bool = True)¶
Owner-only SQLite cache of position rows.
- __enter__() Store¶
Open the cache owner-only, distinguishing a lock from corruption.
Implements The position cache is 0600 ... (REQ_CACHE_MODE_0600) (0600 file in a 0700 directory, self-healing a widened mode) and A lock is never mistaken fo... (REQ_PURGE_NOT_ON_LOCK) (a locked cache is reported as transient, never as corruption to purge), the latter via
_open_failure().With
create=Falsea missing cache is not created: an in-memory empty database answers every read, so a read-only command (export,heading) never brings the file into being.
- property conn: Connection¶
The open connection, or raise if used outside a
withblock.
- upsert(rows: Iterable[tuple[Any, ...]]) int¶
Insert new rows, returning how many were newly inserted.
INSERT OR IGNORE: positions are immutable fixes keyed by the vendor’s own id, so an id already stored is kept unchanged — a re-fetched row (e.g. fromsync --full) is never rewritten, which could otherwise change a point the mirror has already sent. The inserted count comes straight fromcursor.rowcount(a row that violates a constraint is ignored, not stored, and not counted).Implements A fix re-sent under a new i... (REQ_CACHE_NO_RESENT_FIX): a row matching a cached fix (tracker, time, place) under another id is ignored too; the
(tracker_id, fix_at)index keeps that lookup cheap.
- max_id() int | None¶
Return the highest stored position id, or
Nonewhen empty.
- count() int¶
Return the number of cached positions.
- query(tracker_id: int | None = None, start: int | None = None, end: int | None = None) list[Row]¶
Rows ordered by time. start and end are inclusive epoch seconds.
- mirror_position(target: str) int¶
Return the highest id already mirrored to target, or 0 if none.
Part of The mirror never advances p... (REQ_MIRROR_RESUME): the mirror resumes from here.
- set_mirror_position(target: str, last_id: int) None¶
Record that every row up to last_id reached target, and commit.
Commits immediately so a later failure cannot roll back acknowledged progress.
- rows_after(last_id: int, limit: int) list[Row]¶
Return up to limit rows with
id > last_id, ordered by id.
- set_tracker_names(names: Mapping[int, str]) None¶
Pin each tracker’s first real name; upgrade a fallback to a real one.
tracker_nameis part of the InfluxDB series key, so replacing a real name when a tracker is renamed would split its history into two series and defeat idempotent re-mirroring (#24, Re-sending positions never ... (REQ_MIRROR_IDEMPOTENT)). The first real name is therefore kept; a later, different name is ignored. The one exception is thetracker <id>fallback from name_unnamed_trackers, which a real name still replaces (A tracker without a name ne... (REQ_MIRROR_FALLBACK_NAME)).
- name_unnamed_trackers() None¶
Give every cached tracker that has no stored name
tracker <id>.Implements A tracker without a name ne... (REQ_MIRROR_FALLBACK_NAME). Never overwrites a stored name; set_tracker_names may replace a fallback.
- tracker_names() dict[int, str]¶
Return every stored tracker id → display name.
- purge() None¶
Close and delete the cache, including SQLite’s sidecar files.
Implements Purge deletes the cache eve... (REQ_PURGE_DELETES).
Library twin of trackiwi purge (same cache_files). The CLI does not call this: it unlinks without opening, so a corrupt cache still goes.
trackiwi.export¶
Convert position rows into standard GPS formats.
Pure: rows in, text out. No I/O. fix_at is already epoch seconds by the time rows reach here, normalised at parse time.
- by_tracker(rows: Sequence[Row]) dict[Any, list[Row]]¶
Group rows by tracker_id, keeping each tracker’s rows in input order.
store.query() interleaves trackers by time; exporters need one track per tracker. Group order is first appearance (earliest fix).
- _finite(row: Row) tuple[float, float]¶
Return (latitude, longitude), rejecting a non-finite pair.
Implements GeoJSON is RFC 7946-valid (REQ_GEOJSON_VALID) and A malformed row is skipped ... (REQ_MALFORMED_SKIP).
Parse time already rejects these; this guards rows cached earlier. inf is not a valid xsd:decimal. cmd_export converts the ValueError.
- to_gpx(rows: Sequence[Row]) str¶
Render rows as GPX 1.1 with one <trk> per tracker.
Implements A multi-tracker export keep... (REQ_EXPORT_PER_TRACKER).
>>> rows = [ ... {"id": 1, "tracker_id": 7, "fix_at": 1700000000, ... "latitude": 31.0, "longitude": -41.0, "altitude": None}, ... ] >>> gpx = to_gpx(rows) >>> '<trk>' in gpx and 'tracker 7' in gpx True >>> 'lat="31.000000"' in gpx and 'lon="-41.000000"' in gpx True
- to_geojson(rows: Sequence[Row]) str¶
Render rows as a FeatureCollection with one Feature per tracker.
Implements A multi-tracker export keep... (REQ_EXPORT_PER_TRACKER) and GeoJSON is RFC 7946-valid (REQ_GEOJSON_VALID).
A tracker with a single fix becomes a Point, one with several a LineString. Each Feature carries its tracker_id in properties.
>>> import json >>> rows = [ ... {"id": 1, "tracker_id": 7, "fix_at": 1700000000, ... "latitude": 31.0, "longitude": -41.0}, ... {"id": 2, "tracker_id": 7, "fix_at": 1700000060, ... "latitude": 31.5, "longitude": -41.5}, ... ] >>> doc = json.loads(to_geojson(rows)) >>> doc["type"] 'FeatureCollection' >>> doc["features"][0]["geometry"]["type"] 'LineString' >>> doc["features"][0]["properties"]["tracker_id"] 7
- to_csv(rows: Sequence[Row]) str¶
Render rows as CSV with a header, in
trackiwi.COLUMNSorder.A raw dump of the cache, so unlike GPX/GeoJSON it does not re-validate.
>>> row = dict(zip(COLUMNS, ... (1, 7, 1700000000, 1, 31.0, -41.0, 12, 0.0, 0, 0, -71, 9, 98, 4120))) >>> print(to_csv([row]), end="") id,tracker_id,fix_at,fix_timezone,latitude,longitude,altitude,speed,course,distance,rssi,sat,battery,voltage 1,7,1700000000,1,31.0,-41.0,12,0.0,0,0,-71,9,98,4120
trackiwi.heading¶
Estimate which way a tracker is pointing from cached positions.
Pure: rows in, an estimate out. No I/O, no network.
The feed’s only directional field, course, is GPS course-over-ground: it means something only while moving and drifts when parked. There is no compass, so a stationary heading is inferred from the approach.
Caveats, repeated on the CLI. The parked heading assumes the vehicle stopped nose-first in its direction of travel. A vehicle that reversed into its spot points the other way, and nothing in the data reveals that. Every value from this module is an estimate; GPS alone cannot determine a stationary vehicle’s true heading.
- DEFAULT_STALE_AFTER = 3600¶
Seconds after the last moving fix at which a parked estimate becomes
stale. A judgement call (the vehicle may have been moved by hand), so it stays a parameter.
- DEFAULT_MIN_MOVING_SPEED = 2.0¶
Minimum
speed(km/h) that counts as movement. Below it a fix is GPS jitter at a standstill, whosespeedis non-zero andcourseis noise. A judgement call, so it stays a parameter like DEFAULT_STALE_AFTER.
- class Heading(degrees: float | None, state: Literal['moving', 'freshly_parked', 'stale', 'unknown'], source: Literal['course', 'bearing', 'none'], moved_at: int | None, parked_for: int | None)¶
A tracker’s estimated orientation with an explicit confidence flag.
degreesis clockwise from true north in[0, 360), orNonewhen nothing supports an estimate.stateis the confidence flag:moving: the last fix is at or abovemin_speed(a slower fix is GPS jitter at a standstill); the heading is that fix’scourse(GPS course-over-ground, reliable while moving).freshly_parked: the last fix is stationary and the last movement was no more thanstale_afterseconds ago; the heading is the direction of approach as the vehicle came to rest.stale: asfreshly_parked, but the last movement is older thanstale_after. The value is still the approach heading, offered with less confidence.unknown: no fix, or no fix has ever shown movement, so there is no approach to read a heading from.
sourcesays wheredegreescame from:bearingis the initial great-circle bearing between the last two moving positions (preferred when parked, because rawcourseis noisy at low speed),courseis the raw field,nonegoes withdegrees is None.moved_atis thefix_at(epoch seconds) of the last fix at or abovemin_speed;parked_foris how many seconds ago that was, and isNonewhile moving or unknown.- degrees: float | None¶
- state: Literal['moving', 'freshly_parked', 'stale', 'unknown']¶
- source: Literal['course', 'bearing', 'none']¶
- moved_at: int | None¶
- parked_for: int | None¶
- initial_bearing(lat1: float, lon1: float, lat2: float, lon2: float) float¶
Initial great-circle bearing from point 1 to point 2, in
[0, 360).Implements A heading is estimated from... (REQ_HEADING_ESTIMATE).
Degrees clockwise from true north, spherical Earth. Works across the antimeridian because the longitude difference goes through
sin/cos.>>> initial_bearing(0.0, 0.0, 1.0, 0.0) 0.0 >>> round(initial_bearing(0.0, 0.0, 0.0, 1.0), 6) 90.0 >>> round(initial_bearing(1.0, 0.0, 0.0, 0.0), 6) 180.0 >>> round(initial_bearing(0.0, 1.0, 0.0, 0.0), 6) 270.0 >>> round(initial_bearing(0.0, 179.5, 0.0, -179.5), 6) # eastwards across 180° 90.0
- _bearing_between(earlier: Row, later: Row) float | None¶
Bearing
earlier→later, orNonewhen the pair cannot give one.Identical or non-finite coordinates give
Noneso the caller falls back to the rawcourse.
- _approach(moving: Sequence[Row]) tuple[float | None, Literal['course', 'bearing', 'none']]¶
Heading from the last two moving fixes: their bearing, else the last
course.The pair must be close in time (
_MAX_BEARING_GAP) and far enough apart in space (_MIN_SEPARATION_M); otherwise it is not one continuous approach and the bearing would be noise, so the rawcourseis used (#23).
- estimate_heading(rows: Sequence[Row], *, now: int | None = None, stale_after: int = 3600, min_speed: float = 2.0) Heading¶
Estimate one tracker’s heading from its time-ordered position rows.
Implements A heading is estimated from... (REQ_HEADING_ESTIMATE) and The heading carries a confi... (REQ_HEADING_STATE).
rowsmust belong to a single tracker and be ordered by time, as Store.query(tracker_id=…) returns them; the last row is the current fix.now(epoch seconds, default: the wall clock) andstale_afterdecidefreshly_parkedversusstale.Moving: the last fix’s
course. Parked: the bearing between the last two moving fixes, else the last moving fix’scourse. No moving fix isunknown, never an error. See the module docstring for caveats.>>> def fix(fix_at, lat, lon, speed, course): ... return {"fix_at": fix_at, "latitude": lat, "longitude": lon, ... "speed": speed, "course": course} >>> drive = [fix(1000, 50.0, 8.0, 30.0, 0), fix(1010, 50.001, 8.0, 30.0, 0)] >>> estimate_heading(drive, now=1010).state 'moving' >>> parked = drive + [fix(1020, 50.001, 8.0, 0.0, 137)] >>> heading = estimate_heading(parked, now=1600) >>> heading.degrees, heading.state, heading.source, heading.parked_for (0.0, 'freshly_parked', 'bearing', 590) >>> estimate_heading(parked, now=1600, stale_after=300).state 'stale' >>> estimate_heading([], now=1600).state 'unknown'
- estimate_headings(rows: Sequence[Row], *, now: int | None = None, stale_after: int = 3600, min_speed: float = 2.0) dict[Any, Heading]¶
Estimate every tracker’s heading from mixed, time-ordered rows.
Implements A heading is estimated from... (REQ_HEADING_ESTIMATE).
Groups by
tracker_idfirst so another device’s fix never serves as a “previous position”. Keys follow first appearance.>>> rows = [ ... {"tracker_id": 1, "fix_at": 100, "latitude": 0.0, "longitude": 0.0, ... "speed": 5.0, "course": 45}, ... {"tracker_id": 2, "fix_at": 101, "latitude": 0.0, "longitude": 0.0, ... "speed": 0.0, "course": 200}, ... ] >>> {k: v.state for k, v in estimate_headings(rows, now=200).items()} {1: 'moving', 2: 'unknown'}
trackiwi.cli¶
Command-line interface.
Wires the client, the store and the exporters together and owns all user interaction. The password is read with getpass, never echoed, never logged and never written to disk.
- _write_atomically(path: str, text: str) None¶
Write text to path without ever leaving a truncated file behind.
Implements Export writes atomically (REQ_EXPORT_ATOMIC).
Renders to a temp file beside the target, then os.replace()`s it over. A symlink destination is refused (following it could overwrite an unrelated file, or copy the history into a world-readable target); only the owner bits of an existing target’s mode are kept, a new file is 0600; the temp file is removed on any failure, Ctrl-C included (#25). Any `OSError becomes a TrackiwiError.
- cmd_login(args: Namespace) int¶
Authenticate (or store a supplied token) and save the session.
- cmd_logout(_: Namespace) int¶
Revoke the session and remove local credentials.
Implements logout revokes server-side ... (REQ_LOGOUT_REVOKES): revokes server-side when it can and, when it cannot, still removes the local credentials and warns that the token may remain valid. Implements A stored token is always re... (REQ_TOKEN_REMOVABLE): a config file that cannot be loaded is deleted all the same.
- READ_COMMANDS = (('trackers', 'trackers', ('id', 'name'), 'list trackers'), ('tours', 'tours', ('id', 'name', 'tracker_id', 'started_at', 'ended_at'), 'list recorded tours'), ('alarms', 'alarms', ('id', 'tracker_id', 'alarm_type', 'acknowledged', 'inserted_at'), 'list alarms — WARNING: this is location history, see --help'), ('markers', 'markers', ('id', 'name'), 'list markers (field names unverified — see --help)'), ('marker-categories', 'marker_categories', ('id', 'name', 'color'), 'list marker categories'), ('shares', 'shares', ('id', 'name'), 'list active shares (field names unverified — see --help)'))¶
(subcommand, Client method, printed fields, help text).
None of these is cached; the cache holds positions only.
- READ_DESCRIPTIONS = {'alarms': "List the account's alarms, one per line: id, tracker, type, whether it was acknowledged, and when it was recorded.\n\nPRIVACY WARNING: an alarm list is itself a location history. Every alarm record carries an 'event' object embedding latitude and longitude, so this endpoint says where the vehicle was each time an alarm fired — which, for a theft or geofence alarm, is precisely the locations worth protecting. This command prints the fields above and deliberately does not print those coordinates, but the underlying records hold them, so treat the API response with the same care as the position cache.\n\nRead-only: nothing here acknowledges, clears or tests an alarm.", 'markers': "List the account's markers, one per line.\n\nThe element shape of this endpoint is UNVERIFIED: it answered with an empty list on the account it was checked against, so 'id' and 'name' are expectations rather than a confirmed contract. A field that does not exist prints as '-'.", 'shares': "List the account's active shares, one per line.\n\nThe element shape of this endpoint is UNVERIFIED: it answered with an empty list on the account it was checked against, so 'id' and 'name' are expectations rather than a confirmed contract. A field that does not exist prints as '-'.\n\nRead-only: this lists shares, it cannot create or revoke one. Anyone holding a share link can see the vehicle's position.", 'tours': "List recorded tours, one per line: id, name, tracker, start and end.\n\n'started_at' and 'ended_at' are printed exactly as the API sends them, which for this endpoint is an ISO 8601 string, not the epoch integer the sync CSV uses.", 'trackers': "List the account's trackers, one per line: id and name.\n\nPRIVACY WARNING: a tracker record holds location data: the alarm geofence (latitude, longitude, radius, usually where the vehicle is kept) and the latest fix. This command prints only id and name, but the underlying API response holds the rest, so treat it with the same care as the position cache."}¶
Longer –help text, where a warning has room to be read.
- cmd_sync(args: Namespace) int¶
Fetch new positions into the local cache, reporting progress.
- cmd_influx_check(_: Namespace) int¶
Probe the configured InfluxDB target and print a report. Writes nothing.
- cmd_influx_push(_: Namespace) int¶
Mirror cached positions not yet sent to the configured InfluxDB.
- cmd_ingest(args: Namespace) int¶
Sync from trackiwi, refresh tracker names, then push to InfluxDB.
The steps fail independently: the push runs even if sync or the name refresh failed. The first trackiwi-side error is re-raised at the end and decides the exit code, even when the push also fails.
- cmd_export(args: Namespace) int¶
Export cached positions to stdout or a file in the chosen format.
- _heading_line(tracker_id: object, heading: Heading) str¶
Render one tracker’s estimate as a tab-separated line.
Columns: tracker id, degrees (one decimal), state, source, last moved (UTC), parked seconds;
-when unknown. No coordinates.
- cmd_heading(args: Namespace) int¶
Print each tracker’s estimated heading and confidence state.
Implements A heading is estimated from... (REQ_HEADING_ESTIMATE) and The heading carries a confi... (REQ_HEADING_STATE).
Offline and never creates the cache.
--trackerwith no rows still prints anunknownline.
- cmd_purge(args: Namespace) int¶
Delete the local position cache, including SQLite sidecars.
Implements Purge deletes the cache eve... (REQ_PURGE_DELETES): unlinks the cache and its sidecars without opening the database, so a corrupt cache can still be removed.
- build_parser() ArgumentParser¶
Build the argparse parser for every subcommand.
- main(argv: list[str] | None = None) int¶
Dispatch a command and map the project’s exceptions to exit codes.
trackiwi.influx¶
Write cached positions to InfluxDB 2.x.
The only module that talks to InfluxDB. It never imports client.py, and
client.py never imports it. Configuration lives in
~/.config/trackiwi/influx.toml and every key can be overridden by an
environment variable named TRACKIWI_INFLUX_<KEY>. The token is never
stored in the TOML file: it comes from the variable named by token_env
(default TRACKIWI_INFLUX_TOKEN). InfluxDB 1.x is not supported; git holds
the 1.x path if it is ever needed again.
- default_influx_config_path() Path¶
Return the InfluxDB config path, honouring
XDG_CONFIG_HOME.
- class InfluxConfig(url: str, org: str | None, bucket: str | None, token: str | None, token_env: str)¶
A resolved InfluxDB 2.x target.
- url: str¶
- org: str | None¶
- bucket: str | None¶
- token: str | None¶
- token_env: str¶
- _read_file(path: Path) dict[str, Any]¶
Read the TOML file, narrowing a widened mode first (REQ_CONFIG_MODE_0600).
- load_config(path: Path | None = None, env: Mapping[str, str] | None = None) InfluxConfig¶
Resolve the InfluxDB target from the file and the environment.
Precedence: environment > file > default. Implements The token file is 0600 and ... (REQ_CONFIG_MODE_0600) for
influx.toml(see _read_file).
- class InfluxWriter(config: InfluxConfig, opener: Callable[[...], Any] | None = None)¶
Talks to one InfluxDB 2.x target: write and check.
Implements InfluxDB writes are bounded... (REQ_INFLUX_WRITE_SCOPE) and InfluxDB credentials are ne... (REQ_INFLUX_TOKEN_REDACT). Redirects are never followed, so a 3xx is an error, not an acknowledgement (The mirror never advances p... (REQ_MIRROR_RESUME)). opener lets tests inject a fake transport.
- target_key() str¶
Stable identity of this target for mirror bookkeeping; no secrets.
- write(lines: Sequence[str]) None¶
POST one gzipped batch of line protocol; raise unless acknowledged.
Acknowledged means 2xx, or a 4xx whose body says the points were “beyond retention policy” (dropped for good, reported on stderr).
- check() tuple[bool, list[str]]¶
Probe connectivity, auth and the bucket. Read-only: writes nothing.
- class WriterProtocol(*args, **kwargs)¶
Interface for a writer that mirror uses. Lets tests inject fake writers.
- target_key() str¶
Stable identity of this target for mirror bookkeeping.
- write(lines: Sequence[str]) None¶
POST one gzipped batch of line protocol; raise unless acknowledged.
- mirror(store: Store, writer: WriterProtocol, batch_size: int = 5000) int¶
Send every cached row not yet mirrored to the writer’s target.
Implements The mirror never advances p... (REQ_MIRROR_RESUME): the stored position advances only after a batch is acknowledged. Implements Re-sending positions never ... (REQ_MIRROR_IDEMPOTENT): a row always becomes the same point, so re-sending is safe; hence
tracker_nameis only the stored name, and a batch with an unnamed tracker raises TrackiwiError before sending. Returns the number of points sent; the first failed batch re-raises.
trackiwi.lineprotocol¶
Convert cached position rows to InfluxDB line protocol.
Pure: no I/O. Implements Positions are written to In... (REQ_LINEPROTOCOL_UNITS) — fields are written
in natural units (distance cm → m, voltage cV → V, per the units verified
against the live API) so every consumer reads metres and volts, and the two
columns trackiwi misnames are renamed for what they are (rssi is a 0–5
GNSS quality scale; fix_timezone is a 0/1 flag, not a timezone).
>>> row = {"id": 1, "tracker_id": 7, "fix_at": 1700000000, "fix_timezone": 1,
... "latitude": 31.0, "longitude": -41.0, "altitude": 12, "speed": 0.0,
... "course": 90, "distance": 150, "rssi": 5, "sat": 9, "battery": 99,
... "voltage": 1287}
>>> head, fields, timestamp = format_point(row, "Bus").split(" ")
>>> head
'trackiwi_position,tracker_id=7,tracker_name=Bus'
>>> fields.split(",")[:3]
['lat=31.0', 'lon=-41.0', 'altitude_m=12.0']
>>> "distance_m=1.5" in fields, "voltage_v=12.87" in fields, "battery_pct=99i" in fields
(True, True, True)
>>> timestamp
'1700000000'
- format_point(row: Row, tracker_name: str) str¶
Return one line-protocol line for a cached position row.
tracker_name becomes the
tracker_nametag and must not be empty (InfluxDB rejects an empty tag value; mirror never sends an unnamed tracker, Re-sending positions never ... (REQ_MIRROR_IDEMPOTENT)). NULL optional columns are omitted rather than written. RaisesValueErrorfor an empty name or a non-finite value.