npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@kquika-inc/sansai

v1.0.4

Published

SANS.AI Platform API client: airport operations data, passenger flow, airport management, and artificial intelligence predictions.

Downloads

215

Readme

SANS.AI Platform SDK

Client libraries for the SANS.AI Platform API: live and historical airport operations data, passenger flow, and artificial intelligence predictions, in a few lines of code.

SANS.AI (Smart Airport Navigation System) applies artificial intelligence to live airport data and turns it into operational decisions. This SDK gives you that data and those predictions directly, with authentication, retries, and rate limits handled for you.

Full endpoint reference, including request and response schemas for every route: https://www.sans-ai.tech/documentation

Install

Python

pip install kquika-sansai              # core
pip install "kquika-sansai[pandas]"    # with DataFrame support

The distribution is named kquika-sansai and the import is sansai. That difference is normal: the package you install and the module you import do not have to share a name.

Node / TypeScript

npm install @kquika-inc/sansai

Requires Node 18 or newer. TypeScript types are included.

Quickstart

Python

import datetime
from sansai import SansAI

sansai = SansAI(api_key="YOUR_API_KEY")   # or set SANSAI_API_KEY

# Live aircraft states for an airport
for flight in sansai.live_flights(airport="LHR"):
    print(flight.flight_number, flight.status, flight.delay_minutes)

# Historical movements over a window
history = sansai.flight_history(
    airport="LHR",
    start_date=datetime.date(2026, 8, 1),
    end_date=datetime.date(2026, 8, 29),
)

# Where passengers are backing up right now
for zone in sansai.lidar_bottlenecks(terminal="T5"):
    print(zone.name, zone.severity, zone.dwell_minutes)

# Straight to a DataFrame for planning
df = sansai.flight_history_dataframe(airport="LHR")

# Delay prediction from a machine learning model trained on your operation
prediction = sansai.predict_delay(
    scheduled_departure="2026-09-01T14:30:00Z",
    passenger_count=180,
    weather_origin={"visibility": 3, "wind_speed": 22},
)
print(prediction.delay_probability, prediction.predicted_delay_minutes)

# Book a maintenance window
sansai.schedule_maintenance(
    system="Baggage Handling",
    title="Belt replacement",
    scheduled_date="2026-09-10T02:00:00Z",
    duration=4,
)

Node / TypeScript

import { SansAI, type Flight } from "@kquika-inc/sansai";

const sansai = new SansAI({ apiKey: process.env.SANSAI_API_KEY! });

// Live aircraft states for an airport
const flights = await sansai.liveFlights({ airport: "LHR" });
for (const f of flights) {
  console.log(f.flight_number, f.status, f.delay_minutes);
}

// Where passengers are backing up right now
const zones = await sansai.lidarBottlenecks({ terminal: "T5" });
for (const z of zones) console.log(z.name, z.severity, z.dwell_minutes);

// Delay prediction from a machine learning model trained on your operation
const prediction = await sansai.predictDelay({
  scheduledDeparture: "2026-09-01T14:30:00Z",
  passengerCount: 180,
  weatherOrigin: { visibility: 3, wind_speed: 22 },
});
console.log(prediction.delay_probability, prediction.predicted_delay_minutes);

// Book a maintenance window
await sansai.scheduleMaintenance({
  system: "Baggage Handling",
  title: "Belt replacement",
  scheduledDate: "2026-09-10T02:00:00Z",
  duration: 4,
});

Authentication

Two methods are supported. Both are first class.

API key. Create one in the dashboard and set it once.

sansai = SansAI(api_key="YOUR_API_KEY")

OAuth 2.0 client credentials. For server to server integration and third party access. The client exchanges credentials for a token, sends it as a bearer header, and refreshes before expiry without you doing anything.

sansai = SansAI(
    client_id="YOUR_CLIENT_ID",
    client_secret="YOUR_CLIENT_SECRET",
    scope="read:flights read:lidar",
)
const sansai = new SansAI({
  clientId: process.env.SANSAI_CLIENT_ID!,
  clientSecret: process.env.SANSAI_CLIENT_SECRET!,
  scope: "read:flights read:lidar",
});

