Skip to content

v0.8.13

Choose a tag to compare

@github-actions github-actions released this 05 Sep 22:20
· 12 commits to master since this release
48fd640
  • Ruff's F401 (unused-import) rule is now enabled repo-wide: the lint set
    previously covered only the F63/F7/F82 subsets of pyflakes, so orphaned
    imports went unreported by ruff check, ruff format and ty — two left
    behind by the OC-152 fix below passed every gate and were caught only by an
    external linter. 22 dead imports were removed across tests, examples and
    notebooks; 4 intentional availability/side-effect probes (polars, h3,
    sentence_transformers) keep a per-site # noqa: F401 - <reason> waiver.
    Contributor-visible consequence: the pre-commit ruff hook runs with
    --fix, so an unused import added deliberately is now deleted at commit time
    unless it carries a waiver — and ruff honours # noqa only on the import
    statement line, not on a preceding comment. F841, the docstring rules and
    unused-args remain out of scope (OC-09).

  • A misspelled FASTAPI_ENV no longer silently boots the development security
    posture (OC-130):
    the value selected a settings profile through a lookup that
    fell back to DevelopmentSettings for anything unrecognized, so prod, prd,
    staging — or a trailing space picked up from a YAML/CI variable — started the
    server with DEBUG=True, CORS_ORIGINS=["*"], no security headers and the
    production SECRET_KEY check skipped. The wildcard origin is worse than it
    looks: the app also sets allow_credentials=True, and Starlette in that
    combination reflects the caller's origin instead of sending a literal *,
    so the browser's normal refusal never fires and any site can make credentialed
    requests. A second channel was dead as well — FASTAPI_ENV is not a Settings
    field and pydantic-settings never exports dotenv values into the process
    environment, so setting it in .env, the documented configuration file, had
    no effect at all and the server came up in development either way. Both now
    go through one resolver that reads the same channels as every other setting,
    normalizes case and surrounding whitespace, and raises at startup for any
    other value, naming the accepted ones (development, production, testing).
    An empty value raises too, since that is what an unset CI variable renders to.
    Behaviour change: a server with a typo'd or blank FASTAPI_ENV now refuses
    to start instead of starting insecurely. Fix the value rather than removing it
    — the absence of an error was the bug.

  • AWS credentials no longer reach the log through S3 errors (OC-150): both S3
    modules carried their own copy of a _sanitize_error helper that matched on
    credential key names, and in practice it did nothing at all — an S3 403
    response body, a presigned URL and an s3fs options dictionary all passed
    through byte-identical, exposing not just access key IDs and signatures but the
    secret access key itself. Presigned URLs are bearer credentials: anyone who
    obtains one can fetch the object, so logging one publishes it. The same helper
    was simultaneously too aggressive elsewhere, replacing an ordinary
    key=reports/2026/q3.csv not found message wholesale with redacted sensitive S3 error and destroying the only useful part of the diagnostic. Both copies
    are replaced by one redact_credentials() helper beside the existing log
    injection guard, which matches on value shape rather than setting name:
    20-character AWS access key IDs, name=value and name: value assignments for
    the known credential options, and the XML tags S3 uses in its 403 body. Only the
    secret is replaced, so the surrounding message survives intact, and redacting an
    already-redacted message changes nothing. The S3 connector's startup log line
    now also redacts the path it was given, since a caller may pass a presigned URL
    directly. Exception messages raised back to the caller are deliberately left
    readable — they carry your own input, and a redacted error is no use to you.

  • Removed four unused raw-SQL executor methods (OC-152):
    execute_query/execute_update on both the SQLite and PostgreSQL async
    connection managers accepted an arbitrary query string and passed it straight
    to the driver. Nothing in the codebase called them, but their presence offered
    the next contributor an unconstrained injection sink that bypasses every
    parameterisation convention the rest of the database layer follows. Use
    SQLAlchemy constructs, or text() with bound parameters, instead.

  • Deployed-model predictions no longer silently use misaligned or invented
    features (OC-154, OC-155):
    two serving paths degraded quietly instead of
    failing. The bundled path skipped its training-feature-order reindex exactly
    when alignment could not be confirmed — the one case the reindex existed
    for — and handed a positional model whatever columns the feature engineer
    happened to produce; reproduced returning 9921.0 from a frame whose
    columns did not match what the bundle was trained on, with no error and no
    warning. The legacy path imputed absent features with the literal constant
    0, which for income, age, price or any scaled feature is an extreme
    out-of-distribution input, returned the result as a normal prediction behind
    a server-side log line the caller never saw, and wrote the fabricated column
    into the caller's own DataFrame. Both now raise an error naming the offending
    columns, which the API surfaces as HTTP 400 with the column list and the
    canvas displays directly. Behaviour change: a deployment whose recorded
    feature columns no longer match what its own feature engineer emits, or a
    request that omits a feature the model was trained on, now fails loudly
    instead of returning a number. Such a bundle needs retraining and
    redeploying — the old response was wrong, not merely convenient.

  • Merging branches with different row counts no longer duplicates data
    silently (OC-153, OC-157):
    wiring two branches into a merge node expresses
    a feature union, but the engine picked its merge mode purely from row
    counts — so when either branch changed the row count (outlier removal,
    deduplication, any dropna) it silently switched to a row-wise concat and
    returned a taller frame of stacked rows instead of a wider one.
    Reproduced: a 5-row dataset merged with its own filtered branch produced 9
    rows containing 4 duplicates, and because the two branches had identical
    column sets the one condition that emitted a UI advisory was false — the
    canvas showed zero warnings and the only trace was a line in the job log.
    Duplicated rows silently reweight those observations during training and,
    when the merge feeds a split node, place identical rows on both sides of the
    train/test boundary. Row-wise stacking is still supported, since appending
    separate datasets is a legitimate use of a merge node, but it now always
    emits a row_count_mismatch advisory carrying the per-input row counts and
    rendered in the canvas merge banner with the remedy. Separately, the
    first_wins strategy reversed the output column order: it was
    implemented by iterating the inputs backwards, and the accumulator dict's
    insertion order is the merged frame's column order, so the same two
    branches came out ['a','b','c','d'] under last_wins and
    ['c','d','a','b'] under first_wins — contradicting its own docstring and
    handing positional consumers a different layout per configuration. Ownership
    is now resolved by declining to overwrite an already-claimed column while
    walking the inputs in their own order, so both strategies emit columns in
    input order with unchanged winners.

  • Recursive Feature Elimination now selects the number of features you asked
    for (OC-25, also closes OC-143):
    the RFE panel's "K (Number of Features)"
    field was ignored. The backend read n_features_to_select — a key nothing in
    the entire codebase ever wrote — so it was always unset and scikit-learn fell
    back to its own default of keeping half the candidate features. Setting
    K=2 over 6 features selected 3, with no error and no warning, while the node
    summary still read rfe · k=2. It hid because RFE's other field (step)
    is read correctly, so the panel looked fully wired up, and the test fixture
    exercised only the internal n_features_to_select spelling, leaving the k
    path the UI actually sends with no coverage at all. RFE now accepts k as an
    alias, with an explicit n_features_to_select taking precedence, so the
    canvas, direct API calls and notebooks all agree. Added 3 regression tests
    including an end-to-end case over 6 features.