Skip to content

fix(classifier): fail at start with a clear error when the model is missing - #307

Merged
saidsef merged 4 commits into
mainfrom
fix/missing-model-error
Oct 20, 2026
Merged

saidsef merged 4 commits into
mainfrom
fix/missing-model-error

Conversation

@saidsef

@saidsef saidsef commented Oct 16, 2026

Copy link
Copy Markdown
Owner

Summary

A missing model now stops the service at start with FileNotFoundError: /app/data/voting_classifier.pickle.xz is missing, build it with classifier-ml.ipynb. The path resolves next to classifier.py, so the service starts from any working directory.

Related Issues

Fixes #301.

Testing

  • published image with data/ emptied - exits with the FileNotFoundError above
  • published image with the model, started from /tmp - loads the Pipeline

…issing

A checkout without the model file logged "Unable to load file" and
crashed with an AttributeError about a missing clf attribute. The
loader now raises a FileNotFoundError that names the path and the
notebook that builds the model.

- classifier.py resolves the model next to the module, so the service
  starts from any working directory.
- A missing model stops the process before Flask starts.
- Startup with the model in place behaves as before, and nothing else
  broke.
@saidsef saidsef added bug Something isn't working python Pull requests that update Python code mcp labels Oct 16, 2026
@saidsef saidsef self-assigned this Oct 16, 2026
@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Dependency Review

The following issues were found:
  • ✅ 0 vulnerable package(s)
  • ✅ 0 package(s) with incompatible licenses
  • ✅ 0 package(s) with invalid SPDX license definitions
  • ✅ 0 package(s) with unknown licenses.
  • ⚠️ 2 packages with OpenSSF Scorecard issues.
See the Details below.

OpenSSF Scorecard

Scorecard details
PackageVersionScoreDetails
pip/babel 2.18.0 UnknownUnknown
pip/backrefs 8.1 UnknownUnknown
pip/certifi 2026.7.22 🟢 6.2
Details
CheckScoreReason
Code-Review🟢 5Found 2/4 approved changesets -- score normalized to 5
Maintained🟢 67 commit(s) and 1 issue activity found in the last 90 days -- score normalized to 6
Binary-Artifacts🟢 10no binaries found in the repo
Security-Policy🟢 10security policy file detected
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions🟢 10GitHub workflow tokens follow principle of least privilege
Pinned-Dependencies🟢 5dependency not pinned by hash detected -- score normalized to 5
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Fuzzing⚠️ 0project is not fuzzed
License🟢 9license file detected
Signed-Releases⚠️ -1no releases found
Branch-Protection⚠️ 0branch protection not enabled on development/release branches
Packaging🟢 10packaging workflow detected
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
pip/charset-normalizer 3.5.2 UnknownUnknown
pip/colorama 0.4.6 UnknownUnknown
pip/ghp-import 2.1.0 ⚠️ 2.9
Details
CheckScoreReason
Binary-Artifacts🟢 10no binaries found in the repo
Maintained⚠️ 00 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Code-Review⚠️ 1Found 5/28 approved changesets -- score normalized to 1
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
Packaging⚠️ -1packaging workflow not detected
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Security-Policy⚠️ 0security policy file not detected
Fuzzing⚠️ 0project is not fuzzed
Signed-Releases⚠️ -1no releases found
License🟢 10license file detected
Branch-Protection⚠️ 0branch protection not enabled on development/release branches
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
pip/idna 3.20 UnknownUnknown
pip/markdown 3.11.1 UnknownUnknown
pip/mergedeep 1.3.4 ⚠️ 2
Details
CheckScoreReason
Packaging⚠️ -1packaging workflow not detected
Dangerous-Workflow⚠️ -1no workflows found
Maintained⚠️ 00 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0
Binary-Artifacts🟢 10no binaries found in the repo
Code-Review⚠️ 0Found 1/27 approved changesets -- score normalized to 0
Pinned-Dependencies⚠️ -1no dependencies found
Token-Permissions⚠️ -1No tokens found
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Fuzzing⚠️ 0project is not fuzzed
Security-Policy⚠️ 0security policy file not detected
License🟢 10license file detected
Signed-Releases⚠️ -1no releases found
Branch-Protection⚠️ 0branch protection not enabled on development/release branches
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
pip/mkdocs 1.6.1 UnknownUnknown
pip/mkdocs-get-deps 0.2.2 UnknownUnknown
pip/mkdocs-material 9.7.7 UnknownUnknown
pip/mkdocs-material-extensions 1.3.1 UnknownUnknown
pip/packaging 26.3 UnknownUnknown
pip/paginate 0.5.7 UnknownUnknown
pip/pathspec 1.1.1 UnknownUnknown
pip/platformdirs 4.13.3 UnknownUnknown
pip/pygments 2.21.0 UnknownUnknown
pip/pymdown-extensions 12.3 UnknownUnknown
pip/python-dateutil 2.9.0.post0 🟢 5.2
Details
CheckScoreReason
Maintained🟢 97 commit(s) and 4 issue activity found in the last 90 days -- score normalized to 9
Code-Review⚠️ 0Found 0/15 approved changesets -- score normalized to 0
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Binary-Artifacts🟢 10no binaries found in the repo
Packaging⚠️ -1packaging workflow not detected
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Security-Policy⚠️ 0security policy file not detected
Fuzzing🟢 10project is fuzzed
License🟢 9license file detected
Signed-Releases🟢 82 out of the last 2 releases have a total of 2 signed artifacts.
Branch-Protection⚠️ -1internal error: error during branchesHandler.setup: internal error: some github tokens can't read classic branch protection rules: https://github.com/ossf/scorecard-action/blob/main/docs/authentication/fine-grained-auth-token.md
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
pip/pyyaml 6.0.3 UnknownUnknown
pip/pyyaml-env-tag 1.1 UnknownUnknown
pip/requests 2.34.2 UnknownUnknown
pip/six 1.17.0 🟢 3.7
Details
CheckScoreReason
Code-Review⚠️ 2Found 7/30 approved changesets -- score normalized to 2
Binary-Artifacts🟢 10no binaries found in the repo
Packaging⚠️ -1packaging workflow not detected
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Maintained⚠️ 00 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Security-Policy⚠️ 0security policy file not detected
Fuzzing🟢 10project is fuzzed
License🟢 10license file detected
Signed-Releases⚠️ -1no releases found
Branch-Protection⚠️ 0branch protection not enabled on development/release branches
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
pip/urllib3 2.8.0 UnknownUnknown
pip/watchdog 6.0.0 🟢 5.1
Details
CheckScoreReason
Code-Review🟢 6Found 6/10 approved changesets -- score normalized to 6
Maintained🟢 1030 commit(s) and 8 issue activity found in the last 90 days -- score normalized to 10
Binary-Artifacts🟢 10no binaries found in the repo
Dangerous-Workflow⚠️ 0dangerous workflow patterns detected
Pinned-Dependencies🟢 4dependency not pinned by hash detected -- score normalized to 4
Token-Permissions🟢 9detected GitHub workflow tokens with excessive permissions
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Security-Policy⚠️ 0security policy file not detected
License🟢 10license file detected
Fuzzing⚠️ 0project is not fuzzed
Signed-Releases⚠️ -1no releases found
Branch-Protection⚠️ 0branch protection not enabled on development/release branches
SAST🟢 7SAST tool is not run on all commits -- score normalized to 7
Packaging🟢 10packaging workflow detected