Tokens live for one hour. Omit scope to receive every scope your client holds. Requesting a scope the client was not granted fails at token issue rather than at the first call, so a misconfiguration surfaces immediately.

Call sansai.token_info() to see the scopes and expiry on the current token rather than guessing.

Full endpoint coverage

Both clients cover every publicly callable endpoint. The table maps each method to the route it calls and the OAuth scope it needs, so a spec or a server log line ties back to a call site.

| Method (Python / Node) | Endpoint | Scope | | --- | --- | --- | | get_entitlements / getEntitlements | GET /api/v1/entitlements | read:account | | get_health / getHealth | GET /api/v1/health | none | | get_usage_stats / getUsageStats | GET /api/v1/usage-stats | read:account | | introspect_access_token / introspectAccessToken | POST /api/v1/oauth/introspect | none | | revoke_access_token / revokeAccessToken | POST /api/v1/oauth/revoke | none | | list_scopes / listScopes | GET /api/v1/oauth/scopes | none | | issue_access_token / issueAccessToken | POST /api/v1/oauth/token | none | | list_kpi_targets / listKpiTargets | GET /api/v1/frms/kpi-targets | read:frms | | set_kpi_target / setKpiTarget | POST /api/v1/frms/kpi-targets | write:frms | | get_frms_kpis / getFrmsKpis | GET /api/v1/frms/kpis | read:frms | | list_frms_models / listFrmsModels | GET /api/v1/frms/models | read:frms | | predict_turnaround / predictTurnaround | POST /api/v1/frms/predict | write:frms | | queue_turnaround_batch / queueTurnaroundBatch | POST /api/v1/frms/predict/batch | write:frms | | record_allocation_decision / recordAllocationDecision | POST /api/v1/frms/predictions/decide | write:frms | | verify_predictions / verifyPredictions | POST /api/v1/frms/predictions/verify | write:frms | | get_active_rules / getActiveRules | GET /api/v1/frms/rules/active/{airport} | read:frms-rules | | get_rule_effect / getRuleEffect | GET /api/v1/frms/rules/effect | read:frms-rules | | list_local_rules / listLocalRules | GET /api/v1/frms/rules/local | read:frms-rules | | set_local_rule / setLocalRule | POST /api/v1/frms/rules/local | write:frms-rules | | list_rule_sets / listRuleSets | GET /api/v1/frms/rules/sets | read:frms-rules | | get_schedule_config / getScheduleConfig | GET /api/v1/frms/schedule/config/{airport} | read:frms-schedule | | set_schedule_config / setScheduleConfig | POST /api/v1/frms/schedule/config/{airport} | write:frms-schedule | | preview_schedule / previewSchedule | GET /api/v1/frms/schedule/preview/{airport} | read:frms-schedule | | list_frms_training_jobs / listFrmsTrainingJobs | GET /api/v1/frms/train/jobs | read:frms-training | | queue_frms_training / queueFrmsTraining | POST /api/v1/frms/train/queue | write:frms-training | | check_training_readiness / checkTrainingReadiness | POST /api/v1/frms/training/readiness | read:frms-training | | get_airport_flights / getAirportFlights | GET /api/v1/airports/{airportCode}/flights | read:flights | | get_flight_analytics / getFlightAnalytics | GET /api/v1/flights/analytics | read:flights | | list_daily_flight_summaries / listDailyFlightSummaries | GET /api/v1/flights/daily-summary | read:flights | | list_flight_history / listFlightHistory | GET /api/v1/flights/history | read:flights | | list_live_flights / listLiveFlights | GET /api/v1/flights/live | read:flights | | get_flight_statistics / getFlightStatistics | GET /api/v1/flights/statistics | read:flights | | get_lidar_bottlenecks / getLidarBottlenecks | GET /api/v1/lidar/bottlenecks | read:lidar | | get_lidar_heatmap / getLidarHeatmap | GET /api/v1/lidar/heatmap | read:lidar | | get_lidar_historical / getLidarHistorical | GET /api/v1/lidar/historical | read:lidar | | get_lidar_real_time / getLidarRealTime | GET /api/v1/lidar/real-time | read:lidar | | list_maintenance_reports / listMaintenanceReports | GET /api/v1/maintenance/reports | read:maintenance | | schedule_maintenance / scheduleMaintenance | POST /api/v1/maintenance/schedule | write:maintenance | | list_custom_models / listCustomModels | GET /api/v1/custom-models | read:models | | train_custom_model / trainCustomModel | POST /api/v1/custom-models/train | write:models | | list_training_jobs / listTrainingJobs | GET /api/v1/custom-models/training | read:models | | run_custom_model / runCustomModel | POST /api/v1/custom-models/{modelId}/run | write:models | | list_models / listModels | GET /api/v1/ml/models | read:models | | run_model_prediction / runModelPrediction | POST /api/v1/models/{modelId}/predict | write:models | | predict_flight_delay / predictFlightDelay | POST /api/v1/models/predict/delay | read:predictions | | predict_maintenance / predictMaintenance | POST /api/v1/models/predict/maintenance | read:predictions | | get_current_resource_utilization / getCurrentResourceUtilization | GET /api/v1/resource-optimization/resource-utilization | read:resources | | list_resource_history / listResourceHistory | GET /api/v1/resources/history | read:resources | | list_safety_alerts / listSafetyAlerts | GET /api/v1/safety/alerts | read:safety | | acknowledge_safety_alert / acknowledgeSafetyAlert | POST /api/v1/safety/alerts/{alertId}/acknowledge | read:safety | | get_safety_analytics / getSafetyAnalytics | GET /api/v1/safety/analytics | read:safety | | list_safety_history / listSafetyHistory | GET /api/v1/safety/history | read:safety | | list_safety_metrics / listSafetyMetrics | GET /api/v1/safety/metrics | read:safety | | list_weather_history / listWeatherHistory | GET /api/v1/weather/history | read:weather |

