diff --git a/.claude/commands/fix-lint.md b/.claude/commands/fix-lint.md index d31f760..3f708ac 100644 --- a/.claude/commands/fix-lint.md +++ b/.claude/commands/fix-lint.md @@ -5,6 +5,6 @@ Fix all lint errors in the project. 1. Run `uv run poe lint` again to check remaining issues 1. Manually fix any issues Ruff couldn't auto-fix 1. Run `uv run poe typecheck` to verify no type errors introduced -1. Run `uv run poe test` to verify fixes didn't break anything +1. Run `uv run poe check-all` to verify fixes didn't break anything Report what was fixed and what remains (if anything). diff --git a/.claude/commands/release.md b/.claude/commands/release.md index bc3a28c..e731a51 100644 --- a/.claude/commands/release.md +++ b/.claude/commands/release.md @@ -1,6 +1,6 @@ # Release -Release is fully automated via **release-please**. Claude's role is to verify readiness and monitor the pipeline — never to create tags, bump versions manually, or trigger workflows directly. +Release is intended to be automated via **release-please** (not yet configured). Claude's role is to verify readiness and monitor the pipeline — never to create tags, bump versions manually, or trigger workflows directly. ## How it works @@ -22,6 +22,12 @@ conventional commits merged to main 1. **Verify release-please app triggered** + ```bash + gh run list --workflow=reusable-release-please.yml --limit 5 + ``` + +1. **Check for open Release PR** + ```bash gh pr list --label "autorelease: pending" --limit 5 ``` diff --git a/.claude/commands/validate.md b/.claude/commands/validate.md index 42ee370..7d5de7e 100644 --- a/.claude/commands/validate.md +++ b/.claude/commands/validate.md @@ -25,7 +25,7 @@ Run the full validation pipeline for the current repo. Stop and report on first 1. **Tests** ```bash - uv run poe test + uv run poe check-all # fallback: uv run pytest tests/ -x --tb=short -q ``` diff --git a/.claude/memory/long-term-memory.md b/.claude/memory/long-term-memory.md index 34515de..241ba19 100644 --- a/.claude/memory/long-term-memory.md +++ b/.claude/memory/long-term-memory.md @@ -96,5 +96,5 @@ _Nothing yet._ - **Source:** - **Finding:** uv 0.10.8 switches default binary/CPython mirror from python.org to Astral mirrors. Raw `curl | sh` uv installs in CI are unpinned. -- **Impact:** Replaced `curl -LsSf https://astral.sh/uv/install.sh | sh` with `astral-sh/setup-uv@v4` in `.github/workflows/ci.yml` (both jobs). +- **Impact:** Replaced `curl -LsSf https://astral.sh/uv/install.sh | sh` with `astral-sh/setup-uv@v7` in `.github/workflows/ci.yml` (both jobs) and `.github/workflows/package-validation.yml`. Note: these workflows were removed in the 2026-07-16 `.github` cleanup; the UV setup pattern now lives in individual repos (`dataenginex`, `dex-studio`). - **Status:** fixed — 2026-03-19 diff --git a/.claude/memory/project-memory.md b/.claude/memory/project-memory.md new file mode 100644 index 0000000..2aa5204 --- /dev/null +++ b/.claude/memory/project-memory.md @@ -0,0 +1,25 @@ +# Project Memory (active state) + +> Tracks in-flight work, open decisions, and cross-repo state. +> Updated by consolidate-memory skill nightly. +> Facts here are ephemeral — they get promoted or dropped, never age indefinitely. + +## Active Tasks & In-Flight Work + +_Nothing yet._ + +## Open Decisions (unresolved) + +_Nothing yet._ + +## Cross-Repo State + +| Repo | Status | Last Known Action | +|------|--------|------------------| +| dataenginex (dex) | — | — | +| dex-studio | — | — | +| infradex | — | — | + +## Blockers + +_None._ diff --git a/.github/workflows/WORKFLOWS_GUIDE.md b/.github/workflows/WORKFLOWS_GUIDE.md new file mode 100644 index 0000000..e611670 --- /dev/null +++ b/.github/workflows/WORKFLOWS_GUIDE.md @@ -0,0 +1,60 @@ +# GitHub Actions Workflows + +CI/CD automation for the DataEngineX `.github` meta-repo. +Reusable workflows shared across all repos are also in this directory (prefixed `reusable-`). + +## Workflows (run in `.github`) + +### `security.yml` — Security Scans +**Triggers**: Push/PR to `main`/`dev` + +Runs Trivy (misconfig + secret scan). CodeQL is handled by GitHub's default setup. + +### `sync-labels.yml` — Label Synchronization +**Triggers**: Push to `main` on `labels.yml` changes, manual dispatch + +Syncs labels from `labels.yml` to all repos (dataenginex, dex-studio, infradex). + +### `project-automation.yml` — Project Intake Automation +**Triggers**: Issue/PR opened/reopened events, manual dispatch + +Adds new issues/PRs to org project board. Requires `ORG_PROJECT_URL` variable + `ORG_PROJECT_TOKEN` secret. + +### `stale-issues.yml` — Stale Issue Management +**Triggers**: Scheduled (daily at 6 AM UTC) + +Marks issues/PRs stale after 60 days, closes after 30 more. + +## Reusable Workflows (called by other repos) + +| Workflow | Callers | Purpose | +|----------|---------|---------| +| `reusable-security.yml` | dataenginex, dex-studio, infradex | Trivy scan + Slack notifications | +| `reusable-auto-pr-to-main.yml` | dataenginex, dex-studio | Auto-create PR from feature/fix → main | +| `reusable-auto-pr-to-dev.yml` | (available) | Auto-create PR from feature/fix → dev + auto-merge | +| `reusable-auto-pr-dev-to-main.yml` | (available) | Auto-create PR from dev → main | +| `reusable-enforce-dev-to-main.yml` | (available) | Verify PRs to main come from dev | +| `reusable-claude.yml` | (available) | Claude Code action for @claude mentions | +| `reusable-release-please.yml` | (available) | Release please automation | + +## Quick Reference + +```bash +# Check CI status +gh pr checks + +# View workflow runs +gh run list --workflow security.yml + +# Trigger manual label sync +gh workflow run sync-labels.yml +``` + +## Required Secrets + +| Secret | Used by | +|--------|---------| +| `GITHUB_TOKEN` | Auto-provided | +| `ORG_PROJECT_TOKEN` | project-automation.yml | +| `LABELS_SYNC_TOKEN` | sync-labels.yml | +| `SLACK_WEBHOOK` | security.yml (optional) | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8361d4a..60105cc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,16 +23,16 @@ jobs: markdownlint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - uses: DavidAnson/markdownlint-cli2-action@v19 with: globs: "**/*.md" - config: ".github/.markdownlint-cli2.jsonc" + config: ".markdownlint-cli2.jsonc" linkcheck: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - uses: lycheeverse/lychee-action@v2 with: fail: true diff --git a/.github/workflows/project-automation.yml b/.github/workflows/project-automation.yml new file mode 100644 index 0000000..15c6433 --- /dev/null +++ b/.github/workflows/project-automation.yml @@ -0,0 +1,49 @@ +name: Project Automation + +on: + issues: + types: + - opened + - reopened + - transferred + pull_request: + types: + - opened + - reopened + - ready_for_review + workflow_dispatch: + +permissions: + contents: read + issues: write + pull-requests: write + +jobs: + add-to-org-project: + name: Add item to organization project + runs-on: ubuntu-latest + env: + ORG_PROJECT_URL: ${{ vars.ORG_PROJECT_URL }} + ORG_PROJECT_TOKEN: ${{ secrets.ORG_PROJECT_TOKEN }} + + steps: + - name: Add issue/PR to organization project + if: ${{ env.ORG_PROJECT_URL != '' && env.ORG_PROJECT_TOKEN != '' }} + id: add_to_project + continue-on-error: true + uses: actions/add-to-project@v1.0.2 + with: + project-url: ${{ env.ORG_PROJECT_URL }} + github-token: ${{ env.ORG_PROJECT_TOKEN }} + + - name: Explain action failure + if: ${{ steps.add_to_project.outcome == 'failure' }} + run: | + echo "Project automation failed but is non-blocking." + echo "Check ORG_PROJECT_URL and ORG_PROJECT_TOKEN scopes/permissions." + + - name: Explain skipped automation + if: ${{ env.ORG_PROJECT_URL == '' || env.ORG_PROJECT_TOKEN == '' }} + run: | + echo "Project automation skipped." + echo "Set repository/org variable ORG_PROJECT_URL and secret ORG_PROJECT_TOKEN to enable." diff --git a/workflows/reusable-auto-pr-to-main.yml b/.github/workflows/reusable-auto-pr-dev-to-main.yml similarity index 53% rename from workflows/reusable-auto-pr-to-main.yml rename to .github/workflows/reusable-auto-pr-dev-to-main.yml index 8a69a52..ae08d8c 100644 --- a/workflows/reusable-auto-pr-to-main.yml +++ b/.github/workflows/reusable-auto-pr-dev-to-main.yml @@ -1,4 +1,4 @@ -name: Auto PR — branch → main (reusable) +name: Auto PR — dev → main (reusable) on: workflow_call: @@ -12,25 +12,21 @@ jobs: pull-requests: write steps: - - name: Create or update PR + - name: Create or update PR (dev → main) env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_REPO: ${{ github.repository }} - HEAD: ${{ github.ref_name }} run: | - if echo "$HEAD" | grep -qE '^fix[-/]'; then - TITLE="fix: $(echo "$HEAD" | sed 's|^fix[-/]||;s|-| |g')" - elif echo "$HEAD" | grep -qE '^feat[-/]'; then - TITLE="feat: $(echo "$HEAD" | sed 's|^feat[-/]||;s|-| |g')" - else - TITLE="feat: $(echo "$HEAD" | sed 's|-| |g')" + AHEAD=$(gh api "repos/$GH_REPO/compare/main...dev" --jq '.ahead_by') + if [ "$AHEAD" = "0" ]; then + echo "dev is already in sync with main — nothing to do" + exit 0 fi - - echo "PR title: $TITLE" + echo "dev is $AHEAD commit(s) ahead of main" PR_NUMBER=$(gh pr list \ --base main \ - --head "$HEAD" \ + --head dev \ --state open \ --json number \ --jq '.[0].number') @@ -38,16 +34,15 @@ jobs: if [ -n "$PR_NUMBER" ]; then echo "PR #$PR_NUMBER already exists" else - BODY="Automated PR: ${HEAD} → main. Review the preview environment before merging." gh pr create \ --base main \ - --head "$HEAD" \ - --title "$TITLE" \ - --body "$BODY" + --head dev \ + --title "chore: sync dev -> main" \ + --body "Automated PR: dev → main. Merge when all required checks pass." PR_NUMBER=$(gh pr list \ --base main \ - --head "$HEAD" \ + --head dev \ --state open \ --json number \ --jq '.[0].number') diff --git a/.github/workflows/reusable-auto-pr-to-dev.yml b/.github/workflows/reusable-auto-pr-to-dev.yml new file mode 100644 index 0000000..3a118d3 --- /dev/null +++ b/.github/workflows/reusable-auto-pr-to-dev.yml @@ -0,0 +1,77 @@ +# Reusable workflow — auto-creates a PR from a feature/fix branch → dev and enables +# auto-merge. The squash commit title is derived from the branch name in conventional +# commit format (feature/feat-add-foo → "feat: add foo"). +# +# Prerequisites (one-time per repo, GitHub UI): +# 1. Settings → General → Allow auto-merge ✓ +# 2. Settings → Branches → branch protection on 'dev': +# - Require status checks: CI (lint-and-test) +# - Require branches to be up to date before merging + +name: Auto PR — feature → dev (reusable) + +on: + workflow_call: + +jobs: + create-pr: + name: Create PR to dev + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: Create or update PR (feature/fix → dev) + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + HEAD: ${{ github.ref_name }} + run: | + # Derive conventional commit title from branch name + # feature/feat-add-pipeline → "feat: add pipeline" + # feature/add-pipeline → "feat: add pipeline" + # fix/fix-null-ptr → "fix: null ptr" + # fix/null-ptr → "fix: null ptr" + BRANCH_DESC=$(echo "$HEAD" | sed 's|^feature/||;s|^fix/||') + if echo "$BRANCH_DESC" | grep -qE '^feat-'; then + TITLE="feat: $(echo "$BRANCH_DESC" | sed 's|^feat-||;s|-| |g')" + elif echo "$BRANCH_DESC" | grep -qE '^fix-'; then + TITLE="fix: $(echo "$BRANCH_DESC" | sed 's|^fix-||;s|-| |g')" + elif echo "$HEAD" | grep -qE '^fix/'; then + TITLE="fix: $(echo "$BRANCH_DESC" | sed 's|-| |g')" + else + TITLE="feat: $(echo "$BRANCH_DESC" | sed 's|-| |g')" + fi + + echo "PR title: $TITLE" + + # Check for existing open PR + PR_NUMBER=$(gh pr list \ + --base dev \ + --head "$HEAD" \ + --state open \ + --json number \ + --jq '.[0].number') + + if [ -n "$PR_NUMBER" ]; then + echo "PR #$PR_NUMBER already exists" + else + BODY="Automated PR: ${HEAD} -> dev. Auto-merge enabled - squash-merges when all required checks pass. Edit the PR title to a conventional commit format if needed." + gh pr create \ + --base dev \ + --head "$HEAD" \ + --title "$TITLE" \ + --body "$BODY" + + PR_NUMBER=$(gh pr list \ + --base dev \ + --head "$HEAD" \ + --state open \ + --json number \ + --jq '.[0].number') + echo "Created PR #$PR_NUMBER" + fi + + gh pr merge "$PR_NUMBER" --auto --squash + echo "Auto-merge (squash) enabled on PR #$PR_NUMBER" diff --git a/.github/workflows/reusable-auto-pr-to-main.yml b/.github/workflows/reusable-auto-pr-to-main.yml index 290e392..f5689f0 100644 --- a/.github/workflows/reusable-auto-pr-to-main.yml +++ b/.github/workflows/reusable-auto-pr-to-main.yml @@ -1,36 +1,62 @@ -name: Reusable Auto PR to Main +name: Auto PR — feature → main (reusable) on: workflow_call: jobs: - auto-pr: + create-pr: name: Create PR to main runs-on: ubuntu-latest permissions: contents: write pull-requests: write - steps: - - uses: actions/checkout@v4 - - name: Create or update PR to main + steps: + - name: Create or update PR (feature/fix → main) env: - GH_TOKEN: ${{ github.token }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + HEAD: ${{ github.ref_name }} run: | - BRANCH="${{ github.ref_name }}" - REPO="${{ github.repository }}" + # Derive conventional commit title from branch name + # feature/feat-add-pipeline → "feat: add pipeline" + # feature/add-pipeline → "feat: add pipeline" + # fix/fix-null-ptr → "fix: null ptr" + BRANCH_DESC=$(echo "$HEAD" | sed 's|^feature/||;s|^fix/||') + if echo "$BRANCH_DESC" | grep -qE '^feat-'; then + TITLE="feat: $(echo "$BRANCH_DESC" | sed 's|^feat-||;s|-| |g')" + elif echo "$BRANCH_DESC" | grep -qE '^fix-'; then + TITLE="fix: $(echo "$BRANCH_DESC" | sed 's|^fix-||;s|-| |g')" + elif echo "$HEAD" | grep -qE '^fix/'; then + TITLE="fix: $(echo "$BRANCH_DESC" | sed 's|-| |g')" + else + TITLE="feat: $(echo "$BRANCH_DESC" | sed 's|-| |g')" + fi - # Check if PR already exists - EXISTING=$(gh pr list --repo "$REPO" --head "$BRANCH" --base main --json number --jq '.[0].number') + echo "PR title: $TITLE" - if [ -n "$EXISTING" ]; then - echo "PR #$EXISTING already exists for $BRANCH → main" + PR_NUMBER=$(gh pr list \ + --base main \ + --head "$HEAD" \ + --state open \ + --json number \ + --jq '.[0].number') + + if [ -n "$PR_NUMBER" ]; then + echo "PR #$PR_NUMBER already exists" else + BODY="Automated PR: ${HEAD} → main. Review the preview environment before merging." gh pr create \ - --repo "$REPO" \ - --head "$BRANCH" \ --base main \ - --title "chore: merge \`$BRANCH\` → main" \ - --body "Automated PR: merging \`$BRANCH\` into \`main\`." \ - --label "automated-pr" || true + --head "$HEAD" \ + --title "$TITLE" \ + --body "$BODY" + + PR_NUMBER=$(gh pr list \ + --base main \ + --head "$HEAD" \ + --state open \ + --json number \ + --jq '.[0].number') + echo "Created PR #$PR_NUMBER" fi diff --git a/.github/workflows/reusable-claude.yml b/.github/workflows/reusable-claude.yml new file mode 100644 index 0000000..41a1b71 --- /dev/null +++ b/.github/workflows/reusable-claude.yml @@ -0,0 +1,35 @@ +# Reusable workflow — called from each repo's .github/workflows/claude.yml +# Logic lives here once; repo files are thin callers. + +name: Claude Code (reusable) + +on: + workflow_call: + +jobs: + claude: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + actions: read + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 1 + + - name: Run Claude Code + id: claude + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + additional_permissions: | + actions: read diff --git a/.github/workflows/reusable-enforce-dev-to-main.yml b/.github/workflows/reusable-enforce-dev-to-main.yml new file mode 100644 index 0000000..d5f4741 --- /dev/null +++ b/.github/workflows/reusable-enforce-dev-to-main.yml @@ -0,0 +1,24 @@ +# Reusable workflow — called from each repo's .github/workflows/enforce-dev-to-main.yml +# Logic lives here once; repo files are thin callers. + +name: Enforce dev-to-main (reusable) + +on: + workflow_call: + +jobs: + check-source-branch: + name: Verify PR source is dev + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Check source branch + run: | + branch="${{ github.head_ref }}" + if [ "$branch" != "dev" ] && [[ "$branch" != release-please--* ]]; then + echo "PRs to main must come from 'dev' or a release-please branch." + echo "Source branch: $branch" + exit 1 + fi + echo "Source branch is '$branch' — OK" diff --git a/.github/workflows/reusable-release-please.yml b/.github/workflows/reusable-release-please.yml new file mode 100644 index 0000000..d2a129a --- /dev/null +++ b/.github/workflows/reusable-release-please.yml @@ -0,0 +1,72 @@ +# Reusable workflow — called from each repo's .github/workflows/release-please.yml +# +# On push to main, release-please: +# 1. Reads conventional commits since the last release tag +# 2. Creates/updates a Release PR that bumps pyproject.toml version + CHANGELOG.md +# 3. When the Release PR is merged → creates git tag + GitHub Release +# +# Uses RELEASE_TOKEN (PAT) instead of GITHUB_TOKEN so the release:published +# event fires and triggers downstream workflows (e.g. pypi-publish). +# GITHUB_TOKEN-created releases suppress same-repo webhook events. +# +# Required secret: RELEASE_TOKEN — PAT with contents:write + pull-requests:write +# +# After a Release PR is opened/updated, uv.lock is regenerated and committed +# to the PR branch so the lock file stays in sync with the version bump. +# +# Callers need only: +# jobs: +# release: +# uses: TheDataEngineX/.github/workflows/release-please.yml@main +# secrets: inherit + +name: Release Please (reusable) + +on: + workflow_call: + +jobs: + release-please: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - uses: googleapis/release-please-action@v4 + id: release-please + with: + token: ${{ secrets.RELEASE_TOKEN }} + config-file: release-please-config.json + manifest-file: .release-please-manifest.json + + # ── Keep uv.lock in sync with version bump ────────────────────────────── + # When release-please opens or updates a Release PR, the PR bumps + # pyproject.toml but not uv.lock. This step checks out the PR branch, + # regenerates uv.lock, and pushes the update back. + - name: Checkout Release PR branch + if: ${{ steps.release-please.outputs.pr }} + uses: actions/checkout@v6 + with: + ref: ${{ fromJson(steps.release-please.outputs.pr).headBranchName }} + token: ${{ secrets.RELEASE_TOKEN }} + + - name: Set up uv + if: ${{ steps.release-please.outputs.pr }} + uses: astral-sh/setup-uv@v8.1.0 + with: + version: "latest" + + - name: Regenerate uv.lock for version bump + if: ${{ steps.release-please.outputs.pr }} + run: | + uv lock + if git diff --quiet uv.lock; then + echo "uv.lock already up to date — no commit needed" + else + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add uv.lock + git commit -m "chore: regenerate uv.lock for version bump" + git push + fi diff --git a/.github/workflows/reusable-security.yml b/.github/workflows/reusable-security.yml index 66a1baa..1a4f8bf 100644 --- a/.github/workflows/reusable-security.yml +++ b/.github/workflows/reusable-security.yml @@ -1,22 +1,169 @@ -name: Reusable Security Scan +# Reusable workflow — called from each repo's .github/workflows/security.yml +# Logic lives here once; repo files are thin callers. +# +# Runs Trivy (misconfig + secret scan) on push/PR. +# CodeQL is handled by GitHub's default setup — do NOT add a custom codeql job +# (GitHub rejects SARIF uploads when default setup is enabled). +# Results appear in GitHub Security tab. + +name: Security Scans (reusable) on: workflow_call: +env: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + jobs: trivy-security: - name: Trivy Security Scan runs-on: ubuntu-latest permissions: contents: read + security-events: write steps: - - uses: actions/checkout@v4 + - name: Notify Security Scan Start + uses: actions/github-script@v9 + if: always() + with: + script: | + if (!process.env.SLACK_WEBHOOK) return; + try { + await fetch(process.env.SLACK_WEBHOOK, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + text: '🔒 Security Scans Started', + blocks: [{ + type: 'section', + text: { + type: 'mrkdwn', + text: `*🔒 Security Scans Started*\n*Branch:* ${process.env.REF}\n*Tools:* Trivy (repo)` + } + }] + }) + }); + } catch (e) { console.log('Slack notification failed'); } + env: + SLACK_WEBHOOK: ${{ env.SLACK_WEBHOOK }} + REF: ${{ github.ref_name }} + + - uses: actions/checkout@v6 + - name: Run Trivy repo security scan + uses: aquasecurity/trivy-action@v0.36.0 + with: + scan-type: fs + scan-ref: . + scanners: misconfig,secret + format: sarif + output: trivy-repo-results.sarif + severity: HIGH,CRITICAL + exit-code: '0' + + - name: Upload Trivy repo results to GitHub Security + if: always() + uses: github/codeql-action/upload-sarif@v4 + with: + sarif_file: trivy-repo-results.sarif - - name: Run Trivy vulnerability scanner + - name: Enforce Trivy misconfig gate (HIGH/CRITICAL) uses: aquasecurity/trivy-action@v0.36.0 with: scan-type: fs scan-ref: . + scanners: misconfig format: table - severity: CRITICAL,HIGH - exit-code: "0" + severity: HIGH,CRITICAL + exit-code: '1' + + notify-security: + name: Notify Security Status + runs-on: ubuntu-latest + needs: [trivy-security] + if: always() + permissions: + contents: read + + steps: + - name: Notify Security Success + if: ${{ needs.trivy-security.result == 'success' }} + uses: actions/github-script@v9 + with: + script: | + if (!process.env.SLACK_WEBHOOK) return; + try { + await fetch(process.env.SLACK_WEBHOOK, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + text: '✅ Security Scans Passed', + blocks: [ + { + type: 'section', + text: { + type: 'mrkdwn', + text: `*✅ Security Scans Completed*` + } + }, + { + type: 'section', + fields: [ + { type: 'mrkdwn', text: `*Branch:*\n${process.env.REF}` }, + { type: 'mrkdwn', text: `*Trivy:*\n✅ Passed` }, + { type: 'mrkdwn', text: `*Run:*\n<${process.env.URL}|View Details>` } + ] + } + ] + }) + }); + } catch (e) { console.log('Slack notification failed:', e.message); } + env: + SLACK_WEBHOOK: ${{ env.SLACK_WEBHOOK }} + REF: ${{ github.ref_name }} + URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + + - name: Notify Security Issues + if: ${{ needs.trivy-security.result == 'failure' }} + uses: actions/github-script@v9 + with: + script: | + if (!process.env.SLACK_WEBHOOK) return; + try { + await fetch(process.env.SLACK_WEBHOOK, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + text: '⚠️ Security Issues Detected', + blocks: [ + { + type: 'section', + text: { + type: 'mrkdwn', + text: `*⚠️ Security Issues Detected*` + } + }, + { + type: 'section', + fields: [ + { type: 'mrkdwn', text: `*Workflow:*\nSecurity Scans` }, + { type: 'mrkdwn', text: `*Branch:*\n${process.env.REF}` }, + { type: 'mrkdwn', text: `*Commit:*\n\`${process.env.SHA?.substring(0, 8)}\`` }, + { type: 'mrkdwn', text: `*Triggered by:*\n${process.env.ACTOR}` }, + { type: 'mrkdwn', text: `*Event:*\n${process.env.EVENT}` }, + { type: 'mrkdwn', text: `*Run:*\n<${process.env.URL}|View Details>` } + ] + }, + { + type: 'context', + elements: [{ type: 'mrkdwn', text: `🔍 Trivy found issues — review security tab for findings` }] + } + ] + }) + }); + } catch (e) { console.log('Slack notification failed:', e.message); } + env: + SLACK_WEBHOOK: ${{ env.SLACK_WEBHOOK }} + REF: ${{ github.ref_name }} + SHA: ${{ github.sha }} + ACTOR: ${{ github.actor }} + EVENT: ${{ github.event_name }} + URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 0000000..b6d8eaf --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,169 @@ +# Security Scans for the .github meta-repo. +# Runs Trivy (misconfig + secret scan) on push/PR. +# CodeQL is handled by GitHub's default setup — do NOT add a custom codeql job. + +name: Security Scans + +on: + push: + branches: [ main, dev ] + pull_request: + branches: [ main, dev ] + +permissions: + contents: read + security-events: write + +env: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + +jobs: + trivy-security: + runs-on: ubuntu-latest + steps: + - name: Notify Security Scan Start + uses: actions/github-script@v9 + if: always() + with: + script: | + if (!process.env.SLACK_WEBHOOK) return; + try { + await fetch(process.env.SLACK_WEBHOOK, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + text: '🔒 Security Scans Started', + blocks: [{ + type: 'section', + text: { + type: 'mrkdwn', + text: `*🔒 Security Scans Started*\n*Branch:* ${process.env.REF}\n*Tools:* Trivy (repo)` + } + }] + }) + }); + } catch (e) { console.log('Slack notification failed'); } + env: + SLACK_WEBHOOK: ${{ env.SLACK_WEBHOOK }} + REF: ${{ github.ref_name }} + + - uses: actions/checkout@v6 + - name: Run Trivy repo security scan + uses: aquasecurity/trivy-action@v0.36.0 + with: + scan-type: fs + scan-ref: . + scanners: misconfig,secret + format: sarif + output: trivy-repo-results.sarif + severity: HIGH,CRITICAL + exit-code: '0' + + - name: Upload Trivy repo results to GitHub Security + if: always() + uses: github/codeql-action/upload-sarif@v4 + with: + sarif_file: trivy-repo-results.sarif + + - name: Enforce Trivy misconfig gate (HIGH/CRITICAL) + uses: aquasecurity/trivy-action@v0.36.0 + with: + scan-type: fs + scan-ref: . + scanners: misconfig + format: table + severity: HIGH,CRITICAL + exit-code: '1' + + notify-security: + name: Notify Security Status + runs-on: ubuntu-latest + needs: [trivy-security] + if: always() + permissions: + contents: read + + steps: + - name: Notify Security Success + if: ${{ needs.trivy-security.result == 'success' }} + uses: actions/github-script@v9 + with: + script: | + if (!process.env.SLACK_WEBHOOK) return; + try { + await fetch(process.env.SLACK_WEBHOOK, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + text: '✅ Security Scans Passed', + blocks: [ + { + type: 'section', + text: { + type: 'mrkdwn', + text: `*✅ Security Scans Completed*` + } + }, + { + type: 'section', + fields: [ + { type: 'mrkdwn', text: `*Branch:*\n${process.env.REF}` }, + { type: 'mrkdwn', text: `*Trivy:*\n✅ Passed` }, + { type: 'mrkdwn', text: `*Run:*\n<${process.env.URL}|View Details>` } + ] + } + ] + }) + }); + } catch (e) { console.log('Slack notification failed:', e.message); } + env: + SLACK_WEBHOOK: ${{ env.SLACK_WEBHOOK }} + REF: ${{ github.ref_name }} + URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + + - name: Notify Security Issues + if: ${{ needs.trivy-security.result == 'failure' }} + uses: actions/github-script@v9 + with: + script: | + if (!process.env.SLACK_WEBHOOK) return; + try { + await fetch(process.env.SLACK_WEBHOOK, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + text: '⚠️ Security Issues Detected', + blocks: [ + { + type: 'section', + text: { + type: 'mrkdwn', + text: `*⚠️ Security Issues Detected*` + } + }, + { + type: 'section', + fields: [ + { type: 'mrkdwn', text: `*Workflow:*\nSecurity Scans` }, + { type: 'mrkdwn', text: `*Branch:*\n${process.env.REF}` }, + { type: 'mrkdwn', text: `*Commit:*\n\`${process.env.SHA?.substring(0, 8)}\`` }, + { type: 'mrkdwn', text: `*Triggered by:*\n${process.env.ACTOR}` }, + { type: 'mrkdwn', text: `*Event:*\n${process.env.EVENT}` }, + { type: 'mrkdwn', text: `*Run:*\n<${process.env.URL}|View Details>` } + ] + }, + { + type: 'context', + elements: [{ type: 'mrkdwn', text: `🔍 Trivy found issues — review security tab for findings` }] + } + ] + }) + }); + } catch (e) { console.log('Slack notification failed:', e.message); } + env: + SLACK_WEBHOOK: ${{ env.SLACK_WEBHOOK }} + REF: ${{ github.ref_name }} + SHA: ${{ github.sha }} + ACTOR: ${{ github.actor }} + EVENT: ${{ github.event_name }} + URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml new file mode 100644 index 0000000..fa55c7c --- /dev/null +++ b/.github/workflows/stale-issues.yml @@ -0,0 +1,33 @@ +name: Stale Issues + +on: + schedule: + - cron: "0 6 * * *" # Daily at 6 AM UTC + workflow_dispatch: + +jobs: + stale: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/stale@v9 + with: + stale-issue-message: > + This issue has been automatically marked as stale because it has not + had activity in the last 60 days. It will be closed in 30 days if no + further activity occurs. If this issue is still relevant, please + comment or update it. + stale-pr-message: > + This pull request has been automatically marked as stale because it + has not had activity in the last 60 days. It will be closed in 30 days + if no further activity occurs. + close-issue-message: "Closed due to inactivity. Reopen if still relevant." + close-pr-message: "Closed due to inactivity. Reopen if still relevant." + days-before-stale: 60 + days-before-close: 30 + stale-issue-label: "stale" + stale-pr-label: "stale" + exempt-issue-labels: "priority: P1,status: blocked" + exempt-pr-labels: "priority: P1,status: blocked" diff --git a/.github/workflows/sync-labels.yml b/.github/workflows/sync-labels.yml new file mode 100644 index 0000000..e1cd7e1 --- /dev/null +++ b/.github/workflows/sync-labels.yml @@ -0,0 +1,53 @@ +name: Sync Labels + +on: + push: + branches: [main] + paths: + - labels.yml + workflow_dispatch: + +permissions: + contents: read + +jobs: + sync: + name: Sync labels to ${{ matrix.repo }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + repo: + - dataenginex + - dex-studio + - infradex + steps: + - uses: actions/checkout@v6 + + - uses: astral-sh/setup-uv@v8.1.0 + with: + version: "latest" + + - name: Sync labels to ${{ matrix.repo }} + env: + GH_TOKEN: ${{ secrets.LABELS_SYNC_TOKEN }} + run: | + uv pip install pyyaml -q + python3 - <<'PYEOF' + import json, subprocess, yaml + with open('labels.yml') as f: + labels = yaml.safe_load(f) or [] + repo = "TheDataEngineX/${{ matrix.repo }}" + r = subprocess.run( + ['gh', 'label', 'list', '--repo', repo, '--json', 'name', '--limit', '200'], + capture_output=True, text=True, check=True + ) + existing = {l['name'] for l in json.loads(r.stdout)} + for label in labels: + name = label.get('name', '') + color = str(label.get('color', '')).lstrip('#') + desc = label.get('description', '') or '' + cmd = ['gh', 'label', 'edit' if name in existing else 'create', + name, '--repo', repo, '--color', color, '--description', desc] + subprocess.run(cmd, check=True) + PYEOF diff --git a/.github/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc similarity index 88% rename from .github/.markdownlint-cli2.jsonc rename to .markdownlint-cli2.jsonc index 8859cfe..948de44 100644 --- a/.github/.markdownlint-cli2.jsonc +++ b/.markdownlint-cli2.jsonc @@ -9,6 +9,7 @@ "MD033": false, "MD034": false, "MD040": false, - "MD041": false + "MD041": false, + "MD060": false } } diff --git a/CHECKLISTS.md b/CHECKLISTS.md index 6444f33..261294f 100644 --- a/CHECKLISTS.md +++ b/CHECKLISTS.md @@ -1,256 +1,256 @@ -# Code Review Checklists by Domain - -Quick reference checklists for common tasks. **Reference actual code patterns in the repository for detailed examples.** - ---- - -## Python Code - -**Before submitting:** -- [ ] Type hints on all public functions (parameters and return types) -- [ ] No hardcoded secrets, passwords, or API keys -- [ ] Tests written for new functionality (80%+ coverage target) -- [ ] Docstrings on public functions/classes -- [ ] Ran `uv run poe lint-fix` and `uv run poe lint` locally -- [ ] No bare `except:` clauses -- [ ] Specific exception types caught and logged - -**Code review questions:** -- Does this function do one thing well? -- Are error messages helpful for debugging? -- Is sensitive data logged or exposed? -- Can this be tested independently? - -**See examples:** -- Clean code patterns → [src/dataenginex/core/](https://github.com/TheDataEngineX/dataenginex/tree/main/src/dataenginex/core/) -- Error handling → [src/dataenginex/api/](https://github.com/TheDataEngineX/dataenginex/tree/main/src/dataenginex/api/) -- Tests → [tests/unit/](https://github.com/TheDataEngineX/dataenginex/tree/main/tests/unit/) - ---- - -## REST API Endpoints (FastAPI) - -**Before submitting:** -- [ ] Endpoint has clear HTTP verb (GET, POST, PATCH, DELETE) -- [ ] Request/response models define shapes (Pydantic) -- [ ] Input validation on all user-provided data -- [ ] Authentication check on protected endpoints -- [ ] Specific status codes (200, 201, 400, 401, 403, 404, etc.) -- [ ] Errors return consistent format with code + message -- [ ] Docstring explains what endpoint does - -**Code review questions:** -- Can a user access data they shouldn't? -- What happens if someone sends bad data? -- Are all required fields validated? -- Is the response schema documented? - -**See examples:** -- API endpoints → [src/dataenginex/api/](https://github.com/TheDataEngineX/dataenginex/tree/main/src/dataenginex/api/) -- Request models → [src/dataenginex/core/schemas.py](https://github.com/TheDataEngineX/dataenginex/blob/main/src/dataenginex/core/schemas.py) -- Tests → [tests/integration/test_pipeline_e2e.py](https://github.com/TheDataEngineX/dataenginex/blob/main/tests/integration/test_pipeline_e2e.py) - ---- - -## Data Pipelines - -**Before submitting:** -- [ ] Pipeline is idempotent (safe to rerun) -- [ ] Source data validated at entry point -- [ ] Transformation logic clear and testable -- [ ] Error handling: specific exceptions with context -- [ ] Logging includes what data was processed (counts, IDs) -- [ ] Documentation explains source → transform → destination -- [ ] Tests verify behavior with sample data - -**Code review questions:** -- What happens if source data changes schema? -- Is late-arriving data handled? -- Can this pipeline be paused/resumed? -- What metrics would tell us if it failed silently? - -**See examples:** -- Pipeline patterns → [examples/07_api_ingestion.py](https://github.com/TheDataEngineX/dataenginex/blob/main/examples/07_api_ingestion.py) -- Data validation → [src/dataenginex/core/validators.py](https://github.com/TheDataEngineX/dataenginex/blob/main/src/dataenginex/core/validators.py) -- Tests → [tests/unit/test_data.py](https://github.com/TheDataEngineX/dataenginex/blob/main/tests/unit/test_data.py) - ---- - -## Database & SQL - -**Before submitting:** -- [ ] Queries use parameterized statements (not string concatenation) -- [ ] Appropriate indexes on frequently filtered columns -- [ ] No N+1 queries (use JOINs or eager loading) -- [ ] Migrations are backwards compatible -- [ ] Foreign key constraints defined -- [ ] Type-appropriate columns (BIGINT for IDs, DECIMAL for money) - -**Code review questions:** -- Could this query timeout on large datasets? -- Are we over-fetching data we don't need? -- If schema changes, does this still work? -- Is this data connection secured? - -**See examples:** -- SQLAlchemy ORM → src/dataenginex/core/database.py -- Schema patterns → Check database documentation -- Schema patterns → Check database documentation - ---- - -## Tests - -**Before submitting:** -- [ ] Test name describes what it's testing -- [ ] Tests are independent (no dependencies on other tests) -- [ ] Mock external services, not code being tested -- [ ] Edge cases covered (empty, None, boundary values) -- [ ] Error cases tested -- [ ] Tests pass locally - -**Code review questions:** -- Does this test verify behavior or implementation? -- What happens if I delete one test, do others break? -- Is this testing the right layer (unit vs integration)? -- Could you understand the test from the name alone? - -**See examples:** -- Unit tests → [tests/unit/](https://github.com/TheDataEngineX/dataenginex/tree/main/tests/unit/) -- Integration tests → [tests/integration/](https://github.com/TheDataEngineX/dataenginex/tree/main/tests/integration/) -- Fixtures → [tests/conftest.py](https://github.com/TheDataEngineX/dataenginex/blob/main/tests/conftest.py) - ---- - -## Documentation & Comments - -**Before submitting:** -- [ ] Public functions have docstrings -- [ ] Comments explain "why", not "what" -- [ ] Examples work (or link to real code) -- [ ] Complex logic has explanatory comments -- [ ] No credentials in examples - -**Code review questions:** -- Could a new developer understand this? -- Is the code obvious without the comment? -- Do examples match current code? - -**See examples:** -- Docstrings → Any file in [src/](https://github.com/TheDataEngineX/dataenginex/tree/main/src/) -- README patterns → [README.md](https://github.com/TheDataEngineX/dataenginex/blob/main/README.md) -- Architecture → [docs/architecture.md](https://github.com/TheDataEngineX/dataenginex/blob/main/docs/architecture.md) - ---- - -## Security (Critical - Check Every PR) - -**Before submitting:** -- [ ] No hardcoded secrets (API keys, passwords, tokens) -- [ ] All user input validated -- [ ] SQL uses parameterized queries -- [ ] Passwords hashed, never stored plaintext -- [ ] Authentication on protected endpoints -- [ ] No sensitive data in logs -- [ ] HTTPS only in production -- [ ] Dependency versions managed in [pyproject.toml](https://github.com/TheDataEngineX/dataenginex/blob/main/pyproject.toml) - -**Code review questions:** -- Can someone exploit this to access data they shouldn't? -- Is there a hardcoded secret anywhere? -- Could this be an SQL injection? -- What happens if someone sends malicious input? - -**See examples:** -- Auth patterns → src/dataenginex/api/auth.py -- Validation → [src/dataenginex/core/validators.py](https://github.com/TheDataEngineX/dataenginex/blob/main/src/dataenginex/core/validators.py) -- Configuration → [.env.template](https://github.com/TheDataEngineX/dataenginex/blob/main/.env.template) - ---- - -## Performance - -**Before submitting:** -- [ ] Used profiler/debugger to find actual bottlenecks -- [ ] No obvious N+1 queries -- [ ] Not loading huge datasets into memory unnecessarily -- [ ] Appropriate use of indexes -- [ ] Reasonable algorithm complexity - -**Code review questions:** -- How does this scale to 10x current data volume? -- Are we making unnecessary database round trips? -- Could this timeout with large datasets? - -**See examples:** -- Efficient queries → Check src/dataenginex/core/database.py -- Async patterns → [src/dataenginex/api/](https://github.com/TheDataEngineX/dataenginex/tree/main/src/dataenginex/api/) - ---- - -## Principles That Apply Everywhere - -Regardless of domain: - -✅ **Do:** -- Single responsibility: each function does one thing well -- Explicit error handling (catch specific exceptions) -- Type hints and validation -- Tests for new code -- Clear naming -- Code that explains itself - -❌ **Don't:** -- Hardcode secrets -- Catch all exceptions with bare `except:` -- Use dynamic typing without reason -- Ignore errors silently -- Write functions >50 lines -- Skip testing - ---- - -## When in Doubt - -1. **Check for existing patterns** in the codebase (better than any doc) -2. **Run the tests locally** - they show expected behavior -3. **Run the real app** - tests passing doesn't mean the app works -4. **Ask the team** - better to clarify than guess -5. **Refer to principles** in [copilot-instructions.md](copilot-instructions.md) - ---- - -## Real App Validation (Run Before Every PR) - -Tests and lint passing is necessary but NOT sufficient. Always run the real application. - -**Server startup:** -```bash -uv run python examples/02_api_quickstart.py -``` - -**Endpoint validation checklist:** -- [ ] Server starts without import errors or crashes -- [ ] `GET /health` → `{"status": "healthy"}` -- [ ] `GET /ready` → all components `ready` -- [ ] `GET /startup` → `started: true` -- [ ] `GET /` → root response with version -- [ ] `POST /echo` → echoes message back -- [ ] `GET /metrics` → Prometheus metrics -- [ ] `GET /openapi.json` → all routes registered -- [ ] All new/changed endpoints return correct response bodies (not just 200 OK) -- [ ] Validation errors return 422 with detail (send bad input on purpose) - -**Standalone module validation:** -```bash -uv run python -c "import dataenginex; print('OK', dataenginex.__file__)" -``` -- [ ] Key classes instantiate and produce correct output outside the API -- [ ] No hidden import-time side effects or crashes - -**Full validation pipeline order:** -1. `uv run poe lint` (or `uv run python -m ruff check src/ tests/`) -2. `uv run poe typecheck` (or `uv run python -m mypy src/dataenginex/ --strict`) -3. `uv run poe test` (or `uv run python -m pytest tests/ -x --tb=short -q`) -4. Start real server and curl all endpoints (above) -5. Run standalone module imports (above) +# Code Review Checklists by Domain + +Quick reference checklists for common tasks. **Reference actual code patterns in the repository for detailed examples.** + +--- + +## Python Code + +**Before submitting:** +- [ ] Type hints on all public functions (parameters and return types) +- [ ] No hardcoded secrets, passwords, or API keys +- [ ] Tests written for new functionality (80%+ coverage target) +- [ ] Docstrings on public functions/classes +- [ ] Ran `uv run poe lint-fix` and `uv run poe lint` locally +- [ ] No bare `except:` clauses +- [ ] Specific exception types caught and logged + +**Code review questions:** +- Does this function do one thing well? +- Are error messages helpful for debugging? +- Is sensitive data logged or exposed? +- Can this be tested independently? + +**See examples:** +- Clean code patterns → [src/dataenginex/core/](../src/dataenginex/core/) +- Error handling → [src/dataenginex/api/](../src/dataenginex/api/) +- Tests → [tests/unit/](../tests/unit/) + +--- + +## REST API Endpoints (FastAPI) + +**Before submitting:** +- [ ] Endpoint has clear HTTP verb (GET, POST, PATCH, DELETE) +- [ ] Request/response models define shapes (Pydantic) +- [ ] Input validation on all user-provided data +- [ ] Authentication check on protected endpoints +- [ ] Specific status codes (200, 201, 400, 401, 403, 404, etc.) +- [ ] Errors return consistent format with code + message +- [ ] Docstring explains what endpoint does + +**Code review questions:** +- Can a user access data they shouldn't? +- What happens if someone sends bad data? +- Are all required fields validated? +- Is the response schema documented? + +**See examples:** +- API endpoints → [src/dataenginex/api/](../src/dataenginex/api/) +- Request models → [src/dataenginex/core/schemas.py](../src/dataenginex/core/schemas.py) +- Tests → [tests/integration/test_e2e_api.py](../tests/integration/test_e2e_api.py) + +--- + +## Data Pipelines + +**Before submitting:** +- [ ] Pipeline is idempotent (safe to rerun) +- [ ] Source data validated at entry point +- [ ] Transformation logic clear and testable +- [ ] Error handling: specific exceptions with context +- [ ] Logging includes what data was processed (counts, IDs) +- [ ] Documentation explains source → transform → destination +- [ ] Tests verify behavior with sample data + +**Code review questions:** +- What happens if source data changes schema? +- Is late-arriving data handled? +- Can this pipeline be paused/resumed? +- What metrics would tell us if it failed silently? + +**See examples:** +- Pipeline patterns → [examples/07_api_ingestion.py](../examples/07_api_ingestion.py) +- Data validation → [src/dataenginex/core/validators.py](../src/dataenginex/core/validators.py) +- Tests → [tests/unit/test_data.py](../tests/unit/test_data.py) + +--- + +## Database & SQL + +**Before submitting:** +- [ ] Queries use parameterized statements (not string concatenation) +- [ ] Appropriate indexes on frequently filtered columns +- [ ] No N+1 queries (use JOINs or eager loading) +- [ ] Migrations are backwards compatible +- [ ] Foreign key constraints defined +- [ ] Type-appropriate columns (BIGINT for IDs, DECIMAL for money) + +**Code review questions:** +- Could this query timeout on large datasets? +- Are we over-fetching data we don't need? +- If schema changes, does this still work? +- Is this data connection secured? + +**See examples:** +- SQLAlchemy ORM → [src/dataenginex/core/database.py](../src/dataenginex/core/database.py) +- Schema patterns → Check database documentation +- Schema patterns → Check database documentation + +--- + +## Tests + +**Before submitting:** +- [ ] Test name describes what it's testing +- [ ] Tests are independent (no dependencies on other tests) +- [ ] Mock external services, not code being tested +- [ ] Edge cases covered (empty, None, boundary values) +- [ ] Error cases tested +- [ ] Tests pass locally + +**Code review questions:** +- Does this test verify behavior or implementation? +- What happens if I delete one test, do others break? +- Is this testing the right layer (unit vs integration)? +- Could you understand the test from the name alone? + +**See examples:** +- Unit tests → [tests/unit/](../tests/unit/) +- Integration tests → [tests/integration/](../tests/integration/) +- Fixtures → [tests/conftest.py](../tests/conftest.py) + +--- + +## Documentation & Comments + +**Before submitting:** +- [ ] Public functions have docstrings +- [ ] Comments explain "why", not "what" +- [ ] Examples work (or link to real code) +- [ ] Complex logic has explanatory comments +- [ ] No credentials in examples + +**Code review questions:** +- Could a new developer understand this? +- Is the code obvious without the comment? +- Do examples match current code? + +**See examples:** +- Docstrings → Any file in [src/](../src/) +- README patterns → [README.md](../README.md) +- Architecture → [docs/ARCHITECTURE.md](../docs/ARCHITECTURE.md) + +--- + +## Security (Critical - Check Every PR) + +**Before submitting:** +- [ ] No hardcoded secrets (API keys, passwords, tokens) +- [ ] All user input validated +- [ ] SQL uses parameterized queries +- [ ] Passwords hashed, never stored plaintext +- [ ] Authentication on protected endpoints +- [ ] No sensitive data in logs +- [ ] HTTPS only in production +- [ ] Dependency versions managed in [pyproject.toml](../pyproject.toml) + +**Code review questions:** +- Can someone exploit this to access data they shouldn't? +- Is there a hardcoded secret anywhere? +- Could this be an SQL injection? +- What happens if someone sends malicious input? + +**See examples:** +- Auth patterns → [src/dataenginex/api/auth.py](../src/dataenginex/api/auth.py) +- Validation → [src/dataenginex/core/validators.py](../src/dataenginex/core/validators.py) +- Configuration → [.env.example](.env.example) + +--- + +## Performance + +**Before submitting:** +- [ ] Used profiler/debugger to find actual bottlenecks +- [ ] No obvious N+1 queries +- [ ] Not loading huge datasets into memory unnecessarily +- [ ] Appropriate use of indexes +- [ ] Reasonable algorithm complexity + +**Code review questions:** +- How does this scale to 10x current data volume? +- Are we making unnecessary database round trips? +- Could this timeout with large datasets? + +**See examples:** +- Efficient queries → Check [src/dataenginex/core/database.py](../src/dataenginex/core/database.py) +- Async patterns → [src/dataenginex/api/](../src/dataenginex/api/) + +--- + +## Principles That Apply Everywhere + +Regardless of domain: + +✅ **Do:** +- Single responsibility: each function does one thing well +- Explicit error handling (catch specific exceptions) +- Type hints and validation +- Tests for new code +- Clear naming +- Code that explains itself + +❌ **Don't:** +- Hardcode secrets +- Catch all exceptions with bare `except:` +- Use dynamic typing without reason +- Ignore errors silently +- Write functions >50 lines +- Skip testing + +--- + +## When in Doubt + +1. **Check for existing patterns** in the codebase (better than any doc) +2. **Run the tests locally** - they show expected behavior +3. **Run the real app** - tests passing doesn't mean the app works +4. **Ask the team** - better to clarify than guess +5. **Refer to principles** in [copilot-instructions.md](copilot-instructions.md) + +--- + +## Real App Validation (Run Before Every PR) + +Tests and lint passing is necessary but NOT sufficient. Always run the real application. + +**Server startup:** +```bash +uv run python examples/02_api_quickstart.py +``` + +**Endpoint validation checklist:** +- [ ] Server starts without import errors or crashes +- [ ] `GET /health` → `{"status": "healthy"}` +- [ ] `GET /ready` → all components `ready` +- [ ] `GET /startup` → `started: true` +- [ ] `GET /` → root response with version +- [ ] `POST /echo` → echoes message back +- [ ] `GET /metrics` → Prometheus metrics +- [ ] `GET /openapi.json` → all routes registered +- [ ] All new/changed endpoints return correct response bodies (not just 200 OK) +- [ ] Validation errors return 422 with detail (send bad input on purpose) + +**Standalone module validation:** +```bash +uv run python -c "import dataenginex; print('OK', dataenginex.__file__)" +``` +- [ ] Key classes instantiate and produce correct output outside the API +- [ ] No hidden import-time side effects or crashes + +**Full validation pipeline order:** +1. `uv run poe lint` (or `uv run python -m ruff check src/ tests/`) +2. `uv run poe typecheck` (or `uv run python -m mypy src/dataenginex/ --strict`) +3. `uv run poe check-all` (or `uv run python -m pytest tests/ -x --tb=short -q`) +4. Start real server and curl all endpoints (above) +5. Run standalone module imports (above) diff --git a/CLAUDE.md b/CLAUDE.md index b15b88b..883099c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,4 +1,4 @@ -# CLAUDE.md — DataEngineX Workspace +# CLAUDE.md — DEX Workspace Be pragmatic, direct, and challenge assumptions. No sugarcoating. Point out blank spots and opportunity costs. If something is wrong or won't work, say so. Find root causes, never workarounds. @@ -8,14 +8,14 @@ Workspace-wide rules: **dex · dex-studio · infradex**. Repo-specific context i | Repo | Package | Port | Purpose | |------|---------|------|---------| -| `dex` | `dataenginex` | — | Core library — config, registry, CLI, pipelines, ML, AI, PrivacyGuard (pure Python, no HTTP server) | -| `dex-studio` | `dex-studio` | 7860 | Web UI — FastAPI + Jinja2 + HTMX, direct dataenginex import | -| `infradex` | — | — | K3s / Helm / Terraform / ArgoCD infrastructure | +| `dex` | `dataenginex` | 17000 | Core framework — config, registry, CLI, API, ML, AI (routing, runtime, memory, observability, workflows) | +| `dex-studio` | `dex-studio` | 7860 | Web UI — FastAPI + Jinja2 + HTMX single pane of glass | +| `infradex` | — | — | K3s / Helm / Terraform infrastructure | ## Git -- **Branches:** Any descriptive name — never commit to `main` directly -- **Flow:** feature → PR to `main` +- **Branches:** `feature/` or `fix/` — never commit to `dev` or `main` directly +- **Flow:** feature → PR to `dev` → PR to `main` - **Order:** commit `dex` before `dex-studio`/`infradex` (dex is upstream) - **Author:** `git commit --author="jaymyaka "` — no Co-Authored-By @@ -41,10 +41,14 @@ Release-please automates versioning. Never bump manually. Pre-commit hook handle ## Tooling -- **Tasks:** `poe lint` / `poe test` / `poe check-all` / `poe dev` — defined in `poe_tasks.toml` -- **Workflows:** `.github/workflows/reusable-*.yml` (shared workflows called from each repo: security, auto-pr-to-main) +- **Tasks:** `poe lint` / `poe check-all` / `poe dev` — defined in `poe_tasks.toml` +- **Hooks:** `.github/scripts/pre-commit-version-bump.sh` (`uv run poe install-hooks`) +- **Workflows:** `.github/workflows/` (security, sync-labels, project-automation, stale-issues) +- **Slash commands:** `/validate` `/review` `/pr` `/new-feature` `/fix-lint` `/debug` ## Production Rules - Root-cause fixes only — no `# type: ignore` without explanation +- Ask before committing — show diff, wait for approval +- Ask before pushing - Always WebSearch/WebFetch for latest package versions — never assume from training diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 059a7d5..b3d7a86 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,7 +30,7 @@ All repos use the same toolchain: ```bash # Prerequisites -python >= 3.12 # 3.13+ recommended +python >= 3.13 uv # Package manager (https://docs.astral.sh/uv/) # Clone and setup @@ -41,7 +41,7 @@ uv sync # Quality checks uv run poe lint # Ruff lint uv run poe typecheck # mypy --strict -uv run poe test # pytest +uv run poe check-all # lint + typecheck + tests ``` ## Coding Standards diff --git a/ISSUE_TEMPLATE/bug_report.yml b/ISSUE_TEMPLATE/bug_report.yml index d9cc7f8..0943691 100644 --- a/ISSUE_TEMPLATE/bug_report.yml +++ b/ISSUE_TEMPLATE/bug_report.yml @@ -69,4 +69,4 @@ body: id: python attributes: label: Python Version - placeholder: "3.12.0" + placeholder: "3.13.0" diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index a59cfd3..ecc4b80 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -20,5 +20,5 @@ - [ ] Documentation updated (if applicable) - [ ] `uv run poe lint` passes - [ ] `uv run poe typecheck` passes -- [ ] `uv run poe test` passes +- [ ] `uv run poe check-all` passes - [ ] No breaking changes (or documented in description) diff --git a/SUPPORT.md b/SUPPORT.md index ddb2371..12ed834 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -4,8 +4,8 @@ Need help with DataEngineX? Here's where to find it. ## Documentation -- [dataenginex Docs](https://docs.thedataenginex.org) — Core framework -- [dex-studio Docs](https://github.com/TheDataEngineX/dex-studio) — Web UI +- [DEX Docs](https://docs.thedataenginex.org) — Core framework +- [DEX Studio Docs](https://github.com/TheDataEngineX/dex-studio) — Web UI - [InfraDEX Docs](https://github.com/TheDataEngineX/infradex) — Infrastructure ## Getting Help @@ -21,7 +21,7 @@ For bugs and feature requests, use the issue tracker in the relevant repo: | Repo | Issues | |------|--------| -| dataenginex | [Issues](https://github.com/TheDataEngineX/dataenginex/issues) | +| DEX | [Issues](https://github.com/TheDataEngineX/dataenginex/issues) | | dex-studio | [Issues](https://github.com/TheDataEngineX/dex-studio/issues) | | infradex | [Issues](https://github.com/TheDataEngineX/infradex/issues) | diff --git a/agents/devops-engineer.agent.md b/agents/devops-engineer.agent.md index 6b1f611..92024fb 100644 --- a/agents/devops-engineer.agent.md +++ b/agents/devops-engineer.agent.md @@ -9,10 +9,10 @@ You are a DevOps/SRE engineer for the DataEngineX project, managing Docker, Kube - Docker: multi-stage builds, `python:3.13-slim`, non-root `dex` user (UID 1000) - Kubernetes: Kustomize base + overlays (dev/stage/prod), ArgoCD GitOps -- CI/CD: GitHub Actions (`ci.yml`, `release.yml`, `security.yml`) -- Monitoring: Prometheus (`http_*` metrics), Grafana dashboards, AlertManager rules +- CI/CD: GitHub Actions (`security.yml`, `sync-labels.yml`, `project-automation.yml`, `stale-issues.yml`) +- Monitoring: Prometheus (`http_*` metrics), Grafana dashboards, AlertManager rules (in `infradex/monitoring/`) - Observability: Jaeger + OTLP tracing via Docker Compose -- Security: Trivy + CodeQL scanning, `renovate.json`, `poe security` +- Security: Semgrep + CodeQL scanning, `dependabot.yml`, `poe security` ## Your Approach @@ -20,20 +20,19 @@ You are a DevOps/SRE engineer for the DataEngineX project, managing Docker, Kube - Set resource limits/requests on all K8s workloads - Liveness/readiness/startup probes on all deployments - Least-privilege `permissions:` in GitHub Actions workflows -- Pin action versions to tags (e.g., `actions/checkout@v7`) +- Pin action versions to tags (e.g., `actions/checkout@v6`) - Use `uv` for dependency management (never raw pip) ## Key Project Files -- Dockerfile: `Dockerfile` (multi-stage, `PYTHONPATH="/app/src"`) -- Compose: `docker-compose.yml` (dataenginex, prometheus, alertmanager, grafana, jaeger) -- Workflows: `.github/workflows/` (ci, release, security) -- Monitoring: `monitoring/` (prometheus.yml, alertmanager.yml, alerts/, grafana/) +- Dockerfile: None in dataenginex (only `docker-compose.test.yml` for testing) +- Compose: `docker-compose.test.yml` (dataenginex test services) +- Workflows: `.github/workflows/` (security, sync-labels, project-automation, stale-issues) +- Monitoring: `infradex/monitoring/` (prometheus.yml, alertmanager.yml, alerts/, grafana/) - Config: `pyproject.toml`, `poe_tasks.toml` ## Guidelines - YAML: 2-space indent, lowercase hyphenated names (e.g., `dex-dev`) - Workflow naming: files lowercase `.yml`, workflows title case, jobs kebab-case -- Releases published to PyPI via `release.yml` (OIDC trusted publishing) - Always maintain `.dockerignore` — no dev deps in production image diff --git a/agents/llm-architect.agent.md b/agents/llm-architect.agent.md index 300c916..1946fd0 100644 --- a/agents/llm-architect.agent.md +++ b/agents/llm-architect.agent.md @@ -27,10 +27,10 @@ You are an LLM systems architect for DataEngineX, responsible for the AI agent a ## Key Project Files -- LLM provider: `${WORKSPACE_ROOT}/dex/src/dataenginex/ml/llm.py` -- Vector store: `${WORKSPACE_ROOT}/dex/src/dataenginex/ml/vectorstore.py` -- Model serving: `${WORKSPACE_ROOT}/dex/src/dataenginex/ml/serving.py` -- Interfaces (BaseAgentRuntime, BaseLLMProvider, BaseVectorStore): `${WORKSPACE_ROOT}/dex/src/dataenginex/core/interfaces.py` +- LLM provider: `${WORKSPACE_ROOT}/dataenginex/src/dataenginex/ai/llm.py` +- Vector store: `${WORKSPACE_ROOT}/dataenginex/src/dataenginex/ai/vectorstore.py` +- Model serving: `${WORKSPACE_ROOT}/dataenginex/src/dataenginex/ml/serving.py` +- Interfaces (BaseAgentRuntime, BaseLLMProvider, BaseVectorStore): `${WORKSPACE_ROOT}/dataenginex/src/dataenginex/core/interfaces.py` - Hardware constraints: see `.github/workspace.env` (auto-generated — run `setup-workspace.sh` to refresh) ## Guidelines diff --git a/agents/mcp-developer.agent.md b/agents/mcp-developer.agent.md index d4d8e45..323f403 100644 --- a/agents/mcp-developer.agent.md +++ b/agents/mcp-developer.agent.md @@ -25,7 +25,7 @@ You are an MCP (Model Context Protocol) developer for DataEngineX, building and ## Key Project Files - Claude Code MCP config: `${WORKSPACE_ROOT}/.claude/settings.local.json` -- Agent integrations: `${WORKSPACE_ROOT}/dex/src/dataenginex/ml/` +- Agent integrations: `${WORKSPACE_ROOT}/dataenginex/src/dataenginex/` - Context7 MCP: already configured — add `use context7` to prompts for library docs ## Registered MCP Servers diff --git a/agents/mlops-engineer.agent.md b/agents/mlops-engineer.agent.md index 63ff5be..89fb9cd 100644 --- a/agents/mlops-engineer.agent.md +++ b/agents/mlops-engineer.agent.md @@ -8,7 +8,7 @@ You are an MLOps engineer specializing in the DataEngineX ML lifecycle — model ## Your Expertise - Model lifecycle: development → staging → production → archived (`ModelRegistry`, JSON-persisted) -- Drift detection: PSI-based feature and prediction drift — `DriftDetector`, `StatisticalDriftDetector` +- Drift detection: PSI-based feature and prediction drift — `DriftDetector`, `PSIDriftDetector` - Training pipelines: PySpark ML `Pipeline` + `PipelineModel`, reproducible experiment runs - Model CI/CD: automated retraining triggers, validation gates before promotion - Experiment tracking: run metadata, metrics, artifact versioning @@ -26,8 +26,8 @@ You are an MLOps engineer specializing in the DataEngineX ML lifecycle — model - Model Registry: `src/dataenginex/ml/registry.py` - Drift Detection: `src/dataenginex/ml/drift.py` -- ML examples: `examples/07_ml_registry.py`, `examples/08_spark_ml.py` -- Metrics: `src/dataenginex/observability/metrics.py` +- ML examples: `examples/08_spark_ml.py`, `examples/09_feature_engineering.py` +- Metrics: `src/dataenginex/ml/metrics.py` - Tests: `tests/unit/test_ml.py`, `tests/unit/test_drift.py` ## Guidelines diff --git a/agents/multi-agent-coordinator.agent.md b/agents/multi-agent-coordinator.agent.md index 3126003..7889598 100644 --- a/agents/multi-agent-coordinator.agent.md +++ b/agents/multi-agent-coordinator.agent.md @@ -26,11 +26,11 @@ You are a multi-agent systems architect for DataEngineX, designing and implement ## Key Project Files -- Agent source: `${WORKSPACE_ROOT}/dex/src/dataenginex/ml/` -- Agent runtime: `${WORKSPACE_ROOT}/dex/src/dataenginex/ml/serving.py` -- LLM providers: `${WORKSPACE_ROOT}/dex/src/dataenginex/ml/llm.py` -- Interfaces: `${WORKSPACE_ROOT}/dex/src/dataenginex/core/interfaces.py` -- Tests: `${WORKSPACE_ROOT}/dex/tests/` +- Agent source: `${WORKSPACE_ROOT}/dataenginex/src/dataenginex/ai/agents/` +- Agent runtime: `${WORKSPACE_ROOT}/dataenginex/src/dataenginex/ml/serving.py` +- LLM providers: `${WORKSPACE_ROOT}/dataenginex/src/dataenginex/ai/llm.py` +- Interfaces: `${WORKSPACE_ROOT}/dataenginex/src/dataenginex/core/interfaces.py` +- Tests: `${WORKSPACE_ROOT}/dataenginex/tests/` ## Guidelines diff --git a/agents/security-auditor.agent.md b/agents/security-auditor.agent.md index ff406f4..b461d33 100644 --- a/agents/security-auditor.agent.md +++ b/agents/security-auditor.agent.md @@ -28,7 +28,7 @@ You are a security auditor for DataEngineX — read-only analysis of vulnerabili - Dependencies: `pyproject.toml` + `uv.lock` — known CVEs, abandoned packages - Config files: `.env.example`, K8s manifests, Dockerfiles — secrets, misconfigs - Workflows: `.github/workflows/` — permissions, secret exposure, action versions -- Trivy findings: `argocd/`, `helm/` — misconfigs, image vulnerabilities +- Trivy findings: `infradex/argocd/`, `infradex/helm/` — misconfigs, image vulnerabilities ## Red Flags to Always Check diff --git a/agents/sre-engineer.agent.md b/agents/sre-engineer.agent.md index e1b112c..1e84a20 100644 --- a/agents/sre-engineer.agent.md +++ b/agents/sre-engineer.agent.md @@ -24,11 +24,10 @@ You are an SRE for DataEngineX, owning reliability targets, Prometheus/Grafana m ## Key Project Files -- Prometheus config: `monitoring/prometheus.yml` -- Alert rules: `monitoring/alerts/` -- Alertmanager: `monitoring/alertmanager.yml` -- Grafana dashboards: `monitoring/grafana/` -- Docker Compose: `docker-compose.monitoring.yml` +- Prometheus config: `infradex/monitoring/prometheus.yml` +- Alert rules: `infradex/monitoring/alerts/` +- Alertmanager: `infradex/monitoring/alertmanager.yml` +- Grafana dashboards: `infradex/monitoring/grafana/` ## Monitoring Stack Ports diff --git a/copilot-instructions.md b/copilot-instructions.md index edba465..2f3f954 100644 --- a/copilot-instructions.md +++ b/copilot-instructions.md @@ -10,9 +10,9 @@ Be pragmatic, straight forward and challenge my ideas and system design focus on | Repo | Package | Purpose | Port | |------|---------|---------|------| -| `dataenginex` (dex) | `dataenginex` | Core library — config, registry, CLI, pipelines, ML, AI, PrivacyGuard (pure Python, no HTTP server) | — | -| `dex-studio` | `dex-studio` | Web UI — FastAPI + Jinja2 + HTMX, direct dataenginex import | 7860 | -| `infradex` | — | K3s / Helm / Terraform / ArgoCD infrastructure | — | +| `dataenginex` (dex) | `dataenginex` | Core framework — config, registry, CLI, API, ML, AI | 17000 | +| `dex-studio` | `dex-studio` | Web UI — single pane of glass (FastAPI + Jinja2 + HTMX) | 7860 | +| `infradex` | — | IaC + monitoring (Terraform, Helm, K3s) | — | --- @@ -56,7 +56,7 @@ Be pragmatic, straight forward and challenge my ideas and system design focus on 1. `uv run poe lint` — Ruff lint 2. `uv run poe typecheck` — mypy strict -3. `uv run poe test` — pytest +3. `uv run poe check-all` — lint + typecheck + tests 4. Start real server and verify endpoints (see repo `CLAUDE.md`) 5. Standalone import check @@ -64,23 +64,23 @@ Be pragmatic, straight forward and challenge my ideas and system design focus on ### Context7 MCP -Always use Context7 MCP for library/API docs (FastAPI, Pydantic, ArgoCD, Helm, etc.) — without the user having to ask. +Always use Context7 MCP for library/API docs (FastAPI, PySpark, Pydantic, Airflow, Jinja2) — without the user having to ask. --- ## Cross-Repo Sync Policy -Verify consistency across all 3 repos on every `feature → main` PR. +Verify consistency across all 3 repos on every `dev → main` PR. ### Required workflows in all repos -`ci.yml` · `security.yml` +`security.yml` · `sync-labels.yml` · `project-automation.yml` · `stale-issues.yml` ### Required files in all repos -`CLAUDE.md` · `README.md` · `pyproject.toml` · `uv.lock` · `LICENSE` +`CLAUDE.md` · `README.md` · `pyproject.toml` · `uv.lock` · `tasks/todo.md` · `tasks/lessons.md` · `tasks/findings.md` · `.github/PULL_REQUEST_TEMPLATE.md` · `.github/dependabot.yml` · `CODEOWNERS` · `LICENSE` -Do NOT copy dex-only workflows (`release.yml`) to other repos. +Do NOT copy dex-only workflows (`pypi-publish.yml`, `release-dataenginex.yml`) to other repos. --- diff --git a/dependabot.yml b/dependabot.yml new file mode 100755 index 0000000..43a5439 --- /dev/null +++ b/dependabot.yml @@ -0,0 +1,23 @@ +version: 2 +updates: + - package-ecosystem: pip + directory: /dataenginex + schedule: + interval: weekly + day: monday + open-pull-requests-limit: 5 + labels: + - dependencies + commit-message: + prefix: "chore(deps):" + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + day: monday + open-pull-requests-limit: 5 + labels: + - dependencies + commit-message: + prefix: "chore(deps):" diff --git a/instructions/data-pipelines.instructions.md b/instructions/data-pipelines.instructions.md index d470664..95e41b0 100644 --- a/instructions/data-pipelines.instructions.md +++ b/instructions/data-pipelines.instructions.md @@ -38,7 +38,7 @@ Every pipeline must be safe to re-run: ## Lineage -- Track column-level lineage via `LineageTracker` — every transform must be registered +- Track column-level lineage via `PersistentLineage` or `LineageBackend` — every transform must be registered - Never drop lineage events silently — log failures ## Performance diff --git a/instructions/fastapi.instructions.md b/instructions/fastapi.instructions.md index baa5aa6..b7098b6 100644 --- a/instructions/fastapi.instructions.md +++ b/instructions/fastapi.instructions.md @@ -22,7 +22,7 @@ Extends `python.instructions.md`. ## Auth - Protected endpoints check auth via lifespan middleware — not inline per endpoint -- Auth: pure-Python HS256 JWT (no `pyjwt` dependency) +- Auth: HS256 JWT (PyJWT dependency) - Return `401` for missing/invalid token, `403` for insufficient permissions - Never log the token or any credential diff --git a/instructions/ml.instructions.md b/instructions/ml.instructions.md index 491c7fa..7f27366 100644 --- a/instructions/ml.instructions.md +++ b/instructions/ml.instructions.md @@ -1,5 +1,5 @@ --- -applyTo: "src/**/ml/**/*.py,src/**/models/**/*.py,src/**/registry/**/*.py,src/**/drift/**/*.py" +applyTo: "src/**/ml/**/*.py,src/**/models/**/*.py,src/**/registry/**/*.py" --- # ML Standards diff --git a/instructions/workflows.instructions.md b/instructions/workflows.instructions.md index 9e2596e..3c2a62c 100644 --- a/instructions/workflows.instructions.md +++ b/instructions/workflows.instructions.md @@ -10,12 +10,12 @@ applyTo: ".github/workflows/**/*.yml" | --- | --- | | `actions/checkout` | `@v6` | | `actions/setup-python` | `@v6` | -| `actions/upload-artifact` | `@v7` | -| `actions/download-artifact` | `@v8` | -| `astral-sh/setup-uv` | `@v7` | -| `codecov/codecov-action` | `@v7` | +| `actions/upload-artifact` | `@v4` | +| `actions/download-artifact` | `@v4` | +| `astral-sh/setup-uv` | `@v8.1.0` | +| `codecov/codecov-action` | `@v5` | | `github/codeql-action/*` | `@v4` | -| `actions/github-script` | `@v8` | +| `actions/github-script` | `@v9` | Always pin to the current major tag. Verify against actual workflow files before documenting. @@ -44,7 +44,7 @@ test: ## Python setup (always use uv) ```yaml -- uses: astral-sh/setup-uv@v7 +- uses: astral-sh/setup-uv@v8.1.0 with: version: "latest" - uses: actions/setup-python@v6 @@ -58,10 +58,12 @@ Never `pip install` in workflows. ## Coverage upload ```yaml -- uses: codecov/codecov-action@v7 +- uses: codecov/codecov-action@v5 with: flags: fail_ci_if_error: false + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} ``` ## Checklist diff --git a/lychee.toml b/lychee.toml index 5d1c566..27691ee 100644 --- a/lychee.toml +++ b/lychee.toml @@ -1,5 +1,13 @@ -# Exclude URLs that are planned but not yet live +# Exclude URLs that are planned but not yet live, or local paths that don't exist in this meta-repo exclude = [ - "https://github.com/TheDataEngineX/infradex.*", - "https://docs.thedataenginex.org.*", + "https://github.com/TheDataEngineX/infradex", + "https://docs.thedataenginex.org", + "file://", + "\\.env\\.example", + "examples/", + "src/", + "tests/", + "docs/", + "pyproject\\.toml", + "README\\.md", ] diff --git a/profile/README.md b/profile/README.md index 0eb2d51..254d95b 100644 --- a/profile/README.md +++ b/profile/README.md @@ -5,7 +5,6 @@ Unified Data + ML + AI framework — config-driven, self-hosted, production-read [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/TheDataEngineX/dataenginex/blob/main/LICENSE) [![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/) [![PyPI](https://img.shields.io/pypi/v/dataenginex?label=dataenginex)](https://pypi.org/project/dataenginex/) -[![Docs](https://img.shields.io/badge/docs-thedataenginex.org-informational)](https://docs.thedataenginex.org) [![Discussions](https://img.shields.io/github/discussions/TheDataEngineX/.github?label=discussions)](https://github.com/orgs/TheDataEngineX/discussions) --- @@ -15,7 +14,7 @@ Opinionated defaults that work out of the box, swap any layer via extras. > **Self-hosted.** Your data never leaves your infrastructure. > **Config-driven.** One YAML file defines everything. -> **Production-grade.** DuckDB, FastAPI, structlog, Prometheus, OpenTelemetry out of the box. +> **Local-first.** DuckDB embedded, Ollama for LLMs — no external services required. --- @@ -33,11 +32,13 @@ graph LR subgraph Core ["dataenginex (pip install)"] direction TB REG[Plugin Registry\nConnectors · Transforms · Backends] + API[FastAPI\nJWT · RBAC · SCIM · rate limit · metrics] OB[Observability\nstructlog · Prometheus · OTEL · Langfuse] end subgraph Extras ["pip install dataenginex[extra]"] CL[cloud: S3 · GCS · BigQuery] + AU[auth: RS256/JWKS · SCIM v2 · LDAP] OV[observability: Langfuse LLM tracing] end @@ -45,11 +46,11 @@ graph LR Extras -->|same interface| Core subgraph Products - B2B[dex-studio\nB2B platform UI] + ST[DEX Studio\nWeb UI] INF[InfraDEX\nK3s · Helm · Terraform] end - B2B -->|Python lib| Core + ST -->|HTTP :7860| Core INF -->|deploys| Core ``` @@ -59,9 +60,9 @@ graph LR | Repo | What it does | Status | | --- | --- | --- | -| [**dataenginex**](https://github.com/TheDataEngineX/dataenginex) | Core framework: config, CLI, ML registry, LLM routing (LiteLLM/vLLM), AI agents, DuckDB lakehouse — pure Python library | [![PyPI](https://img.shields.io/pypi/v/dataenginex)](https://pypi.org/project/dataenginex/) | -| [**dex-studio**](https://github.com/TheDataEngineX/dex-studio) | B2B web UI: pipelines, ML experiments, AI playground, SQL console (FastAPI/Jinja2 + HTMX) | Alpha | -| [**infradex**](https://github.com/TheDataEngineX/infradex) | IaC: K3s, Helm charts, Terraform — Authentik, Langfuse, Qdrant, Prometheus, Grafana, ArgoCD | Alpha | +| [**dataenginex**](https://github.com/TheDataEngineX/dataenginex) | Core framework: config, CLI, pipelines, ML registry, LLM routing, AI agents, PrivacyGuard | [![PyPI](https://img.shields.io/pypi/v/dataenginex)](https://pypi.org/project/dataenginex/) | +| [**dex-studio**](https://github.com/TheDataEngineX/dex-studio) | Web UI: pipelines, ML experiments, AI playground, SQL console (FastAPI + Jinja2 + HTMX) | Alpha | +| [**infradex**](https://github.com/TheDataEngineX/infradex) | IaC: K3s, Helm charts, Terraform — Authentik, Prometheus, Grafana, ArgoCD | Alpha | --- @@ -70,27 +71,27 @@ graph LR ```bash pip install dataenginex dex validate dex.yaml -``` - -```python -from dataenginex.engine import DexEngine - -engine = DexEngine("dex.yaml") -engine.run_pipeline("clean_users") +dex serve --config dex.yaml # → http://localhost:17000 ``` ```bash -# Web UI (dex-studio) -pip install dex-studio -dex-studio --config dex.yaml # → http://localhost:7860 +# From source +git clone https://github.com/TheDataEngineX/dataenginex && cd dataenginex +uv sync && uv run poe dev # → http://localhost:17000 ``` **Optional extras:** ```bash -pip install "dataenginex[cloud]" # S3 · GCS · BigQuery connectors -pip install "dataenginex[observability]" # Langfuse LLM tracing -pip install 'litellm>=1.83.3' --no-deps # 100+ LLM providers (separate install) +pip install "dataenginex[cloud]" # S3 · GCS · BigQuery connectors +pip install "dataenginex[ml]" # scikit-learn · sentence-transformers +pip install "dataenginex[delta]" # Delta Lake connector +pip install "dataenginex[postgres]" # asyncpg for Postgres lineage +pip install "dataenginex[qdrant]" # Qdrant vector store +pip install "dataenginex[queue]" # arq background jobs +pip install "dataenginex[pytorch]" # PyTorch ML +pip install "dataenginex[tracking]" # MLflow experiment tracking +pip install 'litellm>=1.83.3' --no-deps # 100+ LLM providers (separate install) ``` --- @@ -103,8 +104,8 @@ pip install 'litellm>=1.83.3' --no-deps # 100+ LLM providers (separate install) | **Install** | `pip install dataenginex` | 10+ packages to wire together | | **Backends** | Swap via extras, same interface | Rewrite integration code | | **Self-hosted** | Works on laptop, VPS, or K8s | Cloud lock-in or complex setup | -| **Observability** | structlog + Prometheus + OTEL + Langfuse | Manual instrumentation | -| **UI** | dex-studio: pipelines, ML, AI agents, SQL | Build it yourself | +| **Observability** | structlog + Prometheus + OTEL | Manual instrumentation | +| **Local-first** | DuckDB + Ollama defaults | External services required | --- @@ -117,7 +118,6 @@ pip install 'litellm>=1.83.3' --no-deps # 100+ LLM providers (separate install) | **Bug reports** | Open an issue in the relevant repo | | **Contributing** | [CONTRIBUTING.md](https://github.com/TheDataEngineX/.github/blob/main/CONTRIBUTING.md) | | **Security** | [SECURITY.md](https://github.com/TheDataEngineX/.github/blob/main/SECURITY.md) | -| **Website** | [thedataenginex.org](https://thedataenginex.org) | --- diff --git a/prompts/debug.prompt.md b/prompts/debug.prompt.md new file mode 100644 index 0000000..58186c2 --- /dev/null +++ b/prompts/debug.prompt.md @@ -0,0 +1,30 @@ +--- +description: "Structured debugging workflow for an issue" +tools: ["search/codebase", "execute/runInTerminal", "execute/getTerminalOutput", "read/terminalLastCommand", "read/terminalSelection"] +--- + +Debug the issue in or related to ${file}. + +## Debugging workflow + +1. **Understand** — Read the error message, traceback, or unexpected behavior description +2. **Locate** — Find the relevant source code and trace the execution path +3. **Hypothesize** — Form a theory about root cause based on: + - Recent changes to the file or its dependencies + - Error handling gaps (bare `except:`, swallowed errors) + - Type mismatches (check with `poe typecheck`) + - Missing validation at boundaries +4. **Verify** — Run targeted tests: + - `poe test-cov-core` for core tests + - Check lint/type errors: `poe lint` and `poe typecheck` +5. **Fix** — Apply the minimal change that resolves the issue + - Follow project error patterns from `src/dataenginex/api/errors.py` + - Add structured logging for observability + - Add a test that reproduces the bug (prevents regression) +6. **Validate** — Run `poe check-all` to confirm nothing else broke + +## Project-specific debug tips +- API issues: check middleware order (logging → metrics → auth → rate limit) +- Auth issues: check `DEX_AUTH_ENABLED` and `DEX_JWT_SECRET` env vars +- Import errors: check for circular deps (use lazy imports in routers) +- Test failures: warnings are treated as errors (except uvicorn/websockets) diff --git a/prompts/new-feature.prompt.md b/prompts/new-feature.prompt.md new file mode 100644 index 0000000..ed58632 --- /dev/null +++ b/prompts/new-feature.prompt.md @@ -0,0 +1,43 @@ +--- +description: "Scaffold a complete feature with code, tests, and docs" +tools: ["search/codebase", "execute/runInTerminal", "execute/getTerminalOutput", "read/terminalLastCommand", "read/terminalSelection"] +--- + +Scaffold a new feature for the DataEngineX project. + +## Workflow + +1. **Plan** — Identify which module this belongs to in `src/dataenginex/`: + - `api/` — health, auth, errors, pagination, rate limiting + - `core/` — schemas, validators, medallion architecture, quality gates + - `data/` — connectors, profiler, schema registry + - `lakehouse/` — catalog, partitioning, storage + - `middleware/` — logging, metrics, request logging, tracing + - `ml/` — registry, training, drift, serving, LLM, vectorstore + - `warehouse/` — transforms, lineage + +2. **Implement** — Create the feature code: + - `from __future__ import annotations` at top of every file + - Type hints on all public functions (params + return) + - Structured logging (structlog only — no loguru) + - Error handling with specific exceptions and context + - Docstrings on public functions/classes + +3. **Models** — If API-facing, add Pydantic models in `src/dataenginex/core/schemas.py` + +4. **Tests** — Write tests in `tests/unit/test_.py`: + - Happy path, error paths, edge cases + - Use `TestClient` for API, `tmp_path` for files + - Group in `Test` classes + - No `@pytest.mark.asyncio` needed (auto mode) + +5. **Validate**: + - `poe check-all` — lint + typecheck + tests + +6. **Commit** — Use conventional commit: `feat: (#issue)` + +## Quality gates +- No hardcoded secrets +- No bare `except:` +- Functions under 50 lines, max 4 params +- No `print()` or stdlib `logging` diff --git a/prompts/pr-description.prompt.md b/prompts/pr-description.prompt.md new file mode 100644 index 0000000..5d118f7 --- /dev/null +++ b/prompts/pr-description.prompt.md @@ -0,0 +1,43 @@ +--- +description: "Generate a PR description from staged/changed files" +tools: ["search/codebase", "execute/runInTerminal", "execute/getTerminalOutput", "read/terminalLastCommand", "read/terminalSelection", "web/githubRepo"] +--- + +Generate a pull request description for the current changes using the project's PR template. + +## Steps + +1. Check which files have been changed (staged or modified) +2. Analyze the changes to understand what was done and why +3. Fill in the PR template from `.github/PULL_REQUEST_TEMPLATE.md`: + +```markdown +## Summary + + + +## Related Issues + + + +## Changes + +- [ ] New feature +- [ ] Bug fix +- [ ] Refactor +- [ ] Documentation +- [ ] CI/CD + +## Checklist + +- [ ] Tests added/updated +- [ ] Documentation updated (if applicable) +- [ ] `uv run poe lint` passes +- [ ] `uv run poe typecheck` passes +- [ ] `uv run poe check-all` passes +- [ ] No breaking changes (or documented in description) +``` + +1. Check the appropriate boxes based on actual changes +2. Write a concise but complete description +3. Use conventional commit style for the summary line diff --git a/prompts/write-tests.prompt.md b/prompts/write-tests.prompt.md new file mode 100644 index 0000000..058f44c --- /dev/null +++ b/prompts/write-tests.prompt.md @@ -0,0 +1,26 @@ +--- +description: "Write tests for the current file" +tools: ["search/codebase", "execute/runInTerminal", "execute/getTerminalOutput", "read/terminalLastCommand", "read/terminalSelection"] +--- + +Write tests for the code in ${file}. + +## Rules + +1. **Location** — Place tests in `tests/unit/test_.py` (match module name) +2. **Style**: + - `asyncio_mode = "auto"` — do NOT add `@pytest.mark.asyncio` + - Use `@pytest.mark.parametrize` for input variations + - Group related tests in `Test` classes + - Name: `test_` — descriptive, not implementation-tied +3. **Test Clients**: + - Sync API tests: `TestClient` from Starlette + - Async API tests: `httpx.AsyncClient` with `ASGITransport` + - Create `TestClient` locally in each test file (conftest.py is minimal) +4. **Coverage**: + - Happy path + error paths + edge cases (empty, None, boundary) + - Mock external services only, not code under test + - Use `tmp_path` for file operations +5. **Verify** — Run `poe test-cov-core` to confirm tests pass + +Follow patterns in existing tests under `tests/unit/`. diff --git a/scripts/pre-commit-version-bump.sh b/scripts/pre-commit-version-bump.sh new file mode 100644 index 0000000..fa78651 --- /dev/null +++ b/scripts/pre-commit-version-bump.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# pre-commit-version-bump.sh +# +# Git pre-commit hook: auto-bump patch version if pyproject.toml version +# was not manually changed since HEAD. +# +# Install via: uv run poe install-hooks +# Installed to: .git/hooks/pre-commit +# +# Behavior: +# - Reads version from HEAD:pyproject.toml (last committed) +# - Reads version from index (:pyproject.toml) (about to be committed) +# - If same → bumps patch in pyproject.toml and re-stages it +# - If different → user manually changed it; do nothing + +set -euo pipefail + +PYPROJECT="pyproject.toml" +# NOTE: .release-please-manifest.json is intentionally NOT touched here — +# release-please owns that file; modifying it breaks its release detection. + +# Nothing to do on initial commit (no HEAD yet) +if ! git rev-parse --verify HEAD > /dev/null 2>&1; then + exit 0 +fi + +# Read version from last committed state +HEAD_VERSION=$(git show HEAD:"$PYPROJECT" 2>/dev/null \ + | grep -m1 '^version = ' \ + | sed 's/version = "//;s/".*//' \ + || true) + +if [[ -z "$HEAD_VERSION" ]]; then + exit 0 +fi + +# Read version from the index (what's staged right now) +STAGED_VERSION=$(git show :"$PYPROJECT" 2>/dev/null \ + | grep -m1 '^version = ' \ + | sed 's/version = "//;s/".*//' \ + || echo "$HEAD_VERSION") + +# Version was manually changed — respect it +if [[ "$STAGED_VERSION" != "$HEAD_VERSION" ]]; then + exit 0 +fi + +# Version unchanged — auto-bump patch +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +NEW_VERSION=$(bash "$SCRIPT_DIR/bump-version.sh" patch "$PYPROJECT" | tail -n1) + +git add "$PYPROJECT" +echo "auto-bumped version: $HEAD_VERSION → $NEW_VERSION" diff --git a/wiki-content/Architecture.md b/wiki-content/Architecture.md deleted file mode 100644 index 828659b..0000000 --- a/wiki-content/Architecture.md +++ /dev/null @@ -1,135 +0,0 @@ -# Architecture - -## Medallion Data Pipeline - -dataenginex implements the medallion (lakehouse) architecture as its core data processing pattern: - -``` -Raw Sources (APIs, files, streams) - ↓ - BRONZE LAYER — Raw ingestion (Parquet) - ↓ quality gate ≥ 75% - SILVER LAYER — Cleaned & validated - ↓ quality gate ≥ 90% - GOLD LAYER — Enriched & aggregated - ↓ - API / ML / Analytics -``` - -Each layer transition enforces a `QualityGate`. The gate is injectable — you can supply custom `scorer`, `required_fields`, and `uniqueness_key` arguments. - -```python -from dataenginex.core import MedallionArchitecture, QualityGate - -arch = MedallionArchitecture() -gate = QualityGate( - scorer=my_scorer, - required_fields=["id", "timestamp"], - uniqueness_key="id", -) -``` - -## Module Dependency Graph - -``` -dataenginex.core ← always available (pydantic, pyyaml, structlog) -dataenginex.data ← always available -dataenginex.lakehouse ← always available; cloud extras unlock backends -dataenginex.warehouse ← always available -dataenginex.ml ← always available -dataenginex.plugins ← always available - -dataenginex.api ← requires [api] extra (FastAPI, uvicorn, structlog, OpenTelemetry) -dataenginex.middleware ← requires [api] extra -dataenginex.dashboard ← requires [dashboard] extra (Streamlit) -``` - -Key design decision (v0.6.0): FastAPI is **optional**. The core install ships only lightweight deps. API/middleware consumers must opt in with `pip install dataenginex[api]`. - -## Optional Extras - -| Extra | Unlocks | Key Deps | -|-------|---------|----------| -| `[api]` | `dataenginex.api`, `dataenginex.middleware` | FastAPI, uvicorn, structlog, OpenTelemetry | -| `[s3]` | S3 storage backend | boto3 | -| `[gcs]` | GCS storage backend | google-cloud-storage | -| `[bq]` | BigQuery storage backend | google-cloud-bigquery | -| `[cloud]` | S3 + GCS backends | boto3, google-cloud-storage | -| `[ml]` | `SentenceTransformerEmbedder` | sentence-transformers | -| `[notebook]` | Jupyter utilities | ipykernel | -| `[dashboard]` | Streamlit dashboard | streamlit | -| `[all]` | Everything above | — | - -## Storage Backends - -Storage is accessed via a unified `StorageBackend` ABC and a `get_storage(uri)` factory: - -| URI Scheme | Backend | Extra Required | -|------------|---------|----------------| -| `file://` | `LocalParquetStorage` | — | -| `json://` | `JsonStorage` | — | -| `parquet://` | `ParquetStorage` | — | -| `s3://` | `S3Storage` | `[s3]` | -| `gs://` | `GCSStorage` | `[gcs]` | -| `bq://` | `BigQueryStorage` | `[bq]` | - -All backends implement `read()`, `write()`, `list_objects(prefix)`, and `exists(path)`. - -## API Architecture - -The API module provides reusable primitives and route definitions. Applications use `dataenginex.api` routers directly or define their own. - -Provided utilities: -- **Auth** — Pure-Python HS256 JWT (no pyjwt dependency) -- **Health checks** — `HealthChecker` for liveness/readiness probes -- **Pagination** — Cursor-based `paginate()` helper -- **Rate limiting** — Configurable middleware -- **Error handling** — Structured HTTP error responses - -## Tech Stack - -| Layer | Technology | -|-------|------------| -| Language | Python 3.13+ | -| Package Manager | uv + Hatchling | -| Web Framework | FastAPI + Uvicorn (optional `[api]`) | -| Orchestration | Apache Airflow | -| Big Data | PySpark | -| Code Quality | Ruff + mypy (strict) | -| Testing | pytest + coverage (94%) | -| Observability | Prometheus, Grafana, Jaeger (OpenTelemetry) | -| Containers | Docker (multi-stage, non-root) | -| Kubernetes | K3s + ArgoCD (GitOps) | -| CI/CD | GitHub Actions | - -## Project Structure - -``` -dataenginex/ -├── src/ -│ └── dataenginex/ -│ ├── api/ # FastAPI utilities (auth, health, pagination, rate limiting) -│ ├── core/ # Medallion architecture, validators, schemas -│ ├── data/ # Connectors, profiler, schema registry -│ ├── dashboard/ # Streamlit dashboard -│ ├── lakehouse/ # Catalog, partitioning, storage backends -│ ├── middleware/ # Structured logging, Prometheus metrics, tracing -│ ├── ml/ # Training, registry, serving, drift, LLM, RAG -│ ├── plugins/ # Plugin system (entry-point based discovery) -│ └── warehouse/ # SQL/Spark transforms, column-level lineage -├── examples/ # Runnable scripts (01–10) -├── tests/ -│ ├── unit/ # Unit tests -│ ├── integration/ # End-to-end tests (requires docker-compose.test.yml) -│ └── fixtures/ # Sample data -├── Dockerfile # Multi-stage, non-root, port 7860 -└── docker-compose.test.yml # S3 + GCS emulators for integration tests -``` - -## dataenginex Ecosystem Data Flow - -``` -Ingest → Process (Spark/Flink) → Lakehouse → Warehouse → Feature Store → Model Serving → AI Apps & Agents - ↑ - Terraform → K8s → GitOps (infradex) -``` diff --git a/wiki-content/Development.md b/wiki-content/Development.md deleted file mode 100644 index a1dad07..0000000 --- a/wiki-content/Development.md +++ /dev/null @@ -1,128 +0,0 @@ -# Development - -## Prerequisites - -- Python 3.13+ -- [uv](https://docs.astral.sh/uv/) package manager - -```bash -git clone https://github.com/TheDataEngineX/dataenginex && cd dataenginex -uv run poe setup # install deps + pre-commit hooks -``` - -## Task Runner - -All commands use [poethepoet](https://github.com/nat-n/poethepoet) via `uv run poe `. Task definitions live in `poe_tasks.toml`. - -## Quality Commands - -| Command | Description | -|---------|-------------| -| `uv run poe lint` | Ruff lint (rules: E, F, I, B, UP, SIM, C90) | -| `uv run poe lint-fix` | Ruff lint + auto-fix | -| `uv run poe typecheck` | mypy --strict on `src/dataenginex/` only | -| `uv run poe check-all` | lint + typecheck + test | - -## Test Commands - -| Command | Description | -|---------|-------------| -| `uv run poe test` | All tests | -| `uv run poe test-unit` | Unit tests only (`tests/unit/`) | -| `uv run poe test-integration` | Integration tests only (`tests/integration/`) | -| `uv run poe test-cov` | Tests with HTML coverage report | - -**Integration tests** require running storage emulators: - -```bash -docker compose -f docker-compose.test.yml up -d -uv run poe test-integration -docker compose -f docker-compose.test.yml down -``` - -## Run Commands - -| Command | Description | -|---------|-------------| -| `uv run poe dev` | Dev server (uvicorn reload, port 7860) | -| `uv run poe docker-up` | Docker Compose up | -| `uv run poe docker-down` | Docker Compose down | - -## Dependency Management - -| Command | Description | -|---------|-------------| -| `uv run poe uv-sync` | Sync deps from lockfile | -| `uv run poe uv-lock` | Regenerate lockfile | -| `uv run poe security` | Audit deps for vulnerabilities | - -The `uv.lock` file **must always be committed** — it is the reproducibility contract. - -## Mandatory Validation Pipeline - -Run in this exact order after any code change: - -```bash -# 1. Lint -uv run poe lint - -# 2. Type check -uv run poe typecheck - -# 3. Tests -uv run poe test - -# 4. Real server smoke test -uv run python examples/02_api_quickstart.py -# Then verify endpoints: -# curl http://localhost:7860/health -# curl http://localhost:7860/ -# curl http://localhost:7860/metrics - -# 5. Standalone import check -python -c "from dataenginex.core import MedallionArchitecture, QualityGate" -python -c "from dataenginex.ml import ModelRegistry, RAGPipeline" -``` - -Tests passing does not equal the app working. Step 4 is non-negotiable. - -## API Endpoints (Dev Server) - -| Endpoint | Method | Description | -|----------|--------|-------------| -| `/` | GET | Root | -| `/echo` | POST | Echo endpoint | -| `/health` | GET | Health check | -| `/metrics` | GET | Prometheus metrics | - -## Coding Standards - -- `from __future__ import annotations` in all source files -- Line length: 100 characters -- Max function complexity: 8 -- Functions: under 50 lines, max 4 parameters -- Type hints on all public functions -- `mypy --strict` must pass on `src/dataenginex/` - -### Logging - -- **All code**: `import structlog; logger = structlog.get_logger()` with `logger.info("event", key=value)` -- Never use `print()`, `loguru`, stdlib `logging`, or f-strings in log calls - -## Git Conventions - -- Branches: `main` (prod), any descriptive name for features -- Commits: Conventional — `feat:`, `fix:`, `docs:`, `chore:`, `refactor:`, `test:` -- Reference issues: `feat: add drift detection (#42)` -- Deployment: `main` → production (feature → PR to `main`) - -## Running Examples - -```bash -uv run python examples/01_core_demo.py -uv run python examples/02_api_quickstart.py -uv run python examples/05_rag_demo.py --embed --llm ollama --model llama3.2 -uv run python examples/08_spark_ml.py -``` - -Examples are numbered 01–10 and cover progressively more advanced features. diff --git a/wiki-content/Home.md b/wiki-content/Home.md deleted file mode 100644 index f37e8b1..0000000 --- a/wiki-content/Home.md +++ /dev/null @@ -1,117 +0,0 @@ -# DataEngineX - -> Core framework for data engineering, ML, and observability. - -[![CI](https://github.com/TheDataEngineX/dataenginex/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/TheDataEngineX/dataenginex/actions/workflows/ci.yml) -[![PyPI](https://img.shields.io/pypi/v/dataenginex)](https://pypi.org/project/dataenginex/) -[![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/) -[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/TheDataEngineX/dataenginex/blob/main/LICENSE) -[![Coverage](https://img.shields.io/badge/coverage-94%25-brightgreen)](https://github.com/TheDataEngineX/dataenginex) - -## What is DataEngineX? - -A production-focused Python framework for data engineering — medallion architecture, ML lifecycle management, and enterprise observability out of the box. - -`dataenginex` is the core library. It is the only published package — applications and ecosystem repos are built on top of it. - -## Quick Start - -```bash -# Install core (no web framework dependencies) -pip install dataenginex - -# With FastAPI, middleware, auth, health checks -pip install dataenginex[api] - -# With cloud storage backends -pip install dataenginex[s3] # AWS S3 via boto3 -pip install dataenginex[gcs] # Google Cloud Storage -pip install dataenginex[bq] # Google BigQuery -pip install dataenginex[cloud] # All cloud storage (S3 + GCS) - -# Everything -pip install dataenginex[all] -``` - -```bash -# Clone and develop -git clone https://github.com/TheDataEngineX/dataenginex && cd dataenginex -uv run poe setup # install deps + pre-commit hooks -uv run poe dev # dev server → http://localhost:7860 -uv run poe test # run tests -``` - -## Quick Usage - -```python -# Core — always available -from dataenginex.core import MedallionArchitecture, QualityGate -from dataenginex.data import SchemaRegistry -from dataenginex.ml import ModelRegistry - -# API — requires pip install dataenginex[api] -from dataenginex.api import HealthChecker, AuthMiddleware, paginate -from dataenginex.middleware import configure_logging, configure_tracing - -# Storage — requires the relevant extra -from dataenginex.lakehouse import JsonStorage, get_storage -storage = get_storage("file://./data") # always works -storage = get_storage("s3://my-bucket") # requires [s3] -storage = get_storage("gs://my-bucket") # requires [gcs] -storage = get_storage("bq://my-project/ds") # requires [bq] -``` - -## Module Overview - -| Module | Requires Extra | Description | -|--------|----------------|-------------| -| `dataenginex.core` | — | Medallion architecture, schemas, quality gates, validators | -| `dataenginex.data` | — | Schema registry, data contracts, catalog | -| `dataenginex.lakehouse` | optional `[s3]` `[gcs]` `[bq]` | Storage backends (JSON, Parquet, S3, GCS, BigQuery), catalog, partitioning | -| `dataenginex.warehouse` | — | Warehouse layers, lineage tracking | -| `dataenginex.ml` | — | Model registry, vectorstore, LLM adapters, drift detection | -| `dataenginex.api` | `[api]` | Auth (JWT), health checks, error handling, pagination, rate limiting | -| `dataenginex.middleware` | `[api]` | Structured logging, Prometheus metrics, OpenTelemetry tracing | - -## Plugin System - -Extend the framework by implementing `DataEngineXPlugin` and registering an entry point: - -```toml -# pyproject.toml -[project.entry-points."dataenginex.plugins"] -my_plugin = "my_package.plugin:MyPlugin" -``` - -```python -from dataenginex.plugins import discover, PluginRegistry - -plugins = discover() # auto-loads all installed plugins -registry = PluginRegistry() -for plugin in plugins: - registry.register(plugin) - -status = registry.health_check_all() -``` - -## Workspace - -Part of the [DataEngineX](https://github.com/TheDataEngineX) ecosystem: - -| Repo | Purpose | Port | -|------|---------|------| -| [DataEngineX](https://github.com/TheDataEngineX/dataenginex) | Core framework (data + ML + AI) | — | -| [dex-studio](https://github.com/TheDataEngineX/dex-studio) | Desktop UI | 7860 | -| [infradex](https://github.com/TheDataEngineX/infradex) | Infrastructure | — | - -Full observability stack (Prometheus + Grafana + Jaeger): see [infradex](https://github.com/TheDataEngineX/infradex) `docker-compose.monitoring.yml`. - -## Pages - -- [[Architecture]] — Medallion pipeline, module graph, extras -- [[ML-Guide]] — ModelRegistry, RAG, LLM providers, drift detection -- [[Development]] — Build, test, and run commands - ---- - -**License**: MIT | **Python**: 3.13+ diff --git a/wiki-content/ML-Guide.md b/wiki-content/ML-Guide.md deleted file mode 100644 index 294b0a8..0000000 --- a/wiki-content/ML-Guide.md +++ /dev/null @@ -1,172 +0,0 @@ -# ML Guide - -All ML functionality lives in `dataenginex.ml`. The core install includes the module — no extra required unless you use `SentenceTransformerEmbedder` (which needs `[ml]`). - -## ModelRegistry - -`ModelRegistry` provides a JSON-persisted model lifecycle manager with four stages: `development → staging → production → archived`. - -```python -from dataenginex.ml import ModelRegistry - -registry = ModelRegistry(storage_path="./model-registry.json") - -# Register a model -registry.register( - name="salary-predictor", - version="1.0.0", - artifact_path="./models/salary_predictor.pkl", - metadata={"framework": "xgboost", "accuracy": 0.91}, -) - -# Promote through lifecycle -registry.promote("salary-predictor", "1.0.0", target_stage="staging") -registry.promote("salary-predictor", "1.0.0", target_stage="production") - -# List models -models = registry.list_models() -``` - -Model serialization uses `SafeUnpickler` (restricts deserialization to sklearn/numpy namespaces) with HMAC signature verification on load. Set `DATAENGINEX_MODEL_HMAC_KEY` in your environment. - -## RAG Pipeline - -`RAGPipeline` orchestrates document ingestion and semantic retrieval. It wraps a `VectorStoreBackend` and an optional embedding function. - -### Vector Store Backends - -| Backend | Description | -|---------|-------------| -| `InMemoryBackend` | In-process vector store (dev/test) | -| `ChromaDBBackend` | Persistent ChromaDB store (production) | - -```python -from dataenginex.ml import RAGPipeline, InMemoryBackend - -backend = InMemoryBackend() -pipeline = RAGPipeline(vector_store=backend) - -# Ingest documents -pipeline.ingest([ - {"id": "doc1", "text": "Data engineering best practices..."}, - {"id": "doc2", "text": "Medallion architecture overview..."}, -]) - -# Retrieve relevant documents -results = pipeline.retrieve("How does medallion architecture work?", top_k=3) -``` - -### SentenceTransformerEmbedder (v0.6.1+) - -A thin wrapper over `sentence-transformers` using `all-MiniLM-L6-v2` by default. Implements the `embed_fn` protocol for `RAGPipeline`. - -```bash -pip install dataenginex[ml] -``` - -```python -from dataenginex.ml import RAGPipeline, InMemoryBackend, SentenceTransformerEmbedder - -embedder = SentenceTransformerEmbedder(model_name="all-MiniLM-L6-v2") -pipeline = RAGPipeline(vector_store=InMemoryBackend(), embed_fn=embedder) -``` - -### Full RAG Loop with RAGPipeline.answer() (v0.6.1+) - -`RAGPipeline.answer(question, llm, ...)` performs the complete retrieve → augment → generate loop in one call: - -```python -from dataenginex.ml import RAGPipeline, InMemoryBackend, OllamaProvider - -llm = OllamaProvider(model="llama3.2") -pipeline = RAGPipeline(vector_store=InMemoryBackend(), embed_fn=embedder) -pipeline.ingest(documents) - -answer = pipeline.answer( - question="What is the medallion architecture?", - llm=llm, - top_k=3, -) -print(answer) -``` - -See `examples/05_rag_demo.py` for a complete end-to-end demo with CLI flags (`--embed`, `--llm`, `--model`). - -## LLM Providers - -`LLMProvider` is an ABC with two concrete implementations: - -### OllamaProvider - -Connects to a local [Ollama](https://ollama.ai) instance via REST API. - -```python -from dataenginex.ml import OllamaProvider, LLMConfig - -llm = OllamaProvider( - config=LLMConfig( - model="llama3.2", - base_url="http://localhost:11434", - temperature=0.7, - max_tokens=512, - ) -) - -response = llm.generate("Summarize the medallion architecture in one paragraph.") -print(response.text) - -# RAG-style augmented generation -response = llm.generate_with_context( - question="What is data quality?", - context="Data quality measures completeness, freshness, and uniqueness...", -) -``` - -`OllamaProvider` raises `ConnectionError` on HTTP failures (no silent empty responses). - -### MockProvider - -For testing without a running LLM: - -```python -from dataenginex.ml import MockProvider - -llm = MockProvider(responses=["Mock answer 1", "Mock answer 2"]) -response = llm.generate("Any question") -``` - -### Key Dataclasses - -| Class | Purpose | -|-------|---------| -| `LLMConfig` | Model name, base URL, temperature, max tokens | -| `LLMResponse` | Generated text, token counts, model metadata | -| `ChatMessage` | Role + content for multi-turn conversations | - -## Drift Detector - -PSI (Population Stability Index) based drift detection: - -```python -from dataenginex.ml import DriftDetector - -detector = DriftDetector(psi_threshold=0.2) - -# Compare reference vs current distribution -report = detector.detect( - reference=reference_dataframe["feature"], - current=current_dataframe["feature"], -) - -if report.is_drifted: - print(f"Drift detected: PSI={report.psi_score:.4f}") -``` - -PSI interpretation: -- `< 0.1` — No drift -- `0.1–0.2` — Moderate drift, investigate -- `> 0.2` — Significant drift, retrain model - -## PySpark ML Pipelines - -For Spark-based ML, see `examples/08_spark_ml.py` in the repository. PySpark is an optional dependency — the core package does not require it. diff --git a/wiki-content/push-all-wikis.sh b/wiki-content/push-all-wikis.sh deleted file mode 100755 index b0ba191..0000000 --- a/wiki-content/push-all-wikis.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env bash -# Push pre-written wiki content to all DataEngineX GitHub wiki repos. -# PREREQUISITE: Visit each repo's /wiki tab in the browser and create -# any first page (saves the placeholder) before running this script. -# -# Usage: bash /home/jay/workspace/DataEngineX/.github/wiki-content/push-all-wikis.sh -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -GH_TOKEN=$(gh auth token) -BOT_EMAIL="github-actions[bot]@users.noreply.github.com" -BOT_NAME="github-actions[bot]" - -push_wiki() { - local repo=$1 - local src_dir="$2" - local wiki_dir="/tmp/push-wiki-${repo}" - - echo "--- Cloning ${repo} wiki ---" - rm -rf "$wiki_dir" - git clone "https://x-access-token:${GH_TOKEN}@github.com/TheDataEngineX/${repo}.wiki.git" "$wiki_dir" 2>&1 \ - || { echo "SKIP: ${repo} wiki not initialized (visit https://github.com/TheDataEngineX/${repo}/wiki)"; return; } - - cp -r "${src_dir}"/*.md "$wiki_dir/" - cd "$wiki_dir" - git config user.email "$BOT_EMAIL" - git config user.name "$BOT_NAME" - git add . - git diff --cached --quiet && { echo "OK: ${repo} (no changes)"; return; } - git commit -m "docs: update wiki content" - git push origin HEAD 2>&1 && echo "OK: ${repo}" || echo "FAILED: ${repo}" -} - -push_wiki dataenginex "${SCRIPT_DIR}" diff --git a/workflow-templates/docker-build.yml b/workflow-templates/docker-build.yml new file mode 100644 index 0000000..c5b3d9b --- /dev/null +++ b/workflow-templates/docker-build.yml @@ -0,0 +1,53 @@ +name: Docker Build & Push + +on: + workflow_call: + inputs: + image-name: + description: "Docker image name (e.g. dataenginex)" + required: true + type: string + dockerfile: + description: "Path to Dockerfile" + required: false + type: string + default: "Dockerfile" + secrets: + GHCR_TOKEN: + required: true + +env: + REGISTRY: ghcr.io + IMAGE_NAME: TheDataEngineX/${{ inputs.image-name }} + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v6 + + - uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GHCR_TOKEN }} + + - uses: docker/metadata-action@v5 + id: meta + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=sha + type=ref,event=branch + type=semver,pattern={{version}} + + - uses: docker/build-push-action@v6 + with: + context: . + file: ${{ inputs.dockerfile }} + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/workflow-templates/helm-release.yml b/workflow-templates/helm-release.yml new file mode 100644 index 0000000..6b9b1a7 --- /dev/null +++ b/workflow-templates/helm-release.yml @@ -0,0 +1,57 @@ +name: Helm Release + +on: + workflow_call: + inputs: + chart-path: + description: "Path to the Helm chart directory" + required: true + type: string + chart-version: + description: "Chart version to release" + required: true + type: string + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: azure/setup-helm@v4 + - run: helm lint ${{ inputs.chart-path }} + + package: + needs: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: azure/setup-helm@v4 + + - name: Package chart + run: helm package ${{ inputs.chart-path }} --version ${{ inputs.chart-version }} + + - name: Upload chart artifact + uses: actions/upload-artifact@v4 + with: + name: helm-chart + path: "*.tgz" + + publish: + needs: package + runs-on: ubuntu-latest + permissions: + packages: write + steps: + - uses: actions/download-artifact@v4 + with: + name: helm-chart + + - uses: azure/setup-helm@v4 + + - name: Login to GHCR + run: echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io -u ${{ github.actor }} --password-stdin + + - name: Push to GHCR OCI registry + run: | + CHART_FILE=$(ls *.tgz) + helm push "$CHART_FILE" oci://ghcr.io/${{ github.repository_owner }}/charts diff --git a/workflow-templates/python-ci.yml b/workflow-templates/python-ci.yml new file mode 100644 index 0000000..767efdd --- /dev/null +++ b/workflow-templates/python-ci.yml @@ -0,0 +1,57 @@ +name: Python CI + +on: + workflow_call: + inputs: + python-version: + description: "Python version to use" + required: false + type: string + default: "3.13" + package-path: + description: "Path to source package" + required: false + type: string + default: "src/" + coverage-threshold: + description: "Minimum coverage percentage" + required: false + type: number + default: 80 + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: astral-sh/setup-uv@v8.1.0 + with: + version: "latest" + - run: uv sync --group dev + - run: uv run ruff check ${{ inputs.package-path }} tests/ + - run: uv run ruff format --check ${{ inputs.package-path }} tests/ + + typecheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: astral-sh/setup-uv@v8.1.0 + with: + version: "latest" + - run: uv sync --group dev + - run: uv run mypy ${{ inputs.package-path }} --config-file=pyproject.toml + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: astral-sh/setup-uv@v8.1.0 + with: + version: "latest" + - run: uv sync --group dev + - run: uv run pytest tests/ -v --cov --cov-report=xml --cov-fail-under=${{ inputs.coverage-threshold }} + - uses: actions/upload-artifact@v4 + if: always() + with: + name: coverage-report + path: coverage.xml diff --git a/workflow-templates/security-scan.yml b/workflow-templates/security-scan.yml new file mode 100644 index 0000000..2a72665 --- /dev/null +++ b/workflow-templates/security-scan.yml @@ -0,0 +1,69 @@ +name: Security Scan + +on: + push: + branches: [main, dev] + pull_request: + branches: [main, dev] + schedule: + - cron: "0 6 * * 1" # Weekly on Monday 06:00 UTC + +permissions: + contents: read + security-events: write + +jobs: + trivy: + name: Trivy vulnerability scan + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Run Trivy (SARIF) + uses: aquasecurity/trivy-action@0.36.0 + with: + scan-type: fs + scan-ref: . + format: sarif + output: trivy-results.sarif + severity: CRITICAL,HIGH + + - name: Upload Trivy results to GitHub Security + uses: github/codeql-action/upload-sarif@v4 + if: always() + with: + sarif_file: trivy-results.sarif + + codeql: + name: CodeQL analysis + runs-on: ubuntu-latest + strategy: + matrix: + language: [python] + steps: + - uses: actions/checkout@v6 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + + - name: Autobuild + uses: github/codeql-action/autobuild@v4 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + + pip-audit: + name: Dependency audit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: astral-sh/setup-uv@v8.1.0 + with: + version: "latest" + - uses: actions/setup-python@v6 + with: + python-version: "3.13" + - run: uv sync + - run: uvx pip-audit --progress-spinner off diff --git a/workflows/reusable-security.yml b/workflows/reusable-security.yml deleted file mode 100644 index e7507d3..0000000 --- a/workflows/reusable-security.yml +++ /dev/null @@ -1,83 +0,0 @@ -# Reusable workflow — called from each repo's .github/workflows/security.yml -# Logic lives here once; repo files are thin callers. -# -# Runs Trivy (misconfig + secret scan) on push/PR. -# CodeQL is handled per-repo via GitHub's default setup (only dex-studio has it active). -# Do NOT add a custom codeql job — GitHub rejects duplicate SARIF uploads. -# Results appear in GitHub Security tab. - -name: Security Scans (reusable) - -on: - workflow_call: - -env: - SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} - -jobs: - trivy-security: - runs-on: ubuntu-latest - permissions: - contents: read - security-events: write - steps: - - uses: actions/checkout@v4 - - name: Run Trivy repo security scan - uses: aquasecurity/trivy-action@v0.36.0 - with: - scan-type: fs - scan-ref: . - scanners: misconfig,secret - format: sarif - output: trivy-repo-results.sarif - severity: HIGH,CRITICAL - exit-code: '0' - - - name: Upload Trivy repo results to GitHub Security - if: always() - uses: github/codeql-action/upload-sarif@v4 - with: - sarif_file: trivy-repo-results.sarif - - - name: Enforce Trivy misconfig gate (HIGH/CRITICAL) - uses: aquasecurity/trivy-action@v0.36.0 - with: - scan-type: fs - scan-ref: . - scanners: misconfig - format: table - severity: HIGH,CRITICAL - exit-code: '1' - - - name: Notify Slack - if: always() - uses: actions/github-script@v9 - with: - script: | - if (!process.env.SLACK_WEBHOOK) return; - const status = process.env.STATUS === 'success' ? 'passed' : 'failed'; - const emoji = process.env.STATUS === 'success' ? '✅' : '⚠️'; - try { - await fetch(process.env.SLACK_WEBHOOK, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - text: `${emoji} Security scans ${status}`, - blocks: [{ - type: 'section', - text: { type: 'mrkdwn', text: `*${emoji} Security Scans ${status === 'passed' ? 'Passed' : 'Issues Detected'}*` } - }, { - type: 'section', - fields: [ - { type: 'mrkdwn', text: `*Branch:* ${process.env.REF}` }, - { type: 'mrkdwn', text: `*Run:* <${process.env.URL}|View Details>` } - ] - }] - }) - }); - } catch (e) { console.log('Slack notification failed:', e.message); } - env: - SLACK_WEBHOOK: ${{ env.SLACK_WEBHOOK }} - STATUS: ${{ job.status }} - REF: ${{ github.ref_name }} - URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}