Scanned Files

  • uv.lock

@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 0 complexity · 0 duplication

Metric Results
Complexity 0
Duplication 0

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

github-actions[bot]
github-actions Bot previously approved these changes Oct 16, 2026
)

The score in every classification response was the string "1". It came
from comparing the article text with the predicted label, which never
match. The handler now takes the class probabilities from the
soft-voting ensemble and returns the highest one with its category.

- classifier-ml.py picks the category from the model's classes at the
  highest probability, which agrees with predict.
- Responses carry the score as a number between 0 and 1.
- Classification returns the same categories as before, and nothing
  else broke.
github-actions[bot]
github-actions Bot previously approved these changes Oct 18, 2026
* fix(api): report the predicted category's probability as the score

The score in every classification response was the string "1". It came
from comparing the article text with the predicted label, which never
match. The handler now takes the class probabilities from the
soft-voting ensemble and returns the highest one with its category.

- classifier-ml.py picks the category from the model's classes at the
  highest probability, which agrees with predict.
- Responses carry the score as a number between 0 and 1.
- Classification returns the same categories as before, and nothing
  else broke.

* fix(api): retrain the served model from the train endpoint

The train endpoint never trained anything. Its payload indexing raised
a KeyError that went back to the caller with a 200, and the fit would
have run on a fresh classifier that served nothing. The endpoint now
fits a copy of the served pipeline on 80% of the posted documents,
scores it on the rest and swaps it in once the fit succeeds.

- classifier.py clones the pipeline, so predictions never see a
  half-fitted model.
- A lock turns a second request during a fit into a 409, since each
  fit holds another copy of the model in memory.
- Malformed or undersized payloads get a 400 with the reason.
- Training on 500 articles swaps the new model in while predictions
  keep serving, and nothing else broke.
* fix(api): report the predicted category's probability as the score

The score in every classification response was the string "1". It came
from comparing the article text with the predicted label, which never
match. The handler now takes the class probabilities from the
soft-voting ensemble and returns the highest one with its category.

