foodguidecanada 3.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. foodguidecanada-3.0.0/.dockerignore +12 -0
  2. foodguidecanada-3.0.0/.github/workflows/ci.yml +66 -0
  3. foodguidecanada-3.0.0/.github/workflows/data-refresh.yml +114 -0
  4. foodguidecanada-3.0.0/.github/workflows/release.yml +91 -0
  5. foodguidecanada-3.0.0/.gitignore +29 -0
  6. foodguidecanada-3.0.0/CHANGELOG.md +31 -0
  7. foodguidecanada-3.0.0/CONTRIBUTING.md +262 -0
  8. foodguidecanada-3.0.0/Dockerfile +39 -0
  9. foodguidecanada-3.0.0/IMPLEMENTATIONS.MD +175 -0
  10. foodguidecanada-3.0.0/LICENSE +21 -0
  11. foodguidecanada-3.0.0/PKG-INFO +350 -0
  12. foodguidecanada-3.0.0/README.md +322 -0
  13. foodguidecanada-3.0.0/docker-compose.yml +15 -0
  14. foodguidecanada-3.0.0/docs/RELEASING.md +26 -0
  15. foodguidecanada-3.0.0/docs/data.md +78 -0
  16. foodguidecanada-3.0.0/docs/demo.gif +0 -0
  17. foodguidecanada-3.0.0/docs/demo.tape +43 -0
  18. foodguidecanada-3.0.0/docs/mcp.md +196 -0
  19. foodguidecanada-3.0.0/main.py +11 -0
  20. foodguidecanada-3.0.0/pyproject.toml +81 -0
  21. foodguidecanada-3.0.0/requirements.txt +8 -0
  22. foodguidecanada-3.0.0/scripts/check_manifest.py +48 -0
  23. foodguidecanada-3.0.0/src/foodguide/__init__.py +8 -0
  24. foodguidecanada-3.0.0/src/foodguide/api/__init__.py +5 -0
  25. foodguidecanada-3.0.0/src/foodguide/api/cnf.py +492 -0
  26. foodguidecanada-3.0.0/src/foodguide/api/cnf_api.py +287 -0
  27. foodguidecanada-3.0.0/src/foodguide/api/dri.py +574 -0
  28. foodguidecanada-3.0.0/src/foodguide/api/eer.py +593 -0
  29. foodguidecanada-3.0.0/src/foodguide/api/recipe.py +233 -0
  30. foodguidecanada-3.0.0/src/foodguide/builders/__init__.py +1 -0
  31. foodguidecanada-3.0.0/src/foodguide/builders/dri_eer.py +1135 -0
  32. foodguidecanada-3.0.0/src/foodguide/cli.py +1915 -0
  33. foodguidecanada-3.0.0/src/foodguide/cnf/__init__.py +1 -0
  34. foodguidecanada-3.0.0/src/foodguide/cnf/builder.py +489 -0
  35. foodguidecanada-3.0.0/src/foodguide/cnf/db.py +180 -0
  36. foodguidecanada-3.0.0/src/foodguide/cnf/mcp_source.py +213 -0
  37. foodguidecanada-3.0.0/src/foodguide/config.py +49 -0
  38. foodguidecanada-3.0.0/src/foodguide/data/dri_macronutrients.json +1049 -0
  39. foodguidecanada-3.0.0/src/foodguide/data/eer_equations.json +267 -0
  40. foodguidecanada-3.0.0/src/foodguide/data_manager.py +365 -0
  41. foodguidecanada-3.0.0/src/foodguide/db/__init__.py +8 -0
  42. foodguidecanada-3.0.0/src/foodguide/db/cnf_tools.py +1803 -0
  43. foodguidecanada-3.0.0/src/foodguide/db/connection.py +43 -0
  44. foodguidecanada-3.0.0/src/foodguide/db/dri_tools.py +1101 -0
  45. foodguidecanada-3.0.0/src/foodguide/db/eer_tools.py +456 -0
  46. foodguidecanada-3.0.0/src/foodguide/db/math_tools.py +1132 -0
  47. foodguidecanada-3.0.0/src/foodguide/db/queries.py +303 -0
  48. foodguidecanada-3.0.0/src/foodguide/db/schema.py +1378 -0
  49. foodguidecanada-3.0.0/src/foodguide/db/sql_engine.py +339 -0
  50. foodguidecanada-3.0.0/src/foodguide/models/__init__.py +4 -0
  51. foodguidecanada-3.0.0/src/foodguide/models/cnf_models.py +524 -0
  52. foodguidecanada-3.0.0/src/foodguide/models/db_models.py +29 -0
  53. foodguidecanada-3.0.0/src/foodguide/models/dri_models.py +272 -0
  54. foodguidecanada-3.0.0/src/foodguide/models/eer_models.py +172 -0
  55. foodguidecanada-3.0.0/src/foodguide/models/math_models.py +49 -0
  56. foodguidecanada-3.0.0/src/foodguide/pipeline/__init__.py +1 -0
  57. foodguidecanada-3.0.0/src/foodguide/pipeline/build_all.py +302 -0
  58. foodguidecanada-3.0.0/src/foodguide/pipeline/install.py +391 -0
  59. foodguidecanada-3.0.0/src/foodguide/pipeline/manifest.py +299 -0
  60. foodguidecanada-3.0.0/src/foodguide/recipes/__init__.py +1 -0
  61. foodguidecanada-3.0.0/src/foodguide/recipes/builder.py +93 -0
  62. foodguidecanada-3.0.0/src/foodguide/recipes/fetch.py +88 -0
  63. foodguidecanada-3.0.0/src/foodguide/recipes/parser.py +152 -0
  64. foodguidecanada-3.0.0/src/foodguide/recipes/source.py +191 -0
  65. foodguidecanada-3.0.0/src/foodguide/resources.py +117 -0
  66. foodguidecanada-3.0.0/src/foodguide/server.py +236 -0
  67. foodguidecanada-3.0.0/src/foodguide/utils/__init__.py +5 -0
  68. foodguidecanada-3.0.0/tests/cnf_fixture.py +114 -0
  69. foodguidecanada-3.0.0/tests/conftest.py +28 -0
  70. foodguidecanada-3.0.0/tests/fixtures/dri_eer/dri_macronutrients.json +1049 -0
  71. foodguidecanada-3.0.0/tests/fixtures/dri_eer/dri_minimal.html +164 -0
  72. foodguidecanada-3.0.0/tests/fixtures/dri_eer/eer_equations.json +267 -0
  73. foodguidecanada-3.0.0/tests/fixtures/dri_eer/eer_minimal.html +277 -0
  74. foodguidecanada-3.0.0/tests/fixtures/golden_cnf.json +6142 -0
  75. foodguidecanada-3.0.0/tests/fixtures/golden_recipes.json +206 -0
  76. foodguidecanada-3.0.0/tests/fixtures/pages/README.md +28 -0
  77. foodguidecanada-3.0.0/tests/fixtures/pages/SHA256SUMS +15 -0
  78. foodguidecanada-3.0.0/tests/fixtures/pages/dri_macronutrients.html +1207 -0
  79. foodguidecanada-3.0.0/tests/fixtures/pages/eer_equations.html +754 -0
  80. foodguidecanada-3.0.0/tests/fixtures/pages/recipes/apple-berry-crisp.html +612 -0
  81. foodguidecanada-3.0.0/tests/fixtures/pages/recipes/apple-nachos-peanut-butter-caramel-sauce.html +610 -0
  82. foodguidecanada-3.0.0/tests/fixtures/pages/recipes/apple-sandwiches.html +612 -0
  83. foodguidecanada-3.0.0/tests/fixtures/pages/recipes/baked-fish-chips.html +633 -0
  84. foodguidecanada-3.0.0/tests/fixtures/pages/recipes/black-bean-enchiladas.html +655 -0
  85. foodguidecanada-3.0.0/tests/fixtures/pages/recipes/carrot-potato-pancakes.html +591 -0
  86. foodguidecanada-3.0.0/tests/fixtures/pages/recipes/colourful-quinoa-salad.html +582 -0
  87. foodguidecanada-3.0.0/tests/fixtures/pages/recipes/egg-veggie-scramble.html +600 -0
  88. foodguidecanada-3.0.0/tests/fixtures/pages/recipes/kale-chickpea-curry.html +645 -0
  89. foodguidecanada-3.0.0/tests/fixtures/pages/recipes/okroshka-cold-summer-soup.html +628 -0
  90. foodguidecanada-3.0.0/tests/fixtures/pages/recipes/slow-cooker-burrito-bowls.html +624 -0
  91. foodguidecanada-3.0.0/tests/fixtures/pages/recipes/tofu-masala-curry.html +591 -0
  92. foodguidecanada-3.0.0/tests/fixtures/pages/recipes_index.html +1471 -0
  93. foodguidecanada-3.0.0/tests/fixtures/recipes_html/broken.html +27 -0
  94. foodguidecanada-3.0.0/tests/fixtures/recipes_html/fruit-salad.html +42 -0
  95. foodguidecanada-3.0.0/tests/fixtures/recipes_html/index.html +53 -0
  96. foodguidecanada-3.0.0/tests/fixtures/recipes_html/no-tips.html +34 -0
  97. foodguidecanada-3.0.0/tests/fixtures/recipes_html/two-groups.html +48 -0
  98. foodguidecanada-3.0.0/tests/pages_fixture.py +310 -0
  99. foodguidecanada-3.0.0/tests/test_cli.py +74 -0
  100. foodguidecanada-3.0.0/tests/test_cnf_builder.py +244 -0
  101. foodguidecanada-3.0.0/tests/test_cnf_cli.py +221 -0
  102. foodguidecanada-3.0.0/tests/test_cnf_golden.py +123 -0
  103. foodguidecanada-3.0.0/tests/test_cnf_local_source.py +131 -0
  104. foodguidecanada-3.0.0/tests/test_data.py +23 -0
  105. foodguidecanada-3.0.0/tests/test_dri_eer_cli.py +246 -0
  106. foodguidecanada-3.0.0/tests/test_dri_eer_data_manager.py +135 -0
  107. foodguidecanada-3.0.0/tests/test_dri_eer_mcp.py +151 -0
  108. foodguidecanada-3.0.0/tests/test_dri_eer_parsers.py +352 -0
  109. foodguidecanada-3.0.0/tests/test_dri_eer_snapshot.py +170 -0
  110. foodguidecanada-3.0.0/tests/test_mcp_fixes.py +24 -0
  111. foodguidecanada-3.0.0/tests/test_offline_modules.py +74 -0
  112. foodguidecanada-3.0.0/tests/test_offline_subprocess.py +206 -0
  113. foodguidecanada-3.0.0/tests/test_pages_fixture.py +87 -0
  114. foodguidecanada-3.0.0/tests/test_pipeline_build_all.py +372 -0
  115. foodguidecanada-3.0.0/tests/test_pipeline_install.py +370 -0
  116. foodguidecanada-3.0.0/tests/test_pipeline_manifest.py +512 -0
  117. foodguidecanada-3.0.0/tests/test_recipes_cli.py +331 -0
  118. foodguidecanada-3.0.0/tests/test_recipes_golden.py +184 -0
  119. foodguidecanada-3.0.0/tests/test_recipes_mcp.py +151 -0
  120. foodguidecanada-3.0.0/tests/test_recipes_parser.py +222 -0
  121. foodguidecanada-3.0.0/tests/test_recipes_source.py +270 -0
  122. foodguidecanada-3.0.0/tests/test_server_import.py +13 -0