Handling errors

Both clients raise typed errors, so you can catch the case you care about.

from sansai import (
    AuthenticationError,
    PermissionError_,
    InsufficientScopeError,
    QuotaExceededError,
    RateLimitError,
    NoDataError,
)

try:
    flights = sansai.live_flights(airport="LHR")
except AuthenticationError:
    print("The credential was not accepted.")
except InsufficientScopeError as e:
    print(f"Token is missing the {e.required_scope} scope.")
except PermissionError_:
    print("Your plan does not include this feature.")
except RateLimitError:
    print("Rate limited. The client already retried with backoff.")
except QuotaExceededError:
    print("Monthly plan allowance used. Retrying will not help until next month.")
except NoDataError:
    print("No stored records or trained model for this account.")
import {
  AuthenticationError,
  PermissionError,
  InsufficientScopeError,
  QuotaExceededError,
  RateLimitError,
  NoDataError,
} from "@kquika-inc/sansai";

try {
  const flights = await sansai.liveFlights({ airport: "LHR" });
} catch (e) {
  if (e instanceof AuthenticationError) console.error("Credential not accepted.");
  else if (e instanceof InsufficientScopeError) console.error(`Missing scope: ${e.requiredScope}`);
  else if (e instanceof QuotaExceededError) console.error("Monthly allowance used.");
  else if (e instanceof RateLimitError) console.error("Rate limited.");
  else throw e;
}

RateLimitError and QuotaExceededError are deliberately separate. The first clears within the window and is worth retrying. The second does not clear until the next calendar month, so a client that treats them alike will retry for weeks.

