Skip to main content

mfgparams

CI codecov

A Python library and interactive command-line tool for metal machining calculations. It covers drilling (twist drills), milling (end milling and face milling), and turning (single-point), reporting spindle speed, feed rate, machining time, torque and required power — plus material removal rate for milling.

📖 Full generated documentation (Sphinx) — published automatically to GitHub Pages on every merge to main.

Contributing? See CONTRIBUTING.md for the dev workflow, CI gates, and this repo's spec-kit conventions.

License

mfgparams is free for noncommercial use (personal, hobby, research, education, evaluation, etc.) under the PolyForm Noncommercial License 1.0.0. Any commercial use — using the software inside a for-profit business, in a paid product or service, or any other revenue-generating context — requires a separate, paid commercial license from the copyright holder. To request one, open an issue: https://github.com/kniklas/mfgparams/issues/new. See LICENSE.md for the full terms; all rights not expressly granted there, including all commercial rights, are reserved.

Install

pip install mfgparams                # core dependencies only
pip install "mfgparams[console]"     # adds the console's dependencies
pip install "mfgparams[all]"         # adds every optional runtime dependency

[all] names the other extras rather than restating them, and pip only understands that form from 21.2 onward — older pips (including the one Python 3.9 ships) reject it. python -m pip install --upgrade pip first if you are on one.

An extra gates dependencies, not modules: every install ships the same wheel, mfgparams.console included. What [console] adds is what the console's text GUI needs (prompt-toolkit), so embedding the library in another application does not drag that requirement in with it. pip install mfgparams alone gives you every calculation function but not a working mfgparams command — the console needs [console] installed to run at all.

If a console dependency is ever unavailable, invoking mfgparams prints the exact command that fixes it and exits non-zero rather than showing a traceback. A failure inside mfgparams itself — a damaged install, a missing core dependency — still raises normally, because pip install "mfgparams[console]" would not fix it.

Package structure

Modules are grouped process-first: a manufacturing process contains its operations, and an operation may contain sub-operations.

mfgparams                                          public API — import from here
mfgparams.processes.machining.drilling
mfgparams.processes.machining.milling.end_milling
mfgparams.processes.machining.milling.face_milling
mfgparams.processes.machining.turning
mfgparams.console                                  interactive console

The calculation modules are re-exported at the top level, so from mfgparams import calculate is the supported way to reach them; the qualified paths are there to say where a calculation sits in the manufacturing domain. mfgparams.console is not part of that surface — the console is reached through the mfgparams command, python -m mfgparams, or python -m mfgparams.console. All three behave identically.

A future process (welding, joining, forming) attaches beside machining rather than reorganising it.

Install (development)

python -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip   # see DEVELOPMENT.md if this doesn't fix an old-pip issue
pip install -e ".[dev]"

See DEVELOPMENT.md for the required toolset (including running the full multi-version tox matrix locally), verifying the install landed correctly, and troubleshooting the issues contributors actually hit here — a stale pytest on PATH, pip too old for mfgparams[all]/editable installs, and getting tox to see every Python version.

Use as a library

from mfgparams import calculate, UnitSystem, CalculationMode

result = calculate(
    diameter=10,
    depth=25,
    material="Mild Steel",
    tool="Carbide",
    unit_system=UnitSystem.METRIC,
)
print(result)

Milling

End milling and face milling have their own entry points, since their inputs differ from drilling's:

from mfgparams import calculate_end_milling, calculate_face_milling

# End milling: a slot/profile cut, described by axial and radial depth of cut.
result = calculate_end_milling(
    diameter=10,               # cutter diameter, mm (METRIC) or in (IMPERIAL)
    axial_depth_of_cut=2,
    radial_depth_of_cut=5,
    feed_per_tooth=0.05,       # mm/tooth or in/tooth
    number_of_teeth=4,
    length_of_cut=100,
    material="Mild Steel",
    tool="Carbide",
)
print(result.material_removal_rate)  # cm3/min (METRIC) or in3/min (IMPERIAL)