@@ -0,0 +1,12 @@
1
+ .git
2
+ .github
3
+ .venv
4
+ .pytest_cache
5
+ .ruff_cache
6
+ __pycache__
7
+ cache
8
+ tests
9
+ docs
10
+ dist
11
+ uv.lock
12
+ *.sqlite
@@ -0,0 +1,66 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ jobs:
8
+ test:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - name: Install uv
13
+ uses: astral-sh/setup-uv@v5
14
+ with:
15
+ enable-cache: true
16
+ - name: Install dependencies
17
+ run: uv sync
18
+ - name: Lint
19
+ run: uv run ruff check .
20
+ - name: Test
21
+ run: uv run pytest -q -m "not live"
22
+ - name: Build
23
+ run: uv build
24
+ - name: Smoke test entry point
25
+ run: uv run foodguide --help
26
+
27
+ docker:
28
+ # Builds the image (it downloads the data-latest release) and runs it the way docker-compose.yml does:
29
+ # read-only, no capabilities, 512 MB. Then talks to it over HTTP like a real client.
30
+ runs-on: ubuntu-latest
31
+ steps:
32
+ - uses: actions/checkout@v4
33
+ - uses: astral-sh/setup-uv@v5
34
+ - run: docker build -t foodguide-mcp .
35
+ - name: Run the container
36
+ run: |
37
+ docker run -d --name fg --read-only --tmpfs /tmp --cap-drop ALL \
38
+ --security-opt no-new-privileges:true --memory 512m \
39
+ -p 127.0.0.1:8000:8000 foodguide-mcp
40
+ for i in $(seq 1 40); do curl -fs -o /dev/null http://127.0.0.1:8000/mcp/ && break; sleep 2; done
41
+ curl -fsS -o /dev/null -w "GET /mcp/ -> %{http_code}\n" http://127.0.0.1:8000/mcp/
42
+ - name: Runs as a non-root user
43
+ run: test "$(docker exec fg id -u)" = "10001"
44
+ - name: Data is installed and verified
45
+ run: |
46
+ docker exec fg foodguide data status | tee status.txt
47
+ test "$(grep -c ': ok' status.txt)" -ge 5
48
+ - name: 24 tools over HTTP
49
+ run: |
50
+ uv run --with fastmcp python - <<'EOF'
51
+ import asyncio
52
+ from fastmcp import Client
53
+ async def main():
54
+ async with Client("http://127.0.0.1:8000/mcp") as c:
55
+ n = len(await c.list_tools())
56
+ print("tools:", n)
57
+ assert n == 24, n
58
+ r = await c.call_tool("search_and_get_cnf_macronutrients",
59
+ {"input_data": {"food_name": "lentils", "session_id": "ci", "max_results": 2}})
60
+ assert "Lentils" in str(r), r
61
+ asyncio.run(main())
62
+ EOF
63
+ - name: Container logs on failure
64
+ if: failure()
65
+ run: docker logs fg
66
+
@@ -0,0 +1,114 @@
1
+ name: Data refresh
2
+
3
+ # Rebuilds the datasets the CLI and MCP server read (CNF 2026 SQLite, recipes, DRI, EER) and
4
+ # publishes them as assets of the `data-latest` release. Runs weekly; publishes only when the
5
+ # content changed. `foodguide data fetch` installs from that release.
6
+
7
+ on:
8
+ schedule:
9
+ - cron: "17 9 * * 1" # Mondays 09:17 UTC
10
+ pull_request: # dry run: builds and checks everything, never publishes
11
+ paths:
12
+ - ".github/workflows/data-refresh.yml"
13
+ - "scripts/check_manifest.py"
14
+ - "src/foodguide/pipeline/**"
15
+ - "src/foodguide/builders/**"
16
+ workflow_dispatch:
17
+ inputs:
18
+ force:
19
+ description: "Publish even if nothing changed"
20
+ type: boolean
21
+ default: false
22
+
23
+ permissions:
24
+ contents: write # release assets
25
+ issues: write # failure report
26
+
27
+ concurrency:
28
+ group: data-refresh
29
+ cancel-in-progress: false
30
+
31
+ jobs:
32
+ refresh:
33
+ runs-on: ubuntu-latest
34
+ timeout-minutes: 45
35
+ env:
36
+ GH_TOKEN: ${{ github.token }}
37
+ TAG: data-latest
38
+ steps:
39
+ - uses: actions/checkout@v4
40
+ - uses: astral-sh/setup-uv@v5
41
+ with:
42
+ enable-cache: true
43
+ - run: uv sync
44
+
45
+ - name: Run the live test suite (full recipes crawl + snapshot comparisons)
46
+ run: uv run pytest -q -m live
47
+
48
+ - name: Fetch previous manifest
49
+ id: prev
50
+ run: |
51
+ mkdir -p prev
52
+ if gh release download "$TAG" -p manifest.json -D prev 2>/dev/null; then
53
+ echo "have=true" >> "$GITHUB_OUTPUT"
54
+ else
55
+ echo "have=false" >> "$GITHUB_OUTPUT"
56
+ fi
57
+
58
+ - name: Build all datasets
59
+ run: uv run foodguide data build all --download-cnf --out-dir out
60
+
61
+ - name: Sanity-check the build
62
+ run: |
63
+ if [ "${{ steps.prev.outputs.have }}" = "true" ]; then
64
+ uv run python scripts/check_manifest.py out/manifest.json prev/manifest.json
65
+ else
66
+ uv run python scripts/check_manifest.py out/manifest.json
67
+ fi
68
+
69
+ - name: Did anything change?
70
+ id: changed
71
+ run: |
72
+ if [ "${{ steps.prev.outputs.have }}" != "true" ] || [ "${{ inputs.force }}" = "true" ]; then
73
+ echo "changed=true" >> "$GITHUB_OUTPUT"; exit 0
74
+ fi
75
+ set +e
76
+ uv run foodguide data changed prev/manifest.json out/manifest.json | tee changed.txt
77
+ code=${PIPESTATUS[0]}
78
+ set -e
79
+ if [ "$code" = "0" ]; then echo "changed=false" >> "$GITHUB_OUTPUT"
80
+ elif [ "$code" = "10" ]; then echo "changed=true" >> "$GITHUB_OUTPUT"
81
+ else exit "$code"; fi
82
+
83
+ - name: Publish release assets
84
+ if: steps.changed.outputs.changed == 'true' && github.event_name != 'pull_request'
85
+ run: |
86
+ if ! gh release view "$TAG" >/dev/null 2>&1; then
87
+ gh release create "$TAG" --title "Data (latest)" \
88
+ --notes "Datasets built by the Data refresh workflow. Install with: foodguide data fetch"
89
+ fi
90
+ rm -f out/*.zip
91
+ gh release upload "$TAG" out/* --clobber
92
+ gh release edit "$TAG" --notes "Built $(date -u +%FT%TZ) from workflow run ${{ github.run_id }}. Install with: foodguide data fetch"
93
+
94
+ - name: Summary
95
+ if: always()
96
+ run: |
97
+ {
98
+ echo "## Data refresh"
99
+ echo "Published: ${{ steps.changed.outputs.changed || 'not reached' }}"
100
+ [ -f changed.txt ] && { echo '```'; cat changed.txt; echo '```'; }
101
+ [ -f out/manifest.json ] && { echo '```json'; cat out/manifest.json; echo '```'; }
102
+ } >> "$GITHUB_STEP_SUMMARY"
103
+
104
+ - name: Report failure
105
+ if: failure() && github.event_name != 'pull_request'
106
+ run: |
107
+ title="Data refresh failed"
108
+ url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
109
+ existing=$(gh issue list --state open --search "\"$title\" in:title" --json number --jq '.[0].number')
110
+ if [ -n "$existing" ]; then
111
+ gh issue comment "$existing" --body "Failed again: $url"
112
+ else
113
+ gh issue create --title "$title" --body "The scheduled data refresh failed: $url. Likely cause: a source page changed. The previous release is still in place."
114
+ fi
@@ -0,0 +1,91 @@
1
+ name: Release
2
+
3
+ # Push a tag like v3.0.0 (it must match the version in pyproject.toml). Builds the package, makes
4
+ # a GitHub release with the wheel and sdist, and publishes to PyPI with trusted publishing
5
+ # (no token: configure the "foodguidecanada" project's publisher on pypi.org once, see docs/RELEASING.md).
6
+
7
+ on:
8
+ push:
9
+ tags: ["v*"]
10
+
11
+ jobs:
12
+ build:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: astral-sh/setup-uv@v5
17
+ - run: uv sync
18
+ - run: uv run ruff check .
19
+ - run: uv run pytest -q -m "not live"
20
+ - name: Tag matches the package version
21
+ run: |
22
+ v=$(uv run python -c "import tomllib;print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
23
+ [ "v$v" = "${GITHUB_REF_NAME}" ] || { echo "tag ${GITHUB_REF_NAME} != package version $v"; exit 1; }
24
+ - run: uv build
25
+ - run: uvx twine check dist/*
26
+ - uses: actions/upload-artifact@v4
27
+ with:
28
+ name: dist
29
+ path: dist/
30
+
31
+ github-release:
32
+ needs: build
33
+ runs-on: ubuntu-latest
34
+ permissions:
35
+ contents: write
36
+ steps:
37
+ - uses: actions/checkout@v4
38
+ - uses: actions/download-artifact@v4
39
+ with:
40
+ name: dist
41
+ path: dist
42
+ - name: Release notes from CHANGELOG.md
43
+ run: |
44
+ v="${GITHUB_REF_NAME#v}"
45
+ awk -v v="$v" '$0 ~ "^## "v" " {f=1; next} /^## / {f=0} f' CHANGELOG.md > notes.md
46
+ [ -s notes.md ] || echo "Release $v" > notes.md
47
+ - name: Create the release
48
+ env:
49
+ GH_TOKEN: ${{ github.token }}
50
+ run: |
51
+ v="${GITHUB_REF_NAME#v}"
52
+ gh release create "$GITHUB_REF_NAME" dist/* --title "MCP${v%.*}" --notes-file notes.md
53
+
54
+ pypi:
55
+ needs: build
56
+ runs-on: ubuntu-latest
57
+ environment: pypi
58
+ permissions:
59
+ id-token: write
60
+ steps:
61
+ - uses: actions/download-artifact@v4
62
+ with:
63
+ name: dist
64
+ path: dist
65
+ - uses: pypa/gh-action-pypi-publish@release/v1
66
+
67
+ docker:
68
+ needs: build
69
+ runs-on: ubuntu-latest
70
+ permissions:
71
+ contents: read
72
+ packages: write
73
+ steps:
74
+ - uses: actions/checkout@v4
75
+ - uses: docker/setup-buildx-action@v3
76
+ - uses: docker/login-action@v3
77
+ with:
78
+ registry: ghcr.io
79
+ username: ${{ github.actor }}
80
+ password: ${{ secrets.GITHUB_TOKEN }}
81
+ - name: Image tags
82
+ id: meta
83
+ run: |
84
+ v="${GITHUB_REF_NAME#v}"
85
+ repo=$(echo "ghcr.io/${GITHUB_REPOSITORY}" | tr '[:upper:]' '[:lower:]')
86
+ echo "tags=$repo:$v,$repo:latest" >> "$GITHUB_OUTPUT"
87
+ - uses: docker/build-push-action@v6
88
+ with:
89
+ context: .
90
+ push: true
91
+ tags: ${{ steps.meta.outputs.tags }}
@@ -0,0 +1,29 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .venv/
6
+ venv/
7
+ dist/
8
+ build/
9
+
10
+ # Tooling caches
11
+ .pytest_cache/
12
+ .ruff_cache/
13
+
14
+ # Local databases (never commit)
15
+ *.db
16
+ *.sqlite
17
+ *.sqlite3
18
+
19
+ # Secrets / local env
20
+ .env
21
+ .env.*
22
+
23
+ # Scrape caches and logs
24
+ cache/
25
+ logs/
26
+ uv.lock
27
+
28
+ # macOS
29
+ .DS_Store
@@ -0,0 +1,31 @@
1
+ # Changelog
2
+
3
+ ## 3.0.0 (2026-11-02)
4
+
5
+ A rewrite around an installable package and offline data. The old `v2.0` was the MCP server alone.
6
+
7
+ - **Package:** published as `foodguidecanada` (import name and commands stay `foodguide`): a typer CLI
8
+ (`foodguide`) and the MCP server (`foodguide-mcp`, install with the `mcp` extra). The CLI install is light (no pandas, no fastmcp).
9
+ - **Offline data, no network at run time:** Canadian Nutrient File 2026 as SQLite (EN/FR full-text search),
10
+ the Food Guide recipes (141, from the new canada.ca pages), Health Canada DRI and EER tables.
11
+ - **CLI:** `cnf`, `recipes`, `dri`, `eer`, `data` (build, status, fetch, manifest, changed).
12
+ - **Data pipeline:** `foodguide data build all` plus a weekly GitHub Actions workflow that rebuilds the
13
+ datasets and publishes them to the `data-latest` release only when the content changed.
14
+ - **MCP fixes:** state folder is created, session tables are created at startup, `list_filters` validates,
15
+ recipe tools work again against the local data.
16
+ - **Tests:** no mocks. Real saved pages, local servers and real subprocesses; golden answer keys made by a
17
+ separate script.
18
+ - **Breaking changes for MCP clients (recipes):**
19
+ - `search_recipes`: `max_pages` is replaced by `limit` (default 20); it returns an object
20
+ `{recipes, count, filters_applied, filters_ignored, ...}` instead of a list.
21
+ - `fruits`, `vegetables`, `proteins`, `whole_grains` and `appliances` filters no longer exist on the
22
+ new canada.ca pages; they are reported in `filters_ignored`. Use `search_text`, `meals` and
23
+ `collections`.
24
+ - `get_recipe`: the `url` argument is now `url_or_slug` (a slug or a canada.ca recipe URL).
25
+ - `list_filters` returns `{group: {tag: label}}`.
26
+ - CNF tools read the local SQLite database. Run `foodguide data fetch` (or `data build cnf`) first.
27
+ - **Security default:** the MCP HTTP transport now listens on `127.0.0.1` (it was `0.0.0.0` with no
28
+ authentication). Use `--host` or `HOST` to expose it deliberately.
29
+ - **Docker:** a `Dockerfile` and `docker-compose.yml` for the HTTP MCP server (pinned base image, non-root,
30
+ read-only, data baked in), and images published to GHCR on release.
31
+ - **Removed:** the live scrapers of the old `food-guide.canada.ca` host and `uv.lock`.
@@ -0,0 +1,262 @@
1
+ # Contributing to foodguide
2
+
3
+ `foodguide` is a command-line tool and an MCP server for Canada's Food Guide data. It reads the
4
+ Canadian Nutrient File (CNF) 2026, the Food Guide recipes, and the Health Canada DRI and EER
5
+ tables. All data is read from local files at run time. The MCP server exposes 46 tools on the
6
+ stdio transport.
7
+
8
+ Bug fixes, docs and new features are welcome. This page says how to set up, test and change the
9
+ code. For how the code fits together, read [IMPLEMENTATIONS.MD](IMPLEMENTATIONS.MD).
10
+
11
+ ## Setup
12
+
13
+ You need Python 3.12 or newer, [uv](https://docs.astral.sh/uv/) and Git.
14
+
15
+ ```bash
16
+ git clone https://github.com/Aryan-Jhaveri/mcp-foodguidecanada.git
17
+ cd mcp-foodguidecanada
18
+ uv sync
19
+ uv run foodguide --help
20
+ ```
21
+
22
+ `uv sync` installs the package in editable mode and the dev group. The dev group includes pytest,
23
+ ruff and the `mcp` extra (`foodguidecanada[mcp]`), so the MCP server tests can run.
24
+
25
+ Datasets are not in the repo. The CLI reads them from `~/.local/share/foodguide` (override with
26
+ `FOODGUIDE_DATA_DIR`). Install them with `uv run foodguide data fetch`, or build them yourself
27
+ (see "Change a dataset builder" below). The bundled DRI and EER JSON files in
28
+ `src/foodguide/data/` are the fallback for those two datasets.
29
+
30
+ ## Workflow
31
+
32
+ 1. Make a branch: `git checkout -b feat/short-name` or `fix/short-name`.
33
+ 2. Change the code. Add or update a test.
34
+ 3. Run lint and the tests (below).
35
+ 4. Commit with a clear message: `feat:`, `fix:`, `docs:`, `refactor:`, `test:` or `perf:`.
36
+ 5. Push and open a pull request.
37
+
38
+ ## Lint
39
+
40
+ ```bash
41
+ uv run ruff check .
42
+ ```
43
+
44
+ Settings are in `pyproject.toml`: line length 100, rules E, F, W and I. E501 is ignored because
45
+ older code has long data-table lines; keep new code under 100 characters.
46
+ `src/foodguide/server.py` ignores E402 on purpose: it sets warning filters before it imports
47
+ fastmcp.
48
+
49
+ ## Tests
50
+
51
+ ```bash
52
+ uv run pytest -q -m "not live" # what CI runs: fast, no internet needed
53
+ uv run pytest -q -m live # slow: fetches live canada.ca pages
54
+ ```
55
+
56
+ - `-m "not live"` is the CI run (`.github/workflows/ci.yml`, after `ruff check .`).
57
+ - `-m live` runs the full 141-page recipes crawl and the live DRI/EER snapshot comparisons. The
58
+ weekly data-refresh workflow (`.github/workflows/data-refresh.yml`) runs it too.
59
+ - `FOODGUIDE_SKIP_LIVE_TESTS=1` skips the live DRI/EER snapshot comparisons
60
+ (`tests/test_dri_eer_snapshot.py`), even in a `-m live` run. It does not skip the live recipes
61
+ crawl.
62
+ - Tests that start a local `http.server` skip cleanly in sandboxes that block loopback TCP
63
+ connections. They run in CI.
64
+ - `tests/test_cnf_golden.py` needs the CNF 2026 zip. It skips if the zip is missing. Point it at
65
+ your copy with `FOODGUIDE_TEST_CNF_ZIP`. The live recipes test can use a folder of crawled
66
+ pages in `FOODGUIDE_TEST_RECIPE_CACHE`.
67
+
68
+ ### Tests: no mocks (project rule)
69
+
70
+ We do **not** mock our tests. The suite runs against real inputs: real saved
71
+ canada.ca pages (`tests/fixtures/pages/`, served by a real localhost
72
+ `http.server` from the `pages_server` fixture), real files, a real SQLite
73
+ database, real subprocess runs of the CLI, and answer keys
74
+ (`tests/fixtures/golden_*.json`) produced by separate, independent scripts
75
+ that share no code with the package. Never edit an answer key by hand.
76
+
77
+ **Allowed**
78
+
79
+ - `monkeypatch.setenv` / `monkeypatch.delenv` / `monkeypatch.chdir`. These set
80
+ the environment, they fake nothing. The production code reads all its
81
+ configuration points from the environment at call time
82
+ (`FOODGUIDE_DATA_DIR`, `FOODGUIDE_DB_FILE`, `FOODGUIDE_STATE_DIR`,
83
+ `FOODGUIDE_SOURCE_BASE_URL`, `FOODGUIDE_CKAN_PACKAGE_URL`,
84
+ `FOODGUIDE_RECIPES_CACHE_DIR`, `SOURCE_DATE_EPOCH`), so tests never need to
85
+ patch attributes.
86
+ - A real `http.server.ThreadingHTTPServer` on 127.0.0.1 in a background thread
87
+ (`tests/pages_fixture.py`), including "broken" responses derived from real
88
+ pages (truncated HTML, a page with its Ingredients heading removed, HTTP 500).
89
+ - Answer keys from independent scripts; build options such as
90
+ `data build recipes --max-recipes/--only` to work with a small fixture set.
91
+
92
+ **Not allowed**
93
+
94
+ - `unittest.mock`, `MagicMock`, `pytest`'s `patch`, and any
95
+ `monkeypatch.setattr` / `monkeypatch.setitem` on code under test or on
96
+ libraries (`requests`, `datetime`, builders, fetchers).
97
+ - "Network is blocked" fixtures that swap `requests.Session.request` for a
98
+ raiser. Prove offline-ness instead with the static import check
99
+ (`tests/test_offline_modules.py`) and real subprocess runs with
100
+ `HTTP_PROXY`/`HTTPS_PROXY` pointed at a dead port
101
+ (`tests/test_offline_subprocess.py`).
102
+
103
+ ### Golden answer keys
104
+
105
+ - `tests/fixtures/golden_cnf.json`: CNF foods, nutrient values and measures, taken straight from the
106
+ CNF 2026 zip. Used by `tests/test_cnf_golden.py`.
107
+ - `tests/fixtures/golden_recipes.json`: recipe titles, ingredients, directions, times and servings.
108
+ Used by `tests/test_recipes_golden.py`.
109
+
110
+ The tests compare CLI output against these keys. The keys are checked in. The generator scripts
111
+ are **not** in this repo: they were written separately on purpose, so that they share no code with
112
+ the package, and there is no command here that regenerates them. Do not edit the files by hand. If
113
+ upstream data changes and a golden test fails, raise it in the pull request and regenerate the
114
+ key with an independent script that reads the source directly (the CNF zip or the canada.ca
115
+ pages), then commit the new key.
116
+
117
+ ## Add a CLI command
118
+
119
+ The CLI is one typer app in `src/foodguide/cli.py`.
120
+
121
+ - `app` is the root app. Each group is its own `typer.Typer` (`cnf_app`, `dri_app`, `eer_app`,
122
+ `recipes_app`, `data_app`, and `data_build_app` under `data_app`), attached with
123
+ `app.add_typer(...)`.
124
+ - A command is a function with `@<group>_app.command("name")`.
125
+ - The global `--format table|csv|json` option is the `OutputFormat` enum. It is stored in
126
+ `ctx.obj["format"]`. Commands also accept their own `--format` and fall back to the global one:
127
+ `fmt = format if format is not None else ctx.obj["format"]`.
128
+ - **stdout is data only.** Print results with `typer.echo` (or `csv.writer(sys.stdout)`). Print
129
+ log lines, warnings and errors with `_log(message)`, which writes to stderr.
130
+ - Exit codes: `raise typer.Exit(code=0)` on success, `1` on an error, `2` for bad usage. The
131
+ exception is `data changed`, which exits `10` when datasets differ.
132
+
133
+ Steps:
134
+
135
+ 1. Add `@<group>_app.command("name")` in `src/foodguide/cli.py`, with a docstring and an example.
136
+ 2. Support the three output formats. Keep stdout data-only.
137
+ 3. Add a test in `tests/` that runs the command against real data (the CLI tests use
138
+ `typer.testing.CliRunner` or a real subprocess).
139
+ 4. Run `uv run ruff check .` and `uv run pytest -q -m "not live"`.
140
+ 5. Update the command list in README.md.
141
+
142
+ ## Add an MCP tool
143
+
144
+ Tools are registered in `src/foodguide/server.py` through the `register_*` functions in
145
+ `src/foodguide/db/` and `src/foodguide/api/`, with `@mcp.tool()`. Tools that need SQLite session
146
+ state are only registered on the stdio transport. Add a test in `tests/` (see
147
+ `tests/test_dri_eer_mcp.py` and `tests/test_recipes_mcp.py`).
148
+
149
+ ## Change a dataset builder
150
+
151
+ Builders live here:
152
+
153
+ - DRI and EER: `src/foodguide/builders/dri_eer.py`
154
+ - CNF: `src/foodguide/cnf/builder.py` (reads the CNF zip from open.canada.ca)
155
+ - Recipes: `src/foodguide/recipes/` (`fetch.py`, `parser.py`, `builder.py`)
156
+
157
+ Rules for anything that fetches a page:
158
+
159
+ - Send the identifying User-Agent
160
+ `foodguide-data-build/0.1 (+https://github.com/Aryan-Jhaveri/mcp-foodguidecanada)`. canada.ca
161
+ stalls plain `Mozilla/5.0` agents. Do not change it.
162
+ - Wait at least 1 second between fetches (`PoliteFetcher`).
163
+ - Retry with backoff on network errors and 5xx responses.
164
+ - Build page URLs from `source_base_url()` in `src/foodguide/config.py`. It returns
165
+ `FOODGUIDE_SOURCE_BASE_URL`, default `https://www.canada.ca`. Tests use this to point the
166
+ builders at the local fixture server. Never hard-code the host.
167
+
168
+ After you change a builder:
169
+
170
+ ```bash
171
+ uv run foodguide data build dri # or: eer, recipes, cnf, all
172
+ uv run pytest -q -m "not live"
173
+ ```
174
+
175
+ If the output changed, regenerate the golden key with its independent generator (see "Golden
176
+ answer keys") and explain the change in the pull request. For a full build into a clean folder
177
+ with a manifest, use `uv run foodguide data build all --out-dir out`. Use `--download-cnf` to
178
+ download the CNF zip, or `--cnf-zip PATH`.
179
+
180
+ ## Code standards
181
+
182
+ Follow the patterns already in `src/foodguide`. `uv run ruff check .` must pass.
183
+
184
+ - **Type hints** on every function parameter and return value. Use `Optional[X]` for values that can be
185
+ `None`, and `List[...]` and `Dict[str, Any]` for collections.
186
+ - **Docstrings:** a short module docstring at the top of each file, and a docstring on each public
187
+ function that says what it does and what it returns. MCP tool docstrings are the text the model
188
+ reads, so write them for the model: say when to use the tool and what the arguments mean.
189
+ - **Comments:** use `#` for field explanations in dataclasses.
190
+ - **Errors:** return a clear message instead of raising into the client. MCP tools return
191
+ `{"error": "..."}`, and CLI commands print the message to stderr and exit 1.
192
+
193
+ ```python
194
+ def get_recipe(url_or_slug: str) -> Dict[str, Any]:
195
+ """Return one recipe by slug or canada.ca URL."""
196
+ try:
197
+ recipe = _get_source().get_recipe(url_or_slug)
198
+ except RuntimeError as exc:
199
+ return {"error": str(exc)}
200
+ if recipe is None:
201
+ return {"error": f"Recipe '{url_or_slug}' not found in local data."}
202
+ return recipe
203
+ ```
204
+
205
+ - **Offline modules stay offline.** Code that runs when a user asks a question (CLI readers, MCP tools)
206
+ must not import `requests` or `urllib.request`. Only the builders and `data fetch` touch the network.
207
+ `tests/test_offline_modules.py` checks this.
208
+
209
+ ## Commit messages
210
+
211
+ Use a short prefix and a clear sentence:
212
+
213
+ - `feat:` new feature
214
+ - `fix:` bug fix
215
+ - `docs:` documentation
216
+ - `refactor:` code change with no behaviour change
217
+ - `test:` adding or fixing tests
218
+ - `perf:` performance
219
+
220
+ ## Pull requests
221
+
222
+ Before you open one:
223
+
224
+ - `uv run ruff check .` passes.
225
+ - `uv run pytest -q -m "not live"` passes.
226
+ - Docs are updated if behaviour changed (README.md, IMPLEMENTATIONS.MD).
227
+ - If you tested the MCP server, say which client and transport.
228
+
229
+ A pull request should say what changed and why, the type of change (fix, feature, breaking, docs),
230
+ how you tested it, and any issue it closes. Maintainers review within a few days.
231
+
232
+ ## Releases
233
+
234
+ Maintainers cut releases. The steps (version bump, tag `vX.Y.Z`, PyPI trusted publishing) are in
235
+ [docs/RELEASING.md](docs/RELEASING.md). The PyPI name is `foodguidecanada`. The import package and
236
+ commands stay `foodguide` and `foodguide-mcp`.
237
+
238
+ ## Where help is wanted
239
+
240
+ - **Fewer tool calls.** Merge `calculate_recipe_nutrition_summary` and `query_recipe_macros_table`,
241
+ and `store_recipe_in_temp_tables` and `simple_recipe_setup`, so a model needs fewer steps.
242
+ - **Smaller files.** Split `db/cnf_tools.py` and move helpers into utility modules.
243
+ - **Diagrams.** A Mermaid diagram of the data flow and of how the tools work together.
244
+ - **More nutrition.** Micronutrients, vitamins and minerals in the DRI tables and the adequacy tools.
245
+ - **Easier install.** A Dockerfile for the HTTP transport, Smithery or npm packaging.
246
+ - **Ideas:** weekly meal planning, tracking ingredients in stock, personal nutrition goals, more
247
+ export formats.
248
+
249
+ Check [IMPLEMENTATIONS.MD](IMPLEMENTATIONS.MD) for the current list before you start, and open an
250
+ issue first for anything large.
251
+
252
+ ## Community
253
+
254
+ Be respectful, collaborative, constructive and patient. Use GitHub Issues for bugs and feature
255
+ requests, with clear titles and steps to reproduce. When you discuss nutrition behaviour, link the
256
+ Health Canada page it comes from.
257
+
258
+ ## Data credits
259
+
260
+ - Canadian Nutrient File 2026, Health Canada, under the Open Government Licence – Canada.
261
+ - Food Guide recipes, Crown copyright, Health Canada.
262
+ - Dietary Reference Intakes (DRI) and Estimated Energy Requirement (EER) tables, Health Canada.
@@ -0,0 +1,39 @@
1
+ # foodguidecanada MCP server over HTTP, with the datasets baked into the image.
2
+ #
3
+ # docker build -t foodguide-mcp .
4
+ # docker run --rm -p 127.0.0.1:8000:8000 --read-only --tmpfs /tmp --memory 512m foodguide-mcp
5
+ # endpoint: http://127.0.0.1:8000/mcp
6
+ #
7
+ # Lessons from running containers on a real server: pinned base image, non-root user, read-only
8
+ # root filesystem, a memory limit, published on loopback only (put TLS and auth in front for other
9
+ # people), and no network at run time (the data is fetched once, at build time).
10
+ #
11
+ # python:3.12-slim-bookworm, pinned by digest (2026-11-02).
12
+ FROM python:3.12-slim-bookworm@sha256:e697c81897302a4c4333bade000d98038fbb21eb064f8ee1fd42d02ebcd9d337
13
+
14
+ LABEL org.opencontainers.image.source="https://github.com/Aryan-Jhaveri/mcp-foodguidecanada" \
15
+ org.opencontainers.image.description="Canada's Food Guide MCP server (HTTP) with offline data" \
16
+ org.opencontainers.image.licenses="MIT"
17
+
18
+ # Which release of the datasets to bake in (the Data refresh workflow publishes them).
19
+ ARG DATA_TAG=data-latest
20
+
21
+ RUN useradd --create-home --uid 10001 app
22
+ WORKDIR /src
23
+ COPY pyproject.toml README.md LICENSE ./
24
+ COPY src ./src
25
+ RUN pip install --no-cache-dir '.[mcp]' && rm -rf /src
26
+
27
+ USER app
28
+ ENV FOODGUIDE_DATA_DIR=/home/app/data \
29
+ MCP_TRANSPORT=http \
30
+ HOST=0.0.0.0 \
31
+ PORT=8000 \
32
+ PYTHONUNBUFFERED=1
33
+ # Downloads from GitHub over HTTPS and checks every file against the release manifest.
34
+ RUN foodguide data fetch --tag "${DATA_TAG}" && foodguide data status | head -3
35
+
36
+ EXPOSE 8000
37
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=20s \
38
+ CMD python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/mcp/', timeout=4).status == 200 else 1)"
39
+ CMD ["foodguide-mcp"]