What the clients do for you

  • Auth. Set an API key or client credentials once; every request carries the right header. OAuth tokens are refreshed before expiry without a round trip from you.
  • Retries. A rate limit or a transient server failure is retried with exponential backoff and jitter, honoring Retry-After when it is sent. A rejected credential, an insufficient scope, or an exhausted monthly allowance is not retried, because retrying will not fix it.
  • Typed errors. AuthenticationError, PermissionError, InsufficientScopeError, RateLimitError, QuotaExceededError, NoDataError, NotFoundError, ServerError.
  • Flattened objects. Nested filter, meta, and prediction blocks are lifted onto the result, so delay_minutes and remaining_this_month are one attribute away.
  • Useful ordering. Live flights come back nearest first; safety alerts come back most severe first; bottlenecks come back worst first.
  • Named arguments, not payload dictionaries. Request bodies are flattened into keyword arguments, so a required field that is missing is a TypeError at the call site rather than a 400 from the server. Pass body= alongside them when you need to send a field the schema does not name yet.
  • Real types, not any. Every parameter, body field, and response is typed from the API specification. priority is 'low' | 'medium' | 'high' | 'critical', not string. flightHistory returns Result<Flight>, so delay_minutes completes in your editor and a typo fails tsc rather than production.

Artificial intelligence

Three of the endpoint groups are model-backed rather than lookups.

Delay prediction scores a departure against a model trained on your own movements, weather, and stand data. Maintenance prediction scores tracked systems for upcoming risk. Turnaround prediction scores resource needs per movement against the FRMS models. Custom models lets you train on your own dataset, poll the training job, and call the result through the same client.

job = sansai.train_model(dataset_id=42, target="delay_minutes")

for candidate in sansai.training_jobs():
    print(candidate.status, candidate.progress)

result = sansai.run_custom_model(job.model_id, features={"gate": "B22"})

The models learn your airport's patterns rather than an industry average, which is why a new account gets a 404 rather than a generic number. Nothing is inferred from another operator's data.

Predictions carry their own confidence, and the model that produced them is named in the response, so an output can always be traced back to a version:

prediction = sansai.predict_delay(scheduled_departure="2026-09-01T14:30:00Z")
print(prediction.delay_probability, prediction.confidence, prediction.model)

Every 200 from these endpoints is model output. Where no trained model exists you get NoDataError instead of an estimate, so a prediction in your system is never a placeholder.

Safety alerts and their evidence

Every alert carries how it came about and what was decided about it. Provenance is set when the alert is created and never edited.

alerts = sansai.safety_alerts(severity="critical")

for alert in alerts:
    print(alert.provenance.detection_method, alert.provenance.model_version)
    print(alert.disposition.state, alert.disposition.acknowledged_at)

sansai.acknowledge_alert(
    alerts[0].id,
    disposition="false_positive",
    note="Vehicle had clearance, radio log confirms",
)

Dispositions are actioned, stood_down, false_positive, duplicate and referred. The last three resolve the alert; the first two leave it open.

Alerts with detection_method of sample are demonstration content shown to accounts with no safety data yet. They are never persisted and cannot be dispositioned.

Turnaround and resource allocation

The FRMS endpoints cover the allocation loop end to end: predict what a turnaround needs, record what the planner actually did, and verify the two against each other so the next training run learns from the difference.

predictions = sansai.predict_turnaround(body={"airport": "LHR", "date": "2026-09-01"})

sansai.record_decision(body={"prediction_id": 812, "accepted": True})

sansai.verify_predictions(body={"start_date": "2026-08-01", "end_date": "2026-08-29"})

Resource rules come in two layers, an imported set and local overrides, and the API reports what the combination actually permits:

print(sansai.active_rules("LHR"))
print(sansai.rule_effect())
sansai.set_local_rule(body={"resource": "wide_body_stand", "capability": "A380"})

Forward schedules, KPI targets, and FRMS training runs are reachable through the same client:

schedule = sansai.preview_schedule("LHR")
kpis = sansai.kpis()
job = sansai.queue_frms_training(body={"models": ["turnaround"], "airport": "LHR"})

Turnaround Allocation is licensed separately from the plan tiers, so holding the scopes is not enough on its own. Without the add-on you get 403 with code FRMS_ACCESS_REQUIRED. Talk to [email protected] about adding it.