# Face milling: a facing pass, described by axial depth and width of cut.
result = calculate_face_milling(
    diameter=50,
    axial_depth_of_cut=1.5,
    width_of_cut=40,
    feed_per_tooth=0.15,
    number_of_teeth=5,
    length_of_cut=200,
    material="Mild Steel",
    tool="Carbide",
)

Milling has its own tool catalogs, listed with list_end_mill_tools() and list_face_mill_tools(). Drilling results always leave material_removal_rate as None.

Both entry points also accept the same mode/target_rpm/available_power arguments as drilling's calculate() (see "Constrained calculation modes" below) — for example calculate_end_milling(..., mode=CalculationMode.FIXED_RPM, target_rpm=3000).

See specs/009-milling-calculations/quickstart.md and specs/010-milling-calculation-modes/quickstart.md for full scenarios.

Turning

Single-point turning has its own entry point, taking depth of cut and length of cut along the workpiece rather than milling's axial/radial geometry:

from mfgparams import calculate_turning

result = calculate_turning(
    diameter=40,               # workpiece diameter, mm (METRIC) or in (IMPERIAL)
    depth_of_cut=1.5,
    length_of_cut=80,
    material="Mild Steel",
    tool="Carbide",
)
print(result)

Turning has its own tool catalog, listed with list_turning_tools(), and accepts the same mode/target_rpm/available_power arguments as drilling's and milling's calculate() (see "Constrained calculation modes" below) — plus a turning-only target_feed_rate argument for its three additional feed-rate-constrained modes (see below).

See specs/019-turning-calculations/quickstart.md, specs/020-turning-feed-per-rotation/quickstart.md, and specs/021-turning-combined-constraints/quickstart.md for full scenarios.

Constrained calculation modes

Two opt-in modes are available alongside the default STANDARD mode:

# Power-constrained: reduce spindle speed to fit an available power budget.
result = calculate(
    diameter=10, depth=25, material="Mild Steel", tool="Carbide",
    mode=CalculationMode.POWER_CONSTRAINED,
    available_power=1.2,  # kW (METRIC) or HP (IMPERIAL)
)

# Fixed-RPM: calculate from a caller-supplied target spindle speed.
result = calculate(
    diameter=10, depth=25, material="Mild Steel", tool="Carbide",
    mode=CalculationMode.FIXED_RPM,
    target_rpm=500,
)

See specs/002-constrained-calculation-modes/quickstart.md for full scenarios, including error handling (INFEASIBLE_POWER_BUDGET, INVALID_TARGET_RPM, MODE_CONFLICT).

Use the interactive text GUI

mfgparams

