Upload 18 files
Browse files- .dockerignore +48 -0
- .editorconfig +27 -0
- .env.example +4 -0
- .gitattributes +17 -41
- .github/dependabot.yml +48 -0
- .github/workflows/ci.yml +87 -0
- .gitignore +77 -0
- .pre-commit-config.yaml +21 -0
- .python-version +1 -0
- CONTRIBUTING.md +21 -0
- Makefile +53 -0
- SECURITY.md +18 -0
- pyproject.toml +38 -0
- requirements-dev.txt +5 -0
- requirements.txt +11 -6
- scripts/doctor.py +150 -0
- tests/__pycache__/test_smoke.cpython-311-pytest-8.3.4.pyc +0 -0
- tests/test_smoke.py +63 -0
.dockerignore
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# --- Python / caches ---
|
| 2 |
+
.venv/
|
| 3 |
+
__pycache__/
|
| 4 |
+
*.py[cod]
|
| 5 |
+
.pytest_cache/
|
| 6 |
+
.ruff_cache/
|
| 7 |
+
.mypy_cache/
|
| 8 |
+
.ipynb_checkpoints/
|
| 9 |
+
|
| 10 |
+
# --- Coverage / test artifacts ---
|
| 11 |
+
.coverage
|
| 12 |
+
htmlcov/
|
| 13 |
+
coverage.xml
|
| 14 |
+
|
| 15 |
+
# --- Packaging / build artifacts ---
|
| 16 |
+
dist/
|
| 17 |
+
build/
|
| 18 |
+
*.egg-info/
|
| 19 |
+
|
| 20 |
+
# --- Git / IDE ---
|
| 21 |
+
.git/
|
| 22 |
+
.vscode/
|
| 23 |
+
.idea/
|
| 24 |
+
|
| 25 |
+
# --- Env / secrets ---
|
| 26 |
+
.env
|
| 27 |
+
.env.*
|
| 28 |
+
.envrc
|
| 29 |
+
.streamlit/secrets.toml
|
| 30 |
+
|
| 31 |
+
# --- OS junk ---
|
| 32 |
+
.DS_Store
|
| 33 |
+
Thumbs.db
|
| 34 |
+
|
| 35 |
+
# --- Logs ---
|
| 36 |
+
*.log
|
| 37 |
+
logs/
|
| 38 |
+
|
| 39 |
+
# --- Runtime artifacts (don’t bake into image) ---
|
| 40 |
+
data/
|
| 41 |
+
datasets/
|
| 42 |
+
artifacts/
|
| 43 |
+
models/
|
| 44 |
+
outputs/
|
| 45 |
+
runs/
|
| 46 |
+
|
| 47 |
+
# --- Node (if any) ---
|
| 48 |
+
node_modules/
|
.editorconfig
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
root = true
|
| 2 |
+
|
| 3 |
+
[*]
|
| 4 |
+
charset = utf-8
|
| 5 |
+
end_of_line = lf
|
| 6 |
+
insert_final_newline = true
|
| 7 |
+
trim_trailing_whitespace = true
|
| 8 |
+
indent_style = space
|
| 9 |
+
indent_size = 4
|
| 10 |
+
|
| 11 |
+
[*.{yml,yaml}]
|
| 12 |
+
indent_size = 2
|
| 13 |
+
|
| 14 |
+
[*.{json,jsonc}]
|
| 15 |
+
indent_size = 2
|
| 16 |
+
|
| 17 |
+
[*.{js,jsx,ts,tsx}]
|
| 18 |
+
indent_size = 2
|
| 19 |
+
|
| 20 |
+
[Makefile]
|
| 21 |
+
indent_style = tab
|
| 22 |
+
|
| 23 |
+
[*.md]
|
| 24 |
+
trim_trailing_whitespace = false
|
| 25 |
+
|
| 26 |
+
[*.{bat,cmd,ps1}]
|
| 27 |
+
end_of_line = crlf
|
.env.example
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Optional runtime overrides for Docker / local runs
|
| 2 |
+
HOST=0.0.0.0
|
| 3 |
+
PORT=8501
|
| 4 |
+
APP_FILE=app.py
|
.gitattributes
CHANGED
|
@@ -1,41 +1,17 @@
|
|
| 1 |
-
*
|
| 2 |
-
|
| 3 |
-
*.
|
| 4 |
-
*.
|
| 5 |
-
*.
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
*.
|
| 9 |
-
*.
|
| 10 |
-
*.
|
| 11 |
-
*.
|
| 12 |
-
*.
|
| 13 |
-
*.
|
| 14 |
-
*.
|
| 15 |
-
*.
|
| 16 |
-
*.
|
| 17 |
-
*.
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
-
assets/compare-hist-kde-tips.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
-
assets/compare-scatter-flights.png filter=lfs diff=lfs merge=lfs -text
|
| 38 |
-
assets/matplotlib-iris-sepal-length-hist.png filter=lfs diff=lfs merge=lfs -text
|
| 39 |
-
assets/matplotlib-iris-sepal-length-line.png filter=lfs diff=lfs merge=lfs -text
|
| 40 |
-
assets/seaborn-tips-total-bill-hist-sex.png filter=lfs diff=lfs merge=lfs -text
|
| 41 |
-
assets/seaborn-tips-total-bill-vs-tip-scatter.png filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 1 |
+
* text=auto eol=lf
|
| 2 |
+
|
| 3 |
+
*.bat text eol=crlf
|
| 4 |
+
*.cmd text eol=crlf
|
| 5 |
+
*.ps1 text eol=crlf
|
| 6 |
+
|
| 7 |
+
# binaries (do not normalize / no eol)
|
| 8 |
+
*.png binary
|
| 9 |
+
*.jpg binary
|
| 10 |
+
*.jpeg binary
|
| 11 |
+
*.gif binary
|
| 12 |
+
*.webp binary
|
| 13 |
+
*.ico binary
|
| 14 |
+
*.pdf binary
|
| 15 |
+
*.zip binary
|
| 16 |
+
*.gz binary
|
| 17 |
+
*.7z binary
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.github/dependabot.yml
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: 2
|
| 2 |
+
|
| 3 |
+
updates:
|
| 4 |
+
- package-ecosystem: "pip"
|
| 5 |
+
directory: "/"
|
| 6 |
+
target-branch: "main"
|
| 7 |
+
schedule:
|
| 8 |
+
interval: "weekly"
|
| 9 |
+
day: "monday"
|
| 10 |
+
time: "09:00"
|
| 11 |
+
timezone: "Africa/Cairo"
|
| 12 |
+
open-pull-requests-limit: 2
|
| 13 |
+
rebase-strategy: "auto"
|
| 14 |
+
labels: ["dependencies", "python"]
|
| 15 |
+
groups:
|
| 16 |
+
python-patch-minor:
|
| 17 |
+
patterns: ["*"]
|
| 18 |
+
update-types: ["patch", "minor"]
|
| 19 |
+
python-major:
|
| 20 |
+
patterns: ["*"]
|
| 21 |
+
update-types: ["major"]
|
| 22 |
+
# Optional safety: keep major updates manual for heavy deps
|
| 23 |
+
# ignore:
|
| 24 |
+
# - dependency-name: "numpy"
|
| 25 |
+
# update-types: ["version-update:semver-major"]
|
| 26 |
+
# - dependency-name: "pandas"
|
| 27 |
+
# update-types: ["version-update:semver-major"]
|
| 28 |
+
# - dependency-name: "scikit-learn"
|
| 29 |
+
# update-types: ["version-update:semver-major"]
|
| 30 |
+
|
| 31 |
+
- package-ecosystem: "github-actions"
|
| 32 |
+
directory: "/"
|
| 33 |
+
target-branch: "main"
|
| 34 |
+
schedule:
|
| 35 |
+
interval: "weekly"
|
| 36 |
+
day: "monday"
|
| 37 |
+
time: "09:00"
|
| 38 |
+
timezone: "Africa/Cairo"
|
| 39 |
+
open-pull-requests-limit: 2
|
| 40 |
+
rebase-strategy: "auto"
|
| 41 |
+
labels: ["dependencies", "github-actions"]
|
| 42 |
+
groups:
|
| 43 |
+
actions-patch-minor:
|
| 44 |
+
patterns: ["*"]
|
| 45 |
+
update-types: ["patch", "minor"]
|
| 46 |
+
actions-major:
|
| 47 |
+
patterns: ["*"]
|
| 48 |
+
update-types: ["major"]
|
.github/workflows/ci.yml
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: ci
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches: ["main"]
|
| 6 |
+
pull_request:
|
| 7 |
+
|
| 8 |
+
permissions:
|
| 9 |
+
contents: read
|
| 10 |
+
|
| 11 |
+
concurrency:
|
| 12 |
+
group: ci-${{ github.ref }}
|
| 13 |
+
cancel-in-progress: true
|
| 14 |
+
|
| 15 |
+
jobs:
|
| 16 |
+
ci:
|
| 17 |
+
runs-on: ubuntu-latest
|
| 18 |
+
timeout-minutes: 15
|
| 19 |
+
|
| 20 |
+
strategy:
|
| 21 |
+
fail-fast: false
|
| 22 |
+
matrix:
|
| 23 |
+
python-version: ["3.11"]
|
| 24 |
+
|
| 25 |
+
steps:
|
| 26 |
+
- name: Checkout
|
| 27 |
+
uses: actions/checkout@v6
|
| 28 |
+
|
| 29 |
+
- name: Setup Python
|
| 30 |
+
uses: actions/setup-python@v6
|
| 31 |
+
with:
|
| 32 |
+
python-version: ${{ matrix.python-version }}
|
| 33 |
+
cache: "pip"
|
| 34 |
+
cache-dependency-path: |
|
| 35 |
+
requirements.txt
|
| 36 |
+
requirements-dev.txt
|
| 37 |
+
|
| 38 |
+
- name: Install dependencies
|
| 39 |
+
run: |
|
| 40 |
+
python -m pip install -U pip
|
| 41 |
+
python -m pip install -r requirements-dev.txt
|
| 42 |
+
|
| 43 |
+
- name: Basic sanity (compile)
|
| 44 |
+
run: python -m compileall -q .
|
| 45 |
+
|
| 46 |
+
- name: Ruff (lint + format check)
|
| 47 |
+
run: |
|
| 48 |
+
python -m ruff check .
|
| 49 |
+
python -m ruff format --check .
|
| 50 |
+
|
| 51 |
+
- name: Pytest
|
| 52 |
+
run: python -m pytest -q
|
| 53 |
+
|
| 54 |
+
- name: Docker present?
|
| 55 |
+
id: dockerfile
|
| 56 |
+
shell: bash
|
| 57 |
+
run: |
|
| 58 |
+
if [ -f Dockerfile ]; then echo "present=true" >> "$GITHUB_OUTPUT"; else echo "present=false" >> "$GITHUB_OUTPUT"; fi
|
| 59 |
+
|
| 60 |
+
- name: Set Docker image name (lowercase)
|
| 61 |
+
if: steps.dockerfile.outputs.present == 'true'
|
| 62 |
+
shell: bash
|
| 63 |
+
run: echo "IMAGE_NAME=$(echo '${{ github.event.repository.name }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
|
| 64 |
+
|
| 65 |
+
- name: Set up Docker Buildx
|
| 66 |
+
if: steps.dockerfile.outputs.present == 'true'
|
| 67 |
+
uses: docker/setup-buildx-action@v3
|
| 68 |
+
|
| 69 |
+
- name: Docker test (build test stage)
|
| 70 |
+
if: steps.dockerfile.outputs.present == 'true'
|
| 71 |
+
uses: docker/build-push-action@v6
|
| 72 |
+
with:
|
| 73 |
+
context: .
|
| 74 |
+
target: test
|
| 75 |
+
tags: ${{ env.IMAGE_NAME }}:test
|
| 76 |
+
cache-from: type=gha
|
| 77 |
+
cache-to: type=gha,mode=max
|
| 78 |
+
|
| 79 |
+
- name: Docker build (runtime)
|
| 80 |
+
if: steps.dockerfile.outputs.present == 'true'
|
| 81 |
+
uses: docker/build-push-action@v6
|
| 82 |
+
with:
|
| 83 |
+
context: .
|
| 84 |
+
target: runtime
|
| 85 |
+
tags: ${{ env.IMAGE_NAME }}:ci
|
| 86 |
+
cache-from: type=gha
|
| 87 |
+
cache-to: type=gha,mode=max
|
.gitignore
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# =========================
|
| 2 |
+
# Python
|
| 3 |
+
# =========================
|
| 4 |
+
__pycache__/
|
| 5 |
+
*.py[cod]
|
| 6 |
+
*.pyd
|
| 7 |
+
*.egg-info/
|
| 8 |
+
|
| 9 |
+
# Packaging / build
|
| 10 |
+
dist/
|
| 11 |
+
build/
|
| 12 |
+
pip-wheel-metadata/
|
| 13 |
+
|
| 14 |
+
# Virtual environments
|
| 15 |
+
.venv/
|
| 16 |
+
venv/
|
| 17 |
+
ENV/
|
| 18 |
+
.venv.bak/
|
| 19 |
+
venv.bak/
|
| 20 |
+
|
| 21 |
+
# Environment variables / secrets
|
| 22 |
+
.env
|
| 23 |
+
.env.*
|
| 24 |
+
!.env.example
|
| 25 |
+
|
| 26 |
+
# Caches
|
| 27 |
+
.pytest_cache/
|
| 28 |
+
.ruff_cache/
|
| 29 |
+
.mypy_cache/
|
| 30 |
+
.pytype/
|
| 31 |
+
.pyre/
|
| 32 |
+
|
| 33 |
+
# =========================
|
| 34 |
+
# Tests / coverage
|
| 35 |
+
# =========================
|
| 36 |
+
.coverage
|
| 37 |
+
coverage.xml
|
| 38 |
+
htmlcov/
|
| 39 |
+
.tox/
|
| 40 |
+
.nox/
|
| 41 |
+
*.cover
|
| 42 |
+
*.py,cover
|
| 43 |
+
|
| 44 |
+
# =========================
|
| 45 |
+
# Jupyter / notebooks
|
| 46 |
+
# =========================
|
| 47 |
+
.ipynb_checkpoints/
|
| 48 |
+
*.ipynb~
|
| 49 |
+
|
| 50 |
+
# =========================
|
| 51 |
+
# Streamlit (keep config, ignore secrets)
|
| 52 |
+
# =========================
|
| 53 |
+
.streamlit/secrets.toml
|
| 54 |
+
|
| 55 |
+
# Logs
|
| 56 |
+
*.log
|
| 57 |
+
logs/
|
| 58 |
+
|
| 59 |
+
# Local databases (optional but common)
|
| 60 |
+
*.db
|
| 61 |
+
*.sqlite
|
| 62 |
+
*.sqlite3
|
| 63 |
+
|
| 64 |
+
# =========================
|
| 65 |
+
# OS / IDE
|
| 66 |
+
# =========================
|
| 67 |
+
.DS_Store
|
| 68 |
+
Thumbs.db
|
| 69 |
+
.vscode/
|
| 70 |
+
.idea/
|
| 71 |
+
*.iml
|
| 72 |
+
.history/
|
| 73 |
+
|
| 74 |
+
# =========================
|
| 75 |
+
# Git
|
| 76 |
+
# =========================
|
| 77 |
+
*.orig
|
.pre-commit-config.yaml
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
default_language_version:
|
| 2 |
+
python: python3.11
|
| 3 |
+
|
| 4 |
+
repos:
|
| 5 |
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
| 6 |
+
rev: v0.14.10
|
| 7 |
+
hooks:
|
| 8 |
+
- id: ruff-check
|
| 9 |
+
args: [--fix]
|
| 10 |
+
- id: ruff-format
|
| 11 |
+
|
| 12 |
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
| 13 |
+
rev: v4.6.0
|
| 14 |
+
hooks:
|
| 15 |
+
- id: check-yaml
|
| 16 |
+
- id: end-of-file-fixer
|
| 17 |
+
- id: trailing-whitespace
|
| 18 |
+
- id: check-merge-conflict
|
| 19 |
+
- id: detect-private-key
|
| 20 |
+
- id: check-added-large-files
|
| 21 |
+
args: ["--maxkb=10240"]
|
.python-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
3.11.9
|
CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Contributing
|
| 2 |
+
|
| 3 |
+
This is a portfolio project. Issues and suggestions are welcome.
|
| 4 |
+
|
| 5 |
+
## Ways to contribute
|
| 6 |
+
- Report bugs (include steps to reproduce and logs).
|
| 7 |
+
- Suggest improvements (UX, performance, reliability).
|
| 8 |
+
- Propose small PRs (docs, tests, refactors).
|
| 9 |
+
- Security issues: please avoid posting sensitive details publicly.
|
| 10 |
+
|
| 11 |
+
## Dev setup
|
| 12 |
+
|
| 13 |
+
From the repo root (inside a virtual environment):
|
| 14 |
+
|
| 15 |
+
```bash
|
| 16 |
+
python -m pip install -r requirements.txt
|
| 17 |
+
python -m pip install -r requirements-dev.txt
|
| 18 |
+
python -m ruff check .
|
| 19 |
+
python -m ruff format --check .
|
| 20 |
+
python -m pytest -q
|
| 21 |
+
python -m streamlit run app.py
|
Makefile
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.PHONY: help install dev run lint lint-fix format test check precommit
|
| 2 |
+
|
| 3 |
+
PY ?= python
|
| 4 |
+
APP_FILE ?= app.py
|
| 5 |
+
|
| 6 |
+
help:
|
| 7 |
+
@echo "Targets:"
|
| 8 |
+
@echo " install Install runtime deps (requirements.txt if present)"
|
| 9 |
+
@echo " dev Install dev deps (requirements-dev.txt if present)"
|
| 10 |
+
@echo " run Run app (Streamlit by default)"
|
| 11 |
+
@echo " lint Run ruff lint"
|
| 12 |
+
@echo " lint-fix Run ruff lint with fixes"
|
| 13 |
+
@echo " format Format code with ruff"
|
| 14 |
+
@echo " check Lint + format check + tests"
|
| 15 |
+
@echo " test Run pytest"
|
| 16 |
+
@echo " precommit Install pre-commit hooks"
|
| 17 |
+
|
| 18 |
+
install:
|
| 19 |
+
$(PY) -m pip install -U pip
|
| 20 |
+
ifneq ($(wildcard requirements.txt),)
|
| 21 |
+
$(PY) -m pip install -r requirements.txt
|
| 22 |
+
endif
|
| 23 |
+
@$(PY) -m pip check || true
|
| 24 |
+
|
| 25 |
+
dev:
|
| 26 |
+
$(PY) -m pip install -U pip
|
| 27 |
+
ifneq ($(wildcard requirements-dev.txt),)
|
| 28 |
+
$(PY) -m pip install -r requirements-dev.txt
|
| 29 |
+
endif
|
| 30 |
+
@$(PY) -m pip check || true
|
| 31 |
+
|
| 32 |
+
run:
|
| 33 |
+
$(PY) -m streamlit run $(APP_FILE)
|
| 34 |
+
|
| 35 |
+
lint:
|
| 36 |
+
$(PY) -m ruff check .
|
| 37 |
+
|
| 38 |
+
lint-fix:
|
| 39 |
+
$(PY) -m ruff check . --fix --exit-non-zero-on-fix
|
| 40 |
+
|
| 41 |
+
format:
|
| 42 |
+
$(PY) -m ruff format .
|
| 43 |
+
|
| 44 |
+
test:
|
| 45 |
+
$(PY) -m pytest -q
|
| 46 |
+
|
| 47 |
+
check: lint
|
| 48 |
+
$(PY) -m ruff format --check .
|
| 49 |
+
$(PY) -m pytest -q
|
| 50 |
+
|
| 51 |
+
precommit:
|
| 52 |
+
$(PY) -m pip install -U pre-commit
|
| 53 |
+
pre-commit install
|
SECURITY.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Security Policy
|
| 2 |
+
|
| 3 |
+
## Reporting a Vulnerability
|
| 4 |
+
|
| 5 |
+
Please do **not** create a public GitHub issue for security vulnerabilities.
|
| 6 |
+
|
| 7 |
+
Instead, use GitHub's private reporting flow:
|
| 8 |
+
|
| 9 |
+
1. Go to this repository → **Security** tab
|
| 10 |
+
2. Open **Advisories**
|
| 11 |
+
3. Click **New draft security advisory**
|
| 12 |
+
4. Include:
|
| 13 |
+
- a short description of the issue
|
| 14 |
+
- steps to reproduce (proof-of-concept if safe)
|
| 15 |
+
- affected versions/commits (if known)
|
| 16 |
+
- impact assessment (what an attacker can do)
|
| 17 |
+
|
| 18 |
+
We will respond as soon as possible and coordinate a responsible disclosure.
|
pyproject.toml
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[tool.ruff]
|
| 2 |
+
line-length = 100
|
| 3 |
+
target-version = "py311"
|
| 4 |
+
extend-exclude = [
|
| 5 |
+
".venv",
|
| 6 |
+
"venv",
|
| 7 |
+
"__pycache__",
|
| 8 |
+
".pytest_cache",
|
| 9 |
+
".ruff_cache",
|
| 10 |
+
".mypy_cache",
|
| 11 |
+
"build",
|
| 12 |
+
"dist",
|
| 13 |
+
"artifacts",
|
| 14 |
+
"models",
|
| 15 |
+
"outputs",
|
| 16 |
+
"runs",
|
| 17 |
+
"data",
|
| 18 |
+
"datasets",
|
| 19 |
+
]
|
| 20 |
+
|
| 21 |
+
[tool.ruff.lint]
|
| 22 |
+
select = ["E", "F", "I", "B", "UP"]
|
| 23 |
+
ignore = ["E501"]
|
| 24 |
+
|
| 25 |
+
[tool.ruff.lint.isort]
|
| 26 |
+
known-first-party = []
|
| 27 |
+
combine-as-imports = true
|
| 28 |
+
|
| 29 |
+
[tool.ruff.lint.per-file-ignores]
|
| 30 |
+
"tests/**.py" = ["S101"] # allow assert in tests (safe)
|
| 31 |
+
|
| 32 |
+
[tool.ruff.format]
|
| 33 |
+
quote-style = "double"
|
| 34 |
+
indent-style = "space"
|
| 35 |
+
|
| 36 |
+
[tool.pytest.ini_options]
|
| 37 |
+
testpaths = ["tests"]
|
| 38 |
+
addopts = "-q"
|
requirements-dev.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-r requirements.txt
|
| 2 |
+
|
| 3 |
+
ruff==0.14.11
|
| 4 |
+
pytest==8.3.4
|
| 5 |
+
pre-commit==4.5.1
|
requirements.txt
CHANGED
|
@@ -1,6 +1,11 @@
|
|
| 1 |
-
streamlit
|
| 2 |
-
pandas
|
| 3 |
-
numpy
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
streamlit==1.52.2
|
| 2 |
+
pandas==2.3.3
|
| 3 |
+
numpy==2.4.1
|
| 4 |
+
scikit-learn==1.8.0
|
| 5 |
+
scipy==1.17.0
|
| 6 |
+
plotly==5.24.0
|
| 7 |
+
joblib==1.5.3
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
matplotlib>=3.8,<4
|
| 11 |
+
seaborn>=0.13,<0.14
|
scripts/doctor.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# scripts/doctor.py
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import os
|
| 5 |
+
import platform
|
| 6 |
+
import shutil
|
| 7 |
+
import subprocess
|
| 8 |
+
import sys
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def run(cmd: list[str]) -> int:
|
| 15 |
+
return subprocess.call(cmd, cwd=str(ROOT))
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def has_file(name: str) -> bool:
|
| 19 |
+
return (ROOT / name).exists()
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def which(exe: str) -> str | None:
|
| 23 |
+
return shutil.which(exe)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def banner(title: str) -> None:
|
| 27 |
+
print("\n" + "=" * 72)
|
| 28 |
+
print(title)
|
| 29 |
+
print("=" * 72)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def info(k: str, v: str) -> None:
|
| 33 |
+
print(f"{k:<24} {v}")
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def warn(msg: str) -> None:
|
| 37 |
+
print(f"[WARN] {msg}")
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def fail(msg: str) -> None:
|
| 41 |
+
print(f"[FAIL] {msg}")
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def ok(msg: str) -> None:
|
| 45 |
+
print(f"[OK] {msg}")
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def parse_min_python() -> tuple[int, int]:
|
| 49 |
+
# Default policy for the kit; override via env if needed.
|
| 50 |
+
v = os.getenv("MIN_PYTHON", "3.11").strip()
|
| 51 |
+
parts = v.split(".")
|
| 52 |
+
return int(parts[0]), int(parts[1])
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def check_python_version() -> bool:
|
| 56 |
+
min_major, min_minor = parse_min_python()
|
| 57 |
+
if sys.version_info >= (min_major, min_minor):
|
| 58 |
+
ok(f"Python version >= {min_major}.{min_minor} ({sys.version.split()[0]})")
|
| 59 |
+
return True
|
| 60 |
+
fail(f"Python too old: {sys.version.split()[0]} < {min_major}.{min_minor}")
|
| 61 |
+
return False
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def check_venv() -> None:
|
| 65 |
+
in_venv = sys.prefix != sys.base_prefix
|
| 66 |
+
info("In venv", str(in_venv))
|
| 67 |
+
if not in_venv:
|
| 68 |
+
warn("Not in a virtualenv. Recommended: create .venv and activate it.")
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def suggest_install() -> None:
|
| 72 |
+
banner("Suggested setup commands")
|
| 73 |
+
py = sys.executable
|
| 74 |
+
if has_file("requirements-dev.txt"):
|
| 75 |
+
print(f"{py} -m pip install -U pip")
|
| 76 |
+
print(f"{py} -m pip install -r requirements-dev.txt")
|
| 77 |
+
elif has_file("requirements.txt"):
|
| 78 |
+
print(f"{py} -m pip install -U pip")
|
| 79 |
+
print(f"{py} -m pip install -r requirements.txt")
|
| 80 |
+
warn("requirements-dev.txt not found (dev tooling may be missing).")
|
| 81 |
+
else:
|
| 82 |
+
warn("No requirements*.txt found.")
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def check_tools() -> None:
|
| 86 |
+
banner("Tooling")
|
| 87 |
+
py = sys.executable
|
| 88 |
+
# Try importing these only if installed; don't crash the script.
|
| 89 |
+
for mod in ["ruff", "pytest"]:
|
| 90 |
+
code = run([py, "-c", f"import {mod}"])
|
| 91 |
+
if code == 0:
|
| 92 |
+
ok(f"import {mod}")
|
| 93 |
+
else:
|
| 94 |
+
warn(f"{mod} not importable (install dev deps).")
|
| 95 |
+
|
| 96 |
+
pc = which("pre-commit")
|
| 97 |
+
if pc:
|
| 98 |
+
ok("pre-commit found in PATH")
|
| 99 |
+
else:
|
| 100 |
+
warn("pre-commit not found in PATH (optional, but recommended).")
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def run_checks() -> int:
|
| 104 |
+
banner("Running checks")
|
| 105 |
+
py = sys.executable
|
| 106 |
+
steps = [
|
| 107 |
+
([py, "-m", "ruff", "check", "."], "ruff check"),
|
| 108 |
+
([py, "-m", "ruff", "format", "--check", "."], "ruff format --check"),
|
| 109 |
+
([py, "-m", "pytest", "-q"], "pytest"),
|
| 110 |
+
]
|
| 111 |
+
rc = 0
|
| 112 |
+
for cmd, name in steps:
|
| 113 |
+
print(f"\n$ {' '.join(cmd)}")
|
| 114 |
+
code = run(cmd)
|
| 115 |
+
if code != 0:
|
| 116 |
+
fail(f"{name} failed (exit={code})")
|
| 117 |
+
rc = code
|
| 118 |
+
break
|
| 119 |
+
ok(f"{name} passed")
|
| 120 |
+
return rc
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def main() -> int:
|
| 124 |
+
banner("Doctor")
|
| 125 |
+
info("OS", f"{platform.system()} {platform.release()}")
|
| 126 |
+
info("Python", sys.version.splitlines()[0])
|
| 127 |
+
info("Repo", str(ROOT))
|
| 128 |
+
|
| 129 |
+
py_ok = check_python_version()
|
| 130 |
+
check_venv()
|
| 131 |
+
|
| 132 |
+
banner("Project files")
|
| 133 |
+
for f in ["pyproject.toml", "requirements.txt", "requirements-dev.txt", "Dockerfile"]:
|
| 134 |
+
info(f, "present" if has_file(f) else "missing")
|
| 135 |
+
|
| 136 |
+
check_tools()
|
| 137 |
+
suggest_install()
|
| 138 |
+
|
| 139 |
+
if not py_ok:
|
| 140 |
+
return 2
|
| 141 |
+
|
| 142 |
+
if "--check" in sys.argv:
|
| 143 |
+
return run_checks()
|
| 144 |
+
|
| 145 |
+
print("\nTip: run `python scripts/doctor.py --check`")
|
| 146 |
+
return 0
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
if __name__ == "__main__":
|
| 150 |
+
raise SystemExit(main())
|
tests/__pycache__/test_smoke.cpython-311-pytest-8.3.4.pyc
ADDED
|
Binary file (4.9 kB). View file
|
|
|
tests/test_smoke.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import importlib
|
| 2 |
+
import importlib.util
|
| 3 |
+
import os
|
| 4 |
+
import sys
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def _parse_version(v: str) -> tuple[int, int]:
|
| 8 |
+
parts = v.strip().split(".")
|
| 9 |
+
if len(parts) < 2:
|
| 10 |
+
raise ValueError("MIN_PYTHON must be like '3.10' or '3.11'")
|
| 11 |
+
return int(parts[0]), int(parts[1])
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def test_smoke_python_version():
|
| 15 |
+
min_py = os.getenv("MIN_PYTHON", "3.11")
|
| 16 |
+
assert sys.version_info >= _parse_version(min_py)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def test_smoke_core_imports():
|
| 20 |
+
"""
|
| 21 |
+
Fast fail if core dependencies are broken.
|
| 22 |
+
|
| 23 |
+
Configure via:
|
| 24 |
+
SMOKE_IMPORTS="numpy,pandas,sklearn,streamlit,plotly"
|
| 25 |
+
"""
|
| 26 |
+
imports = os.getenv("SMOKE_IMPORTS", "")
|
| 27 |
+
if not imports.strip():
|
| 28 |
+
return
|
| 29 |
+
|
| 30 |
+
for mod in [m.strip() for m in imports.split(",") if m.strip()]:
|
| 31 |
+
importlib.import_module(mod)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def test_smoke_project_module_importable():
|
| 35 |
+
"""
|
| 36 |
+
Ensure the main project module imports.
|
| 37 |
+
Configure via:
|
| 38 |
+
PROJECT_MODULE="your_package_name"
|
| 39 |
+
"""
|
| 40 |
+
module = os.getenv("PROJECT_MODULE", "").strip()
|
| 41 |
+
if not module:
|
| 42 |
+
return
|
| 43 |
+
importlib.import_module(module)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def test_smoke_app_importable():
|
| 47 |
+
"""
|
| 48 |
+
Importing the app module should not execute heavy work at import-time.
|
| 49 |
+
|
| 50 |
+
This kit doesn't ship `app.py` because it varies per project. So we:
|
| 51 |
+
- Try to import the module if present
|
| 52 |
+
- Otherwise, skip silently (new project will add app.py later)
|
| 53 |
+
|
| 54 |
+
Override via:
|
| 55 |
+
APP_MODULE="app" (default)
|
| 56 |
+
"""
|
| 57 |
+
app_module = os.getenv("APP_MODULE", "app").strip()
|
| 58 |
+
|
| 59 |
+
# If the module isn't present yet, skip.
|
| 60 |
+
if importlib.util.find_spec(app_module) is None:
|
| 61 |
+
return
|
| 62 |
+
|
| 63 |
+
importlib.import_module(app_module)
|