They carry their own scopes: read:frms, write:frms, read:frms-rules, write:frms-rules, read:frms-schedule, write:frms-schedule, read:frms-training, and write:frms-training.

Reading the fields

| Field | Meaning | | --- | --- | | delay_minutes | Minutes late. Negative means early. | | delay_probability | 0 to 100, the modeled chance of a delay. | | predicted_delay_minutes | Expected delay if one occurs. | | confidence | 0 to 100, how much weight the model puts on its own answer. | | utilization_percentage | 0 to 100, how hard a stand or resource is working. | | efficiency_change | Percentage points against the previous period. | | severity | advisory, warning, or critical. | | visibility | Statute miles. | | wind_speed | Knots. | | altitude | Feet above mean sea level. | | generated_at | RFC 3339 UTC, ending in Z. Use this, not any timestamp field. |

Knowing what you can reach

Rather than discovering an entitlement by receiving a 403, ask:

access = sansai.entitlements()

print(access.plan.name)
print([f.name for f in access.features])

for add_on in access.add_ons:
    print(add_on.label, "enabled" if add_on.enabled else "not licensed")

The response reports the plan behind the credential, the features on the account, which add-ons are licensed, and the scopes the credential carries. An integration can check once at start-up and route around anything it cannot use.

Arrival times, and where they came from

Not every feed publishes an arrival time. Each flight says which case it is:

for flight in sansai.live_flights(airport="LHR"):
    if flight.eta is None:
        continue                      # on the ground, or no time available
    print(flight.eta, flight.eta_source, flight.eta_estimated)

eta_estimated is false only for an actual recorded arrival. eta_source of derived means the time was computed from position, destination and ground speed rather than published, so treat it as an estimate. Absent means the feed supplied its own arrival time.

Where the feed provides them, arrival_gate, arrival_terminal and arrival_delay_minutes come through too.

Knowing when a list is cut short

List endpoints clamp above a ceiling. Every list response now says whether it did:

result = sansai.flight_history(airport="LHR", limit=5000)

print(result.meta.page.limit)            # 2000, the applied ceiling
print(result.meta.page.limit_requested)  # 5000, what you asked for
print(result.meta.page.truncated)        # True, more records may exist

truncated is true when the response filled the applied limit, so more records may sit beyond it. Narrow the date range and call again.

Limits and quotas

Three separate ceilings apply. Your position against all three comes back on every call, so you never have to discover a limit by hitting it.

result = sansai.flight_history(airport="LHR")
print(result.meta.rate_limit.remaining_today)
print(result.meta.subscription.plan, result.meta.subscription.remaining_this_month)

| Limit | Scope | Resets | | --- | --- | --- | | Per-minute and per-day rate limit | One credential | Within the window | | Monthly call allowance | Your account, across every credential it owns | Start of the calendar month | | Minimum plan tier | Optional, set per credential | On upgrade |

Monthly allowance by plan: Standard 10,000, Professional 50,000, Enterprise 75,000, Custom negotiated. See https://www.sans-ai.tech/pricing.

List endpoints accept limit and do not paginate. Values above each ceiling are clamped, so split larger windows with start_date and end_date rather than trying to page.

Data guarantees

Every successful response carries observed records or trained model output. The API never substitutes estimated, sample, or heuristic values. Where no records or no trained model exist you get NoDataError, so absence is never mistaken for a measurement.

If you are feeding this into a system that assigns stands or dispatches crew, that guarantee is the point. You do not need a defensive check on every field to ask whether the number is real.

Need a client in another language?

The SDKs are generated from an OpenAPI specification, which the API serves directly, so the contract you generate from is always the one the service is running:

curl -O https://www.sans-ai.tech/api/openapi.yaml     # as authored
curl -O https://www.sans-ai.tech/api/openapi.json     # for tooling that wants JSON

Both are unauthenticated, because the spec describes the shape of the API rather than any data. Every endpoint it documents still needs a credential.