- classifier-ml.py picks the category from the model's classes at the
  highest probability, which agrees with predict.
- Responses carry the score as a number between 0 and 1.
- Classification returns the same categories as before, and nothing
  else broke.

* fix(api): retrain the served model from the train endpoint

The train endpoint never trained anything. Its payload indexing raised
a KeyError that went back to the caller with a 200, and the fit would
have run on a fresh classifier that served nothing. The endpoint now
fits a copy of the served pipeline on 80% of the posted documents,
scores it on the rest and swaps it in once the fit succeeds.

- classifier.py clones the pipeline, so predictions never see a
  half-fitted model.
- A lock turns a second request during a fit into a 409, since each
  fit holds another copy of the model in memory.
- Malformed or undersized payloads get a 400 with the reason.
- Training on 500 articles swaps the new model in while predictions
  keep serving, and nothing else broke.

* fix(api): listen on port 7070 by default

The service fell back to port 8080 when PORT was unset, while the
image, the manifests and the probes all use 7070. A checkout started
without PORT came up on the wrong port. The default now matches the
rest of the project.

- classifier-ml.py defaults PORT to 7070.
- The comment above the setting goes, since it only restated the
  default.
- With PORT unset the server listens on 7070, and nothing else broke.

* fix(deployment): apply the image tag override in the default kustomization (#308)

The images override in the default kustomization was three list items
where kustomize expects one, so it matched no image and dropped the
tag. Changing newTag had no effect, and the tag pinned in the base
Deployment always won. The override is now a single entry.

- deployment/kustomization.yml joins name, newName and newTag into one
  item.
- Rendered output stays the same at the current tag, and a different
  newTag now reaches the Deployment.

* docs(site): publish the documentation on Read the Docs (#310)

* fix(deployment): apply the image tag override in the default kustomization

The images override in the default kustomization was three list items
where kustomize expects one, so it matched no image and dropped the
tag. Changing newTag had no effect, and the tag pinned in the base
Deployment always won. The override is now a single entry.

- deployment/kustomization.yml joins name, newName and newTag into one
  item.
- Rendered output stays the same at the current tag, and a different
  newTag now reaches the Deployment.

* docs: add pages on training, running and deploying the classifier

The project's documentation was a handful of usage commands. Eight
pages now cover how the model is trained, how to run and call
the service, how to deploy and operate it, and what CI builds.

- docs/README.md serves as the home page on GitHub and on the site.
- Architecture opens with a diagram of the path from dataset to
  cluster.
- Image tags, resource limits and other values that drift stay in the
  files that set them, and the pages link there.
- Every link and anchor resolves, and nothing broke.

* docs(site): publish the documentation on Read the Docs

The documentation pages are now a Material-themed site that Read the
Docs builds with MkDocs. Read the Docs installs the tooling from a
docs dependency group in the uv lock file, which keeps it out of the
image and lets Dependabot update it with everything else.

- .readthedocs.yaml syncs the docs group with uv on Python 3.14.
- pyproject.toml pins MkDocs, Material, pymdown-extensions and Pygments
  by major version.
- mkdocs.yml runs in strict mode, so a broken link fails the build.
- README.md links to the published site and carries its badge.
- The site builds without warnings, and the image still installs only
  the runtime dependencies.
@saidsef saidsef added the preview Create new preview environment label Oct 19, 2026
@saidsef-pr

saidsef-pr Bot commented Oct 19, 2026

Copy link
Copy Markdown

Application ml-classifier-eca654a-307-pr-reviews has been created.

5 similar comments
@saidsef-pr

saidsef-pr Bot commented Oct 19, 2026

Copy link
Copy Markdown

Application ml-classifier-eca654a-307-pr-reviews has been created.

@saidsef-pr

saidsef-pr Bot commented Oct 19, 2026

Copy link
Copy Markdown

Application ml-classifier-eca654a-307-pr-reviews has been created.

@saidsef-pr

saidsef-pr Bot commented Oct 19, 2026

Copy link
Copy Markdown

Application ml-classifier-eca654a-307-pr-reviews has been created.

@saidsef-pr

saidsef-pr Bot commented Oct 19, 2026

Copy link
Copy Markdown

Application ml-classifier-eca654a-307-pr-reviews has been created.

@saidsef-pr

saidsef-pr Bot commented Oct 19, 2026

Copy link
Copy Markdown

Application ml-classifier-eca654a-307-pr-reviews has been created.

@saidsef
saidsef merged commit 51bff9b into main Oct 20, 2026
8 checks passed
@saidsef
saidsef deleted the fix/missing-model-error branch October 20, 2026 23:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working mcp preview Create new preview environment python Pull requests that update Python code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(classifier): fail at start with a clear error when the model is missing

1 participant