@kquika-inc/trakt 1.0.0
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.
- package/README.md +163 -0
- package/dist/index.d.ts +191 -0
- package/dist/index.js +315 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Trakt SDK
|
|
2
|
+
|
|
3
|
+
Client libraries for the Trakt Integrations API: fleet predictions and
|
|
4
|
+
maintenance forecasts, in a few lines of code.
|
|
5
|
+
|
|
6
|
+
Trakt predicts which components on your fleet need attention, when, and what to
|
|
7
|
+
do about them. This SDK gives you that data directly, with authentication,
|
|
8
|
+
retries, and rate limits handled for you.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
**Python**
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install kquika-trakt # core
|
|
16
|
+
pip install "kquika-trakt[pandas]" # with DataFrame support
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The distribution is named `kquika-trakt` and the import is `trakt`. That
|
|
20
|
+
difference is normal: the package you install and the module you import do not
|
|
21
|
+
have to share a name.
|
|
22
|
+
|
|
23
|
+
**Node / TypeScript**
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install @kquika-inc/trakt
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Requires Node 18 or newer. TypeScript types are included.
|
|
30
|
+
|
|
31
|
+
## Quickstart
|
|
32
|
+
|
|
33
|
+
**Python**
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from trakt import Trakt
|
|
37
|
+
|
|
38
|
+
trakt = Trakt(token="YOUR_API_KEY") # or set TRAKT_TOKEN
|
|
39
|
+
|
|
40
|
+
# What can this token do?
|
|
41
|
+
cfg = trakt.config()
|
|
42
|
+
print(cfg.access_level, cfg.max_predictions, cfg.can_export)
|
|
43
|
+
|
|
44
|
+
# Per-component predictions, most urgent first
|
|
45
|
+
for c in trakt.components():
|
|
46
|
+
print(c.name, c.aircraft_tail_number, c.health,
|
|
47
|
+
c.failure_probability, c.recommended_action)
|
|
48
|
+
|
|
49
|
+
# The next 90 days of maintenance, soonest first
|
|
50
|
+
for item in trakt.forecast(days=90):
|
|
51
|
+
if item.is_overdue:
|
|
52
|
+
print("OVERDUE:", item.tail_number, item.component_name)
|
|
53
|
+
|
|
54
|
+
# Straight to a DataFrame for planning
|
|
55
|
+
df = trakt.components_dataframe()
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Node / TypeScript**
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { Trakt } from "@kquika/trakt";
|
|
62
|
+
|
|
63
|
+
const trakt = new Trakt({ token: process.env.TRAKT_TOKEN! });
|
|
64
|
+
|
|
65
|
+
const cfg = await trakt.config();
|
|
66
|
+
console.log(cfg.access_level, cfg.max_predictions);
|
|
67
|
+
|
|
68
|
+
// Per-component predictions, most urgent first
|
|
69
|
+
const components = await trakt.components();
|
|
70
|
+
for (const c of components) {
|
|
71
|
+
console.log(c.name, c.aircraft_tail_number, c.health, c.recommended_action);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// The next 90 days of maintenance, soonest first
|
|
75
|
+
const forecast = await trakt.forecast({ days: 90 });
|
|
76
|
+
for (const item of forecast) {
|
|
77
|
+
if (item.is_overdue) console.log("OVERDUE:", item.tail_number, item.component_name);
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Handling errors
|
|
82
|
+
|
|
83
|
+
Both clients raise typed errors, so you can catch the case you care about.
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from trakt import AuthenticationError, PermissionError_, RateLimitError
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
components = trakt.components()
|
|
90
|
+
except AuthenticationError:
|
|
91
|
+
print("The token was not accepted.")
|
|
92
|
+
except PermissionError_:
|
|
93
|
+
print("This token's access level does not cover that call.")
|
|
94
|
+
except RateLimitError:
|
|
95
|
+
print("Quota exhausted. The client already retried with backoff.")
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
import { AuthenticationError, PermissionError, RateLimitError } from "@kquika-inc/trakt";
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const components = await trakt.components();
|
|
103
|
+
} catch (e) {
|
|
104
|
+
if (e instanceof AuthenticationError) console.error("The token was not accepted.");
|
|
105
|
+
else if (e instanceof PermissionError) console.error("Access level does not cover that call.");
|
|
106
|
+
else if (e instanceof RateLimitError) console.error("Quota exhausted.");
|
|
107
|
+
else throw e;
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## What the clients do for you
|
|
112
|
+
|
|
113
|
+
- **Auth.** Set the token once; every request carries the bearer header.
|
|
114
|
+
- **Retries.** A rate limit or a transient server failure is retried with
|
|
115
|
+
exponential backoff and jitter, honoring `Retry-After` when it is sent. A
|
|
116
|
+
rejected token or a permission error is not retried, because retrying will not
|
|
117
|
+
fix it.
|
|
118
|
+
- **Typed errors.** `AuthenticationError`, `PermissionError`, `RateLimitError`,
|
|
119
|
+
`NotFoundError`, `ServerError`.
|
|
120
|
+
- **Flattened objects.** The nested prediction, survival, and maintenance blocks
|
|
121
|
+
are lifted onto the component, so `health` and `recommended_action` are one
|
|
122
|
+
attribute away.
|
|
123
|
+
- **Useful ordering.** Components come back most urgent first; forecast items
|
|
124
|
+
come back soonest first.
|
|
125
|
+
|
|
126
|
+
## Reading the fields
|
|
127
|
+
|
|
128
|
+
| Field | Meaning |
|
|
129
|
+
| --- | --- |
|
|
130
|
+
| `health` | 0 to 100, where higher is healthier. |
|
|
131
|
+
| `health_trend` | stable, declining, or critical. |
|
|
132
|
+
| `predicted_rul_hours` / `predicted_rul_days` | Remaining useful life before attention is due. |
|
|
133
|
+
| `failure_probability` | 0 to 1, the modeled chance of failure in the near term. |
|
|
134
|
+
| `recommended_action` | do_nothing, inspect, repair, or replace. |
|
|
135
|
+
| `priority` | The urgency band for planning. |
|
|
136
|
+
| `task_reference` | The task identifier from your source system, so a row ties back to its task. |
|
|
137
|
+
| `days_until_due` | Negative means the item is already past due. |
|
|
138
|
+
| `is_trained` | False means the prediction is provisional and refines after training. |
|
|
139
|
+
|
|
140
|
+
## Access levels
|
|
141
|
+
|
|
142
|
+
Your token is issued at one of three levels, which control both the endpoints it
|
|
143
|
+
can reach and how many records a request returns:
|
|
144
|
+
|
|
145
|
+
- `read_only` — read predictions and forecasts
|
|
146
|
+
- `read_write` — read, plus the write endpoints
|
|
147
|
+
- `full` — everything, including fleet export
|
|
148
|
+
|
|
149
|
+
A call outside your token's level raises a permission error. Call `config()` to
|
|
150
|
+
see the level and limits for your token rather than guessing.
|
|
151
|
+
|
|
152
|
+
## Need a client in another language?
|
|
153
|
+
|
|
154
|
+
The SDKs are built from an OpenAPI specification, so a client can be generated
|
|
155
|
+
for most languages. Contact us and we will provide the spec.
|
|
156
|
+
|
|
157
|
+
## Support
|
|
158
|
+
|
|
159
|
+
Questions, a token, or a raised limit: support@kquika.com
|
|
160
|
+
|
|
161
|
+
## License
|
|
162
|
+
|
|
163
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trakt Node / TypeScript SDK.
|
|
3
|
+
*
|
|
4
|
+
* A thin, typed client for the Trakt Integrations API. It handles
|
|
5
|
+
* authentication, retries with backoff on rate limits and transient failures,
|
|
6
|
+
* and gives you typed objects instead of raw JSON.
|
|
7
|
+
*
|
|
8
|
+
* Quickstart
|
|
9
|
+
* ----------
|
|
10
|
+
*
|
|
11
|
+
* import { Trakt } from "@kquika-inc/trakt";
|
|
12
|
+
*
|
|
13
|
+
* const trakt = new Trakt({ token: process.env.TRAKT_TOKEN! });
|
|
14
|
+
*
|
|
15
|
+
* const config = await trakt.config();
|
|
16
|
+
* console.log(config.access_level, config.max_predictions);
|
|
17
|
+
*
|
|
18
|
+
* const components = await trakt.components();
|
|
19
|
+
* for (const c of components) {
|
|
20
|
+
* console.log(c.name, c.aircraft_tail_number, c.health, c.failure_probability);
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* const forecast = await trakt.forecast({ days: 90 });
|
|
24
|
+
* for (const item of forecast) {
|
|
25
|
+
* console.log(item.days_until_due, item.tail_number, item.component_name);
|
|
26
|
+
* }
|
|
27
|
+
*
|
|
28
|
+
* Requires Node 18 or newer, which has a built-in fetch.
|
|
29
|
+
*/
|
|
30
|
+
export declare const VERSION = "1.0.0";
|
|
31
|
+
/** Base class for every error this SDK throws. */
|
|
32
|
+
export declare class TraktError extends Error {
|
|
33
|
+
constructor(message: string);
|
|
34
|
+
}
|
|
35
|
+
/** The token is missing, malformed, revoked, or expired (HTTP 401). */
|
|
36
|
+
export declare class AuthenticationError extends TraktError {
|
|
37
|
+
constructor(message: string);
|
|
38
|
+
}
|
|
39
|
+
/** The token is valid but its access level does not permit this call (403). */
|
|
40
|
+
export declare class PermissionError extends TraktError {
|
|
41
|
+
constructor(message: string);
|
|
42
|
+
}
|
|
43
|
+
/** The resource does not exist or is not visible to this token (404). */
|
|
44
|
+
export declare class NotFoundError extends TraktError {
|
|
45
|
+
constructor(message: string);
|
|
46
|
+
}
|
|
47
|
+
/** A daily or monthly quota for this token is exhausted (429). */
|
|
48
|
+
export declare class RateLimitError extends TraktError {
|
|
49
|
+
constructor(message: string);
|
|
50
|
+
}
|
|
51
|
+
/** Trakt returned a 5xx. The client retries these before giving up. */
|
|
52
|
+
export declare class ServerError extends TraktError {
|
|
53
|
+
constructor(message: string);
|
|
54
|
+
}
|
|
55
|
+
export type AccessLevel = "read_only" | "read_write" | "full";
|
|
56
|
+
export type Priority = "critical" | "high" | "medium" | "low";
|
|
57
|
+
export type Urgency = "critical" | "high" | "medium" | "normal";
|
|
58
|
+
/** What the calling token is permitted to do. */
|
|
59
|
+
export interface Config {
|
|
60
|
+
access_level?: AccessLevel;
|
|
61
|
+
allowed_endpoints: string[];
|
|
62
|
+
/** Maximum records a single request may return for this token. */
|
|
63
|
+
max_predictions?: number;
|
|
64
|
+
can_export: boolean;
|
|
65
|
+
raw: Record<string, unknown>;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* A component and the model's view of it. The nested prediction, survival, and
|
|
69
|
+
* maintenance values are flattened so the fields you want are one property away.
|
|
70
|
+
*/
|
|
71
|
+
export interface Component {
|
|
72
|
+
id?: number;
|
|
73
|
+
name?: string;
|
|
74
|
+
component_type?: string;
|
|
75
|
+
part_number?: string;
|
|
76
|
+
serial_number?: string;
|
|
77
|
+
external_id?: string;
|
|
78
|
+
/** The task identifier from the source system, so a row ties back to its task. */
|
|
79
|
+
task_reference?: string;
|
|
80
|
+
aircraft_id?: number;
|
|
81
|
+
aircraft_tail_number?: string;
|
|
82
|
+
manufacturer?: string;
|
|
83
|
+
unit_cost?: number;
|
|
84
|
+
lead_time_days?: number;
|
|
85
|
+
/** 0 to 100, where higher is healthier. */
|
|
86
|
+
health?: number;
|
|
87
|
+
health_trend?: string;
|
|
88
|
+
predicted_rul_hours?: number;
|
|
89
|
+
predicted_rul_days?: number;
|
|
90
|
+
/** 0 to 1, the modeled probability of failure in the near term. */
|
|
91
|
+
failure_probability?: number;
|
|
92
|
+
prediction_confidence?: number;
|
|
93
|
+
hazard_rate?: number;
|
|
94
|
+
/** One of do_nothing, inspect, repair, or replace. */
|
|
95
|
+
recommended_action?: string;
|
|
96
|
+
priority?: Priority;
|
|
97
|
+
/**
|
|
98
|
+
* True once a trained model has scored this component. When false the
|
|
99
|
+
* prediction is provisional and refines after the next training run.
|
|
100
|
+
*/
|
|
101
|
+
is_trained: boolean;
|
|
102
|
+
raw: Record<string, unknown>;
|
|
103
|
+
}
|
|
104
|
+
/** One item coming due in the forecast window. */
|
|
105
|
+
export interface ForecastItem {
|
|
106
|
+
kind?: string;
|
|
107
|
+
urgency?: Urgency;
|
|
108
|
+
tail_number?: string;
|
|
109
|
+
aircraft_type?: string;
|
|
110
|
+
component_name?: string;
|
|
111
|
+
component_type?: string;
|
|
112
|
+
part_number?: string;
|
|
113
|
+
task_reference?: string;
|
|
114
|
+
failure_probability?: number;
|
|
115
|
+
predicted_rul_hours?: number;
|
|
116
|
+
/** Negative when the item is already past due. */
|
|
117
|
+
days_until_due?: number;
|
|
118
|
+
suggested_action_date?: string;
|
|
119
|
+
suggested_duration_hours?: number;
|
|
120
|
+
/** True when the item is already past its due date. */
|
|
121
|
+
is_overdue: boolean;
|
|
122
|
+
raw: Record<string, unknown>;
|
|
123
|
+
}
|
|
124
|
+
export interface Aircraft {
|
|
125
|
+
id?: number;
|
|
126
|
+
tail_number?: string;
|
|
127
|
+
aircraft_type?: string;
|
|
128
|
+
manufacturer?: string;
|
|
129
|
+
model?: string;
|
|
130
|
+
total_flight_hours?: number;
|
|
131
|
+
total_flight_cycles?: number;
|
|
132
|
+
raw: Record<string, unknown>;
|
|
133
|
+
}
|
|
134
|
+
export interface TraktOptions {
|
|
135
|
+
/** Your API token. Falls back to the TRAKT_TOKEN environment variable. */
|
|
136
|
+
token?: string;
|
|
137
|
+
/** API base. Falls back to TRAKT_BASE_URL, then production. */
|
|
138
|
+
baseUrl?: string;
|
|
139
|
+
/** Per-request timeout in milliseconds. */
|
|
140
|
+
timeoutMs?: number;
|
|
141
|
+
/** How many times to retry a rate limit or a transient failure. */
|
|
142
|
+
maxRetries?: number;
|
|
143
|
+
/** Supply your own fetch (for tests, or a proxied agent). */
|
|
144
|
+
fetch?: typeof fetch;
|
|
145
|
+
}
|
|
146
|
+
export interface ComponentQuery {
|
|
147
|
+
aircraft_id?: number;
|
|
148
|
+
component_type?: string;
|
|
149
|
+
priority?: Priority;
|
|
150
|
+
currency?: string;
|
|
151
|
+
}
|
|
152
|
+
export interface ForecastQuery {
|
|
153
|
+
days?: number;
|
|
154
|
+
max_items?: number;
|
|
155
|
+
start_date?: string;
|
|
156
|
+
fleet_type?: string;
|
|
157
|
+
station?: string;
|
|
158
|
+
fp_threshold?: number;
|
|
159
|
+
}
|
|
160
|
+
export declare class Trakt {
|
|
161
|
+
private readonly token;
|
|
162
|
+
private readonly baseUrl;
|
|
163
|
+
private readonly timeoutMs;
|
|
164
|
+
private readonly maxRetries;
|
|
165
|
+
private readonly fetchImpl;
|
|
166
|
+
constructor(options?: TraktOptions);
|
|
167
|
+
private request;
|
|
168
|
+
private errorMessage;
|
|
169
|
+
private sleep;
|
|
170
|
+
/** What this token is permitted to do. A good first call to check a token. */
|
|
171
|
+
config(): Promise<Config>;
|
|
172
|
+
/**
|
|
173
|
+
* Per-component predictions, ordered with the most urgent first.
|
|
174
|
+
* The number of records is capped by the token's max_predictions limit.
|
|
175
|
+
*/
|
|
176
|
+
components(query?: ComponentQuery): Promise<Component[]>;
|
|
177
|
+
/**
|
|
178
|
+
* Upcoming maintenance over a window, soonest first.
|
|
179
|
+
* A negative days_until_due means the item is already past due.
|
|
180
|
+
*/
|
|
181
|
+
forecast(query?: ForecastQuery): Promise<ForecastItem[]>;
|
|
182
|
+
/** Aircraft in the fleet. */
|
|
183
|
+
aircraft(): Promise<Aircraft[]>;
|
|
184
|
+
/** A full fleet snapshot. Requires a token whose level permits export. */
|
|
185
|
+
fleetExport(): Promise<Record<string, unknown>>;
|
|
186
|
+
/** Planning scenarios. */
|
|
187
|
+
scenarios(): Promise<Array<Record<string, unknown>>>;
|
|
188
|
+
/** Create a planning scenario. Requires read_write or full. */
|
|
189
|
+
createScenario(name: string, description?: string): Promise<Record<string, unknown>>;
|
|
190
|
+
}
|
|
191
|
+
export default Trakt;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trakt Node / TypeScript SDK.
|
|
3
|
+
*
|
|
4
|
+
* A thin, typed client for the Trakt Integrations API. It handles
|
|
5
|
+
* authentication, retries with backoff on rate limits and transient failures,
|
|
6
|
+
* and gives you typed objects instead of raw JSON.
|
|
7
|
+
*
|
|
8
|
+
* Quickstart
|
|
9
|
+
* ----------
|
|
10
|
+
*
|
|
11
|
+
* import { Trakt } from "@kquika-inc/trakt";
|
|
12
|
+
*
|
|
13
|
+
* const trakt = new Trakt({ token: process.env.TRAKT_TOKEN! });
|
|
14
|
+
*
|
|
15
|
+
* const config = await trakt.config();
|
|
16
|
+
* console.log(config.access_level, config.max_predictions);
|
|
17
|
+
*
|
|
18
|
+
* const components = await trakt.components();
|
|
19
|
+
* for (const c of components) {
|
|
20
|
+
* console.log(c.name, c.aircraft_tail_number, c.health, c.failure_probability);
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* const forecast = await trakt.forecast({ days: 90 });
|
|
24
|
+
* for (const item of forecast) {
|
|
25
|
+
* console.log(item.days_until_due, item.tail_number, item.component_name);
|
|
26
|
+
* }
|
|
27
|
+
*
|
|
28
|
+
* Requires Node 18 or newer, which has a built-in fetch.
|
|
29
|
+
*/
|
|
30
|
+
export const VERSION = "1.0.0";
|
|
31
|
+
const DEFAULT_BASE_URL = "https://trakt.tech";
|
|
32
|
+
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
33
|
+
const DEFAULT_MAX_RETRIES = 4;
|
|
34
|
+
/* -------------------------------------------------------------------------- */
|
|
35
|
+
/* Errors */
|
|
36
|
+
/* -------------------------------------------------------------------------- */
|
|
37
|
+
/** Base class for every error this SDK throws. */
|
|
38
|
+
export class TraktError extends Error {
|
|
39
|
+
constructor(message) {
|
|
40
|
+
super(message);
|
|
41
|
+
this.name = "TraktError";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** The token is missing, malformed, revoked, or expired (HTTP 401). */
|
|
45
|
+
export class AuthenticationError extends TraktError {
|
|
46
|
+
constructor(message) {
|
|
47
|
+
super(message);
|
|
48
|
+
this.name = "AuthenticationError";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** The token is valid but its access level does not permit this call (403). */
|
|
52
|
+
export class PermissionError extends TraktError {
|
|
53
|
+
constructor(message) {
|
|
54
|
+
super(message);
|
|
55
|
+
this.name = "PermissionError";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** The resource does not exist or is not visible to this token (404). */
|
|
59
|
+
export class NotFoundError extends TraktError {
|
|
60
|
+
constructor(message) {
|
|
61
|
+
super(message);
|
|
62
|
+
this.name = "NotFoundError";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** A daily or monthly quota for this token is exhausted (429). */
|
|
66
|
+
export class RateLimitError extends TraktError {
|
|
67
|
+
constructor(message) {
|
|
68
|
+
super(message);
|
|
69
|
+
this.name = "RateLimitError";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** Trakt returned a 5xx. The client retries these before giving up. */
|
|
73
|
+
export class ServerError extends TraktError {
|
|
74
|
+
constructor(message) {
|
|
75
|
+
super(message);
|
|
76
|
+
this.name = "ServerError";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/* -------------------------------------------------------------------------- */
|
|
80
|
+
/* Client */
|
|
81
|
+
/* -------------------------------------------------------------------------- */
|
|
82
|
+
export class Trakt {
|
|
83
|
+
token;
|
|
84
|
+
baseUrl;
|
|
85
|
+
timeoutMs;
|
|
86
|
+
maxRetries;
|
|
87
|
+
fetchImpl;
|
|
88
|
+
constructor(options = {}) {
|
|
89
|
+
const token = options.token ?? process.env.TRAKT_TOKEN;
|
|
90
|
+
if (!token) {
|
|
91
|
+
throw new TraktError("No API token. Pass { token } or set the TRAKT_TOKEN environment variable.");
|
|
92
|
+
}
|
|
93
|
+
this.token = token;
|
|
94
|
+
this.baseUrl = (options.baseUrl ?? process.env.TRAKT_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
95
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
96
|
+
this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
97
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
98
|
+
if (!this.fetchImpl) {
|
|
99
|
+
throw new TraktError("No fetch available. Use Node 18 or newer, or pass your own fetch in options.");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/* -- transport ---------------------------------------------------------- */
|
|
103
|
+
async request(method, path, opts = {}) {
|
|
104
|
+
const url = new URL(this.baseUrl + path);
|
|
105
|
+
for (const [k, v] of Object.entries(opts.query ?? {})) {
|
|
106
|
+
if (v !== undefined && v !== null)
|
|
107
|
+
url.searchParams.set(k, String(v));
|
|
108
|
+
}
|
|
109
|
+
const headers = {
|
|
110
|
+
Authorization: `Bearer ${this.token}`,
|
|
111
|
+
Accept: "application/json",
|
|
112
|
+
"User-Agent": `trakt-node/${VERSION}`,
|
|
113
|
+
};
|
|
114
|
+
if (opts.body !== undefined)
|
|
115
|
+
headers["Content-Type"] = "application/json";
|
|
116
|
+
let lastError = null;
|
|
117
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
118
|
+
const controller = new AbortController();
|
|
119
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
120
|
+
let resp;
|
|
121
|
+
try {
|
|
122
|
+
resp = await this.fetchImpl(url.toString(), {
|
|
123
|
+
method,
|
|
124
|
+
headers,
|
|
125
|
+
body: opts.body === undefined ? undefined : JSON.stringify(opts.body),
|
|
126
|
+
signal: controller.signal,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
clearTimeout(timer);
|
|
131
|
+
lastError = e;
|
|
132
|
+
if (attempt < this.maxRetries) {
|
|
133
|
+
await this.sleep(attempt, null);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
throw new TraktError(`Could not reach Trakt at ${url.toString()}: ${String(e)}`);
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
clearTimeout(timer);
|
|
140
|
+
}
|
|
141
|
+
if (resp.status === 200) {
|
|
142
|
+
try {
|
|
143
|
+
return (await resp.json());
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
throw new TraktError(`Trakt returned a non-JSON body (HTTP 200) for ${path}.`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Errors that retrying will not fix.
|
|
150
|
+
if (resp.status === 401) {
|
|
151
|
+
throw new AuthenticationError(await this.errorMessage(resp, "The token was not accepted."));
|
|
152
|
+
}
|
|
153
|
+
if (resp.status === 403) {
|
|
154
|
+
throw new PermissionError(await this.errorMessage(resp, "This token's access level does not permit that call."));
|
|
155
|
+
}
|
|
156
|
+
if (resp.status === 404) {
|
|
157
|
+
throw new NotFoundError(await this.errorMessage(resp, "Not found."));
|
|
158
|
+
}
|
|
159
|
+
// Retryable.
|
|
160
|
+
if (resp.status === 429) {
|
|
161
|
+
if (attempt < this.maxRetries) {
|
|
162
|
+
await this.sleep(attempt, resp.headers.get("Retry-After"));
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
throw new RateLimitError(await this.errorMessage(resp, "Rate limit exceeded for this token."));
|
|
166
|
+
}
|
|
167
|
+
if (resp.status >= 500 && resp.status < 600) {
|
|
168
|
+
if (attempt < this.maxRetries) {
|
|
169
|
+
await this.sleep(attempt, null);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
throw new ServerError(await this.errorMessage(resp, `Trakt returned HTTP ${resp.status}.`));
|
|
173
|
+
}
|
|
174
|
+
throw new TraktError(await this.errorMessage(resp, `Unexpected response (HTTP ${resp.status}).`));
|
|
175
|
+
}
|
|
176
|
+
throw new TraktError(`Request failed: ${String(lastError)}`);
|
|
177
|
+
}
|
|
178
|
+
async errorMessage(resp, fallback) {
|
|
179
|
+
try {
|
|
180
|
+
const body = (await resp.json());
|
|
181
|
+
if (body && typeof body.error === "string" && body.error)
|
|
182
|
+
return body.error;
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
// fall through
|
|
186
|
+
}
|
|
187
|
+
return fallback;
|
|
188
|
+
}
|
|
189
|
+
sleep(attempt, retryAfter) {
|
|
190
|
+
let delayMs;
|
|
191
|
+
const parsed = retryAfter ? Number(retryAfter) : NaN;
|
|
192
|
+
if (!Number.isNaN(parsed) && parsed > 0) {
|
|
193
|
+
delayMs = Math.min(parsed * 1000, 60_000);
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
// Exponential backoff with jitter, capped.
|
|
197
|
+
delayMs = Math.min(2 ** attempt, 30) * 1000 + Math.random() * 500;
|
|
198
|
+
}
|
|
199
|
+
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
200
|
+
}
|
|
201
|
+
/* -- endpoints ---------------------------------------------------------- */
|
|
202
|
+
/** What this token is permitted to do. A good first call to check a token. */
|
|
203
|
+
async config() {
|
|
204
|
+
const d = await this.request("GET", "/api/integrations/config");
|
|
205
|
+
return {
|
|
206
|
+
access_level: d.access_level,
|
|
207
|
+
allowed_endpoints: d.allowed_endpoints ?? [],
|
|
208
|
+
max_predictions: d.max_predictions,
|
|
209
|
+
can_export: Boolean(d.can_export),
|
|
210
|
+
raw: d,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Per-component predictions, ordered with the most urgent first.
|
|
215
|
+
* The number of records is capped by the token's max_predictions limit.
|
|
216
|
+
*/
|
|
217
|
+
async components(query = {}) {
|
|
218
|
+
const d = await this.request("GET", "/api/integrations/components/extended", { query: { currency: "USD", ...query } });
|
|
219
|
+
const items = (d.components ?? []).map((c) => {
|
|
220
|
+
const preds = c.predictions ?? {};
|
|
221
|
+
const surv = c.survival_analysis ?? {};
|
|
222
|
+
const maint = c.maintenance ?? {};
|
|
223
|
+
return {
|
|
224
|
+
id: c.id,
|
|
225
|
+
name: c.name,
|
|
226
|
+
component_type: c.component_type,
|
|
227
|
+
part_number: c.part_number,
|
|
228
|
+
serial_number: c.serial_number,
|
|
229
|
+
external_id: c.external_id,
|
|
230
|
+
task_reference: c.task_reference,
|
|
231
|
+
aircraft_id: c.aircraft_id,
|
|
232
|
+
aircraft_tail_number: c.aircraft_tail_number,
|
|
233
|
+
manufacturer: c.manufacturer,
|
|
234
|
+
unit_cost: c.unit_cost,
|
|
235
|
+
lead_time_days: c.lead_time_days,
|
|
236
|
+
health: preds.current_health_score,
|
|
237
|
+
health_trend: preds.health_trend,
|
|
238
|
+
predicted_rul_hours: preds.predicted_rul_hours,
|
|
239
|
+
predicted_rul_days: preds.predicted_rul_days,
|
|
240
|
+
failure_probability: preds.failure_probability,
|
|
241
|
+
prediction_confidence: preds.prediction_confidence,
|
|
242
|
+
hazard_rate: surv.hazard_rate,
|
|
243
|
+
recommended_action: c.recommended_action ?? c.optimal_action ?? maint.optimal_action,
|
|
244
|
+
priority: c.priority ?? maint.priority,
|
|
245
|
+
is_trained: preds.prediction_confidence !== null && preds.prediction_confidence !== undefined,
|
|
246
|
+
raw: c,
|
|
247
|
+
};
|
|
248
|
+
});
|
|
249
|
+
items.sort((a, b) => {
|
|
250
|
+
const fpA = a.failure_probability ?? 0;
|
|
251
|
+
const fpB = b.failure_probability ?? 0;
|
|
252
|
+
if (fpA !== fpB)
|
|
253
|
+
return fpB - fpA;
|
|
254
|
+
return (a.health ?? 100) - (b.health ?? 100);
|
|
255
|
+
});
|
|
256
|
+
return items;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Upcoming maintenance over a window, soonest first.
|
|
260
|
+
* A negative days_until_due means the item is already past due.
|
|
261
|
+
*/
|
|
262
|
+
async forecast(query = {}) {
|
|
263
|
+
const d = await this.request("GET", "/api/integrations/planner/forecast", { query: { days: 30, max_items: 100, ...query } });
|
|
264
|
+
const items = (d.items ?? []).map((i) => ({
|
|
265
|
+
kind: i.kind,
|
|
266
|
+
urgency: i.urgency,
|
|
267
|
+
tail_number: i.tail_number,
|
|
268
|
+
aircraft_type: i.aircraft_type,
|
|
269
|
+
component_name: i.component_name,
|
|
270
|
+
component_type: i.component_type,
|
|
271
|
+
part_number: i.part_number,
|
|
272
|
+
task_reference: i.task_reference,
|
|
273
|
+
failure_probability: i.failure_probability,
|
|
274
|
+
predicted_rul_hours: i.predicted_rul_hours,
|
|
275
|
+
days_until_due: i.days_until_due,
|
|
276
|
+
suggested_action_date: i.suggested_action_date,
|
|
277
|
+
suggested_duration_hours: i.suggested_duration_hours,
|
|
278
|
+
is_overdue: typeof i.days_until_due === "number" && i.days_until_due < 0,
|
|
279
|
+
raw: i,
|
|
280
|
+
}));
|
|
281
|
+
items.sort((a, b) => (a.days_until_due ?? 1_000_000) - (b.days_until_due ?? 1_000_000));
|
|
282
|
+
return items;
|
|
283
|
+
}
|
|
284
|
+
/** Aircraft in the fleet. */
|
|
285
|
+
async aircraft() {
|
|
286
|
+
const d = await this.request("GET", "/api/integrations/aircraft/extended");
|
|
287
|
+
return (d.aircraft ?? []).map((a) => ({
|
|
288
|
+
id: a.id,
|
|
289
|
+
tail_number: a.tail_number,
|
|
290
|
+
aircraft_type: a.aircraft_type,
|
|
291
|
+
manufacturer: a.manufacturer,
|
|
292
|
+
model: a.model,
|
|
293
|
+
total_flight_hours: a.total_flight_hours,
|
|
294
|
+
total_flight_cycles: a.total_flight_cycles,
|
|
295
|
+
raw: a,
|
|
296
|
+
}));
|
|
297
|
+
}
|
|
298
|
+
/** A full fleet snapshot. Requires a token whose level permits export. */
|
|
299
|
+
async fleetExport() {
|
|
300
|
+
return this.request("GET", "/api/integrations/fleet/export");
|
|
301
|
+
}
|
|
302
|
+
/** Planning scenarios. */
|
|
303
|
+
async scenarios() {
|
|
304
|
+
const d = await this.request("GET", "/api/integrations/scenarios");
|
|
305
|
+
return d.scenarios ?? [];
|
|
306
|
+
}
|
|
307
|
+
/** Create a planning scenario. Requires read_write or full. */
|
|
308
|
+
async createScenario(name, description) {
|
|
309
|
+
const d = await this.request("POST", "/api/integrations/scenarios", {
|
|
310
|
+
body: description ? { name, description } : { name },
|
|
311
|
+
});
|
|
312
|
+
return d.scenario ?? {};
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
export default Trakt;
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kquika-inc/trakt",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Trakt System Integrations API client: fleet predictions and maintenance forecasts.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=18"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc",
|
|
24
|
+
"prepublishOnly": "npm run build"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"typescript": "^5.4.0",
|
|
28
|
+
"@types/node": "^20.0.0"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"trakt",
|
|
32
|
+
"predictive-maintenance",
|
|
33
|
+
"aviation",
|
|
34
|
+
"mro",
|
|
35
|
+
"api-client"
|
|
36
|
+
]
|
|
37
|
+
}
|