Point your generator of choice at it, for example:

openapi-generator-cli generate \
  -i https://www.sans-ai.tech/api/openapi.yaml \
  -g go -o ./sansai-go

Citation

If SANS.AI informs a paper, technical report, operations study, or regulatory submission, cite the release you actually ran.

Two things here are citable and they are not the same. Cite SANS.AI when your results depend on its data or predictions, which is the usual case. Cite the client library only when the work is about the client itself, or when a reviewer needs to reproduce the exact calls you made.

Citing SANS.AI

The API documentation renders the current platform citation in BibTeX, APA, IEEE, and CITATION.cff, generated from the running service so the version is never stale: https://www.sans-ai.tech/documentation

That page is the source to copy from. This README deliberately does not restate the platform version, because a version pinned in a package README goes out of date the moment the service ships a release.

Citing this client

BibTeX

@misc{kquika2026sansaipy,
  title        = {SANS.AI Platform Python SDK},
  author       = {{Kquika, Inc.}},
  year         = {2026},
  version      = {1.0.3},
  publisher    = {{Kquika, Inc.}},
  howpublished = {\url{https://pypi.org/project/kquika-sansai/}},
  url          = {https://pypi.org/project/kquika-sansai/},
  note         = {Computer software, version 1.0.0}
}

@misc{kquika2026sansaijs,
  title        = {SANS.AI Platform Node SDK},
  author       = {{Kquika, Inc.}},
  year         = {2026},
  version      = {1.0.3},
  publisher    = {{Kquika, Inc.}},
  howpublished = {\url{https://www.npmjs.com/package/@kquika-inc/sansai}},
  url          = {https://www.npmjs.com/package/@kquika-inc/sansai},
  note         = {Computer software, version 1.0.0}
}

@misc is used because every BibTeX style accepts it. On biblatex you can change the type to @software and drop howpublished.

APA 7th

Kquika, Inc. (2026). SANS.AI Platform Python SDK (Version 1.0.3) [Computer software]. https://pypi.org/project/kquika-sansai/

Kquika, Inc. (2026). SANS.AI Platform Node SDK (Version 1.0.3) [Computer software]. https://www.npmjs.com/package/@kquika-inc/sansai

In text: (Kquika, Inc., 2026).

IEEE

Kquika, Inc., "SANS.AI Platform Python SDK," version 1.0.3, 2026. [Online]. Available: https://pypi.org/project/kquika-sansai/

Kquika, Inc., "SANS.AI Platform Node SDK," version 1.0.3, 2026. [Online]. Available: https://www.npmjs.com/package/@kquika-inc/sansai

CITATION.cff

Drop this in the root of a repository that depends on SANS.AI and reference managers will pick it up. Keep the entry for the client you actually use.

cff-version: 1.2.1
message: "If you use SANS.AI in your work, please cite it as below."
title: "SANS.AI Platform Python SDK"
type: software
version: "1.0.3"
license: MIT
url: "https://pypi.org/project/kquika-sansai/"
repository-artifact: "https://pypi.org/project/kquika-sansai/"
authors:
  - name: "Kquika, Inc."

For the Node client, change title to SANS.AI Platform Node SDK and both URLs to https://www.npmjs.com/package/@kquika-inc/sansai.

Reporting the version

Pin the version in your methods section. The client reports itself in the User-Agent on every request, so a server log line and a paper can be tied together:

sansai-python/1.0.3
sansai-node/1.0.3

Read it at run time rather than transcribing it, so the number in your write up cannot drift from the number that ran:

import sansai
print(sansai.__version__)
import { VERSION } from "@kquika-inc/sansai";
console.log(VERSION);

Predictions also depend on which models produced them, and those version independently of both the API and this client. Call sansai.models() at run time and record the model version alongside the API version, so a reviewer can tell which models produced your numbers.

Support

API reference: https://www.sans-ai.tech/documentation

Questions, a key, or a raised limit: [email protected]

License

MIT