(python -m mfgparams and python -m mfgparams.console reach the same interface.) A persistent menu bar stays visible across the top of the screen, in a distinct shade from the blue desktop directly beneath it (no divider line between them): Exit, Machining, Configuration, About, Help — navigate with the arrow keys and Enter (Down does the same as Enter: it opens the highlighted item, the natural "descend into" gesture for a horizontal bar), or an item's underlined keyboard shortcut. Choosing Machining, Configuration, About, or Help opens a floating dropdown, its top edge directly under the bar and its background matching the bar's own shade, with a Midnight Commander-style drop shadow — not inline content replacing the desktop. Machining's dropdown shows Milling, Drilling, and Turning as flat leaves; selecting any of them opens its operation screen directly, with no further tree-level expansion. Escape closes whichever dropdown/panel is open and returns focus to the bar; Up does the same the instant you're at the top of a navigable list (Machining's tree) or in a single-block panel with nothing to navigate (Configuration/About/Help) — the dropdown is erased, not left open-but-unfocused underneath. Selecting Exit opens a "Are you sure you want to exit?" confirmation dropdown (defaulting to No) rather than exiting immediately — Left/Right toggle Yes/No, Enter/Space confirms the highlighted choice, and y/n answer directly.

Opening Drilling, Milling, or Turning shows a centered, bordered floating window, shaded and colored the same cyan-on-black as every other floating window, over the menu bar and tree (which stay visible underneath, untouched): the left pane lists every input for that operation at once — unit system, calculation mode, material type/material, tool, and the operation's geometry fields (plus, for Milling, the end-milling/face-milling choice) — all simultaneously visible and editable, with no separate screen per field. The right pane shows the live result, refreshing automatically once every required field has a value. Up/Down (or j/k) always moves to the next/previous field, regardless of its type. A radio field (unit system, mode, material type, material, tool, sub-operation) is always a single Label: value line — Left/Right/h/l/Space cycle its value with wraparound and commit it immediately, with no separate confirm step. A numeric field becomes editable the instant you select it (typing a digit or ./- edits its buffer immediately, Backspace removes the last character, Left/Right nudges it by a small step) — but that text is only written to the field the instant you navigate away from it (Up/Down); text that still doesn't parse as a number at that point is discarded (the field keeps its last valid value) and a message appears in the status bar beneath both panes, replacing the usual keyboard hint until you correct it.

Escape closes the operation window outright (a single press, not two) and returns focus to the still-open Machining tree if it was expanded — not the bare menu bar — so you land back exactly where you opened the operation from; a second Escape from there closes the tree itself and reaches the bar. Either way, you can start another calculation — the same operation or a different one — without leaving the text GUI. Each operation remembers its own previous answers as defaults for the rest of the session.

The whole application follows one consistent, Turbo-Vision-style color scheme — a cyan bar, a distinct blue desktop behind it, and the bar's own cyan-on-black for every floating window (the Machining/Configuration/ About/Help dropdowns, the Exit confirmation dialog, and the Drilling/ Milling/Turning operation window alike), each with a Midnight Commander-style black drop shadow — rather than the terminal's own default background.

For drilling, the calculation-mode field (standard, power-constrained, fixed-rpm) sits right after unit system in the left pane; power-constrained then makes available power a required field, and fixed-rpm adds a required target spindle speed (with an optional advisory available power). Milling (both end milling and face milling) presents the same calculation-mode field in the same position. Turning presents a calculation-mode field in the same position too, with three additional feed-rate-constrained modes beyond drilling's set — see specs/020-turning-feed-per-rotation/quickstart.md and specs/021-turning-combined-constraints/quickstart.md.

Material selection is two-step

Materials are grouped by material type, so the text GUI asks for the type first and then only offers the materials belonging to it: choosing Wood then offers only Oak, Maple, Pine, Spruce, Fir, Plywood, MDF.

This keeps the material list short as the catalog grows. On a repeat calculation the previous type is offered as the default; switching to a different type discards the remembered material, so you always pick a material that actually belongs to the chosen type.

Configurable materials & tools

The built-in materials are grouped into two types:

  • metal — Mild Steel, Stainless Steel, Aluminum, Cast Iron, Brass, Titanium
  • wood — Oak, Maple (hardwood), Pine, Spruce, Fir (softwood), Plywood, MDF (engineered wood)

Three built-in tools (HSS, Cobalt, Carbide) are bundled with the package and used automatically with no configuration required.

To add your own materials/tools, or override a built-in tool's factors, pass an optional user TOML file via --materials-config, either through the mfgparams console script or python -m mfgparams (both parse the same CLI flag):

mfgparams --materials-config my-mfgparams.toml
# or: python -m mfgparams --materials-config my-mfgparams.toml
# my-mfgparams.toml
[[materials]]
name = "Bronze"
material_type = "metal"           # groups it under the "Metal" type prompt
reference_cutting_speed = 45.0    # m/min (or ft/min if unit_system = "imperial")
reference_feed_per_rev = 0.18     # mm/rev (or in/rev if imperial)
specific_cutting_force = 750.0    # N/mm^2 (or psi if imperial)

[[materials]]
name = "PVC"
material_type = "plastic"         # a brand-new type - no code change needed
reference_cutting_speed = 200.0
reference_feed_per_rev = 0.30
specific_cutting_force = 80.0

[[tools]]
name = "Carbide"                  # matches a built-in name -> overrides its factors
cutting_speed_factor = 3.0
feed_factor = 1.1
  • material_type is a free-form identifier, so declaring a value that is not yet in use (e.g. "plastic" or "cement") registers a new type in the material-type prompt without any code change. Types are listed in the order they first appear across the bundled then user-supplied materials.

  • Types with a known identifier (metal, wood) get a translated label; any other identifier is displayed title-cased (composite-fibre → Composite Fibre).

  • material_type is optional. A material that omits it is grouped under uncategorized. Because the key is "sticky" when merging, a user entry that overrides a built-in material without restating material_type keeps the built-in type rather than being decategorized.

  • Entries with a new name are added alongside the built-in defaults; entries whose name matches a built-in material/tool override it.

  • unit_system = "imperial" (default: "metric") declares that the entry's numeric fields are in imperial units; they are converted to metric automatically and produce identical results to an equivalent metric entry.

  • An optional [materials.translations] (or [tools.translations]) table maps a locale code (e.g. fr) to a translated display name, shown when MFGPARAMS_LOCALE is set accordingly; unset/unsupported locales and entries without a translation fall back to the English name.

  • A missing/unreadable --materials-config file is a non-fatal notice — the CLI falls back to the bundled defaults. A malformed TOML file or a duplicate material/tool name within the file is a fatal, translated error and the CLI exits without starting the text GUI.

  • Invalid material numeric fields (missing/non-numeric/non-positive cutting speed, feed, or specific cutting force) are logged as warnings at registry load time; startup continues and the entry remains listable, but calculations with that entry fail safely with a user-facing UNUSABLE_MATERIAL error.

See specs/005-configurable-materials-tools/quickstart.md for full runnable scenarios, and specs/005-configurable-materials-tools/contracts/materials-config-schema.md for the exact TOML schema.

Adding or changing a user-facing string

Every user-facing string lives in a message catalog, never inlined at its call site (Constitution Principle VIII). Which catalog depends on what the string is:

  • A prompt, label, or status/progress message the interactive console displays — add or edit it in mfgparams/console/locales/en.py.
  • Error, warning, or startup-notice text returned by the library API (ErrorInfo.message, CalculationResult.feasibility_warning, a materials-config load notice/error) — add or edit it in mfgparams/locales/en.py. This text must stay readable without the console extra installed.
  • The material/tool display name a user supplies via [materials.translations]/ [tools.translations] in their own config file is not a catalog entry at all — see the materials configuration section above.

See specs/015-console-i18n-relocation/contracts/catalogue-ownership-contract.md for the full rule, including two narrow, explicitly documented exceptions.

console/tui/ architecture

The text GUI (mfgparams/console/tui/) is one persistent prompt-toolkit Application for the whole session, constructed once by app.py's build_app() — not a chain of short-lived, per-screen Applications the way the 017-era dialog chain worked (prompt-toolkit does not support a second, nested Application.run() call, which is exactly why that model changed). Three kinds of state stay deliberately separate:

  • SessionUI (app.py) — the session-lifetime, business-relevant state: the menu bar's fixed entries, the Machining tree's expand/collapse flag (MachiningTree — Milling and Drilling are flat leaves, so this is a single bool), which operation screen (if any) is open (OperationScreen), and each operation's own remembered inputs (DrillingSessionState/MillingSessionState, one instance per operation — two for Milling, one per sub-operation — so revisiting a screen offers the previous answers as defaults). tree and open_operation are independent fields with no code path writing both from the same handler: collapsing the tree never closes an open operation, and vice versa — trivially so, since the operation screen renders as a floating window (a prompt_toolkit.layout.FloatContainer/Float layered above the bar+tree, not embedded inside their own container) that isn't part of the tree's container at all.
  • _ViewState (app.py, module-private) — pure UI-presentation state that does not survive a body change on purpose: which background body is currently shown (body_mode — the tree, About, Help, or Configuration; an open operation is not one of its values, since the floating window is independent of it) and which row is highlighted within it. Kept separate from SessionUI so, for example, selecting Configuration from the bar never touches the Machining tree's own state.
  • OperationScreen.field_buffer — the raw, not-yet-committed text of whichever numeric field is currently selected in a split-pane screen (see below); empty for a radio field, which has no buffered state of its own since Left/Right/Space commit it immediately. Committed only when the user navigates away from the field (Up/Down) — distinct from that field's last-committed value until then. OperationScreen.status is a sibling field: None normally, or a transient message (e.g. "'abc' is not a number") shown in the bottom status bar in place of the usual keyboard hint, set only when navigating away from a field whose buffer didn't parse.

Four widgets render as pure functions of this state — menu.render_menu_bar, machining_menu.render_tree, screens.about.render_about, screens.help.render_help — rather than each owning its own dialog/Layout. The Drilling/Milling screens (screens/drilling.py, screens/milling.py) are built the same way but share one more layer, screens/split_pane.py: each screen's rows_for() returns a list of RadioRow/NumberRow describing that operation's current fields (a later row's presence or options can depend on an earlier row's committed value — e.g. the specific-material row only appears once a material type is chosen — so the list is rebuilt every render, not cached), and split_pane.py owns the shared navigation/edit/nudge/commit logic and the right pane's two-state result machine (a placeholder while any required field is still unset; a result, or an error calculate() itself rejects for a complete-but-invalid combination, once every required field has a value). Text that never parses as a number is not a right-pane state at all — it never reaches session_state, so it can't reach calculate() either; instead it surfaces via the bottom status bar (render_bottom_bar), only once the user tries to navigate away from the offending field, not while still typing. Deliberately, this module does not re-validate field ranges itself — calculate()/calculate_end_milling()/calculate_face_milling() already re-validate every field internally regardless of caller.

Testing drives the real Application headlessly: prompt_toolkit.output.DummyOutput renders nowhere, and prompt_toolkit.input.create_pipe_input feeds a scripted key sequence from a background thread (with explicit contextvars propagation — a plain threading.Thread does not inherit prompt-toolkit's ambient input/output context). tests/integration/_tui_test_support.py's run_headless() supports an on_batch hook that runs between each batch of keys, so a test can inspect SessionUI/OperationScreen state mid-session (e.g. confirming the right pane shows a placeholder before every field is complete), not just after the whole script finishes.

Run the tests

pytest

This project targets Python 3.9+ for compatibility with older/stable Linux distributions (see .specify/memory/constitution.md Principle V), and aims for ≥90% test coverage on calculation modules (Principle II). The command above runs against whichever Python interpreter is active in your virtual environment.

See specs/001-metal-drilling-calc/ for the full spec, plan, and task breakdown driving this implementation.

Checking every supported Python version locally

To verify a change against every officially supported Python version (3.9-3.12) without hand-building a separate environment per version, use tox (installed as part of the dev extra; each environment it builds installs the narrower test extra):

tox            # the suite + coverage gate once per supported version
tox -p auto    # the same, in parallel
tox -e py39    # or just one version, for a faster inner loop

# Narrowing down one failure: pass `--no-cov` with any filter, or the 90%
# coverage gate fails the environment even when every selected test passed.
tox -e py39 -- --no-cov -k drilling -x

# The wheel-contents assertions, which the envs above deselect. They verify
# packaging rather than Python-version compatibility, so they run once.
tox -e packaging

The version envs deselect the packaging marker (tests/integration/test_packaging_bundled_data.py). Those tests shell out to python -m build, and setuptools writes its scratch tree to <repo>/build/ regardless of --outdir — one directory shared by every env. Running them in all four was what made tox -p unsafe (issue #74); with no env in envlist building, parallel mode works. CI runs them once in its build job, which is equally merge-blocking, so nothing stops gating on them.

Any supported interpreter not installed on your machine (e.g., no python3.9 on PATH) is reported SKIPPED rather than failing the run — install it (e.g., via pyenv install 3.9) to include it.

Legacy-hardware performance suite (opt-in)

A separate, opt-in tests/performance/ suite checks that every public calculation function stays within the resource budget of Constitution Principle V's legacy/low-power hardware target (single-core CPU, ~64-128 MB RAM, 0.5-1.0s per calculation). It is skipped automatically by the pytest command above and does not affect its duration, outcome, or coverage. Run it explicitly with:

MFGPARAMS_RUN_PERFORMANCE_TESTS=1 pytest tests/performance/ -m performance -p no:cacheprovider --no-cov -v -s

See specs/006-legacy-hardware-performance-tests/quickstart.md for the full set of validation scenarios (including graceful degradation on macOS/Windows and actionable failure reporting).

Quality & Security Gates (CI)

Every pull request is measured against the following checks, per .specify/memory/constitution.md Principle IX. All of them gate a merge, but only three are named in main's ruleset — see the note below the table. Most of them also run conditionally: a check with nothing in its scope to evaluate (e.g. typecheck on a pull request that touches no Python source) reports Skipped, not run — see the path-based selection note below the table.

Check Tool Enforces
changes dorny/paths-filter Classifies the diff into path categories that decide which of the jobs below actually run
lint ruff (incl. C90/mccabe) Style, formatting, cyclomatic complexity (FR-001); also runs whenever .github/skills/**/.claude/** changes (skill-symlink integrity)
complexity scripts/check_maintainability.py (radon mi) Maintainability Index (FR-002)
typecheck mypy Static type errors (FR-003)
security bandit High/medium-severity security findings (FR-004)
dependency-scan pip-audit Known CVEs in resolved dependencies (FR-005); also runs weekly, independent of PRs, and is never path-selected
test (3.9), test (3.10), test (3.11), test (3.12) pytest -m "not packaging" --cov Test failures / coverage below 90%, checked separately on every officially supported Python version
build python -m build, then pytest -m packaging Package build failures, and the wheel-contents assertions — the only place in CI they run; also runs whenever README.md/LICENSE.md changes (packaging metadata)
docs Sphinx Docs build failures
repo-invariants pytest (two repo-wide static tests) No stray reference to this package's old name/layout anywhere in the tree — reruns unconditionally, since test's own path-based skip can't safely cover a check that scans every file regardless of category
CodeQL (Analyze (python)) GitHub CodeQL default setup New high-confidence security alerts (FR-006)
ci-ok aggregate Passes when every one of the ten jobs above (excluding CodeQL) either succeeded or was intentionally skipped by path selection; fails on any real failure or cancellation

main's ruleset requires exactly three checks — ci-ok, Analyze (python) and CodeQL — not the individual jobs. Every job in the table still runs and reports under its own name; they are simply no longer read by branch protection, so renaming a job or adding a Python version no longer requires a ruleset change (issue #75 P2.4). Analyze (python) and CodeQL stay separate because they come from GitHub's managed CodeQL setup rather than ci.yml.

Path-based selection (specs/016-ci-path-based-selection): lint, complexity, typecheck, security, test, build, and docs skip themselves when a pull request's changed paths fall outside what each depends on (e.g. a specs-only or docs-only PR shows most of them as Skipped) — a Skipped job does not block ci-ok, only a real failure/ cancelled does. changes and repo-invariants are never skipped for path reasons. performance, quality-summary, deploy-docs and sync-agent-integrations are supporting jobs, deliberately outside ci-ok; pulling one in would make it a merge blocker, which tests/static/test_ci_ok_aggregate_check.py fails on.

main is protected by two GitHub rulesets (not classic branch protection): a status-checks ruleset with no bypass for anyone (a failing required check blocks every contributor, including the repository owner), and a separate PR-review ruleset whose "require a pull request" rule has a bypass scoped only to the repository owner. See specs/003-ci-quality-security-gates/contracts/ci-checks-contract.md for the full contract.

Multi-agent skill sync

A separate, non-PR-blocking sync-agent-integrations job (same file, weekly cron plus on-demand workflow_dispatch) regenerates every installed coding-agent integration (currently GitHub Copilot's .github/agents/+.github/prompts/, Claude Code's .claude/skills/) from Spec Kit's upstream template source, and opens a pull request only when something actually drifted. This keeps per-agent instructions from being hand-duplicated or silently going stale, per .specify/memory/constitution.md Principle XI — see specs/011-multi-agent-skill-sync for the full design.

Shared skills across coding agents

Some repo-specific skills under .github/skills/ (GitHub Copilot's skill directory) are genuinely useful from Claude Code too — e.g. pr-review-loop, pypi-package-builder, skill-authoring. Rather than hand-copying them into .claude/skills/ (which would duplicate and drift, exactly what .specify/memory/constitution.md Principle XI forbids), each one is symlinked from .claude/skills/<name> to the canonical skill directory .github/skills/<name>/ (which holds SKILL.md and any supporting files) — one physical directory, referenced from two places, so it can't diverge (Principle XI's "genuinely shared, hand-authored skills" exception, v1.9.0).

code-review is deliberately not symlinked: it collides with Claude Code's own bundled /code-review skill, and a same-named project skill takes precedence over a bundled one, so linking it would silently shadow Claude Code's built-in review capability instead of adding to it.

Setting this up (already done once and committed as real symlinks, but run this after cloning if .claude/skills/<name> looks broken or missing — notably on Windows, where a clone made without symlink support checked out as plain text files instead of real symlinks):

python scripts/setup_skill_symlinks.py          # create/fix the symlinks
python scripts/setup_skill_symlinks.py --check  # report status only, exit non-zero if anything's wrong

CI runs the --check form in the lint job, so a committed link that goes missing, points at the wrong skill, or gets replaced by a hand-copied duplicate fails the build rather than drifting unnoticed.

On Windows, running the script above requires either Developer Mode (Settings > Update & Security > For developers) or an elevated (Administrator) terminal — the script's error message repeats this if it can't create a symlink. Separately, and only relevant to future clones (not to fixing an existing checkout with the script above): setting git config --global core.symlinks true before cloning makes git checkout materialize a real symlink for you in the first place, instead of the plain-text placeholder file that puts you in this situation. core.symlinks has no effect on the script's own symlink creation — Developer Mode/elevation alone is what that needs.

Documented exceptions instead of silent suppressions

If a finding is a genuine false positive or an accepted, understood risk, suppress it with the tool's own native mechanism and a rationale comment, so the exception is visible in the code/PR diff rather than hidden in CI config:

  • Complexity (ruff/C901): # noqa: C901 <why this function's complexity is necessary/accepted>
  • Security (bandit): # nosec B### <why this specific finding is a false positive or accepted risk> — do not use a bare # nosec (it silently suppresses everything on that line), and avoid putting the literal word "nosec" in unrelated comment text elsewhere on the line, since bandit's suppression regex matches that substring anywhere in the trailing comment.
  • Type errors (mypy): # type: ignore[<error-code>] <why this specific mypy rule doesn't apply here> — never a bare # type: ignore (it hides all future errors on that line, not just the one you reviewed).
  • Dependency findings (pip-audit): add a documented ignore entry (e.g. pyproject.toml [tool.pip-audit] ignore-vulns, with a comment linking the CVE and the acceptance rationale) rather than pinning to an insecure version silently.

Complexity and security exceptions specifically also require the same rationale to be restated in the pull request description itself (Constitution Principle IX) — the in-code comment alone is not sufficient for these two gates, since they are the ones most likely to hide a real defect if suppressed casually. See .github/pull_request_template.md for the required section, and reviewers: reject any PR that suppresses a complexity or security finding without both the in-code rationale comment and the PR-description restatement.

Metadata

Release files for mfgparams 2.5.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for mfgparams 2.5.0
File Size Uploaded Provenance Actions
mfgparams-2.5.0.tar.gz 179.3 kB With provenance Details for mfgparams-2.5.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for mfgparams 2.5.0
File Interpreter ABI Platform Actions
mfgparams-2.5.0-py3-none-any.whl Python 3 none any Details for mfgparams-2.5.0-py3-none-any.whl

Total release size: 369.7 kB

Release files / mfgparams-2.5.0.tar.gz

Download URL mfgparams-2.5.0.tar.gz
Size 179.3 kB
Tags Source
SHA-256 checksum
How to use checksums
46c45d57baaa079e8e0c07dd18d3c9c98150b1adfb97acd7cfd961e89318e647
BLAKE2b-256 checksum
How to use checksums
4ae52a91ac667d71fdcb02c4eaa458a9ea10b8da8b2c10962e6c1fc27ff4c8b3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Release files / mfgparams-2.5.0-py3-none-any.whl

Download URL mfgparams-2.5.0-py3-none-any.whl
Size 190.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7a8b30607564d8a7d9beb611eb034a99dc81c868449cf0259a3ba82f66985e50
BLAKE2b-256 checksum
How to use checksums
6dad2738b4c26633e0143fc36bd0a3c271f932a52c786bb8179d5d5e6059f11b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Security for mfgparams 2.5.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for mfgparams-2.5.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page