[build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"

[project]
name = "freecad-robust-mcp"
dynamic = ["version"]
description = "Robust MCP Server for FreeCAD integration with Claude Code and other AI assistants"
readme = "README.md"
license = "MIT"
requires-python = ">=3.11"
authors = [
    { name = "Sean P. Kane", email = "spkane@gmail.com" }
]
maintainers = [
    { name = "Sean P. Kane", email = "spkane@gmail.com" }
]
keywords = [
    "freecad",
    "mcp",
    "model-context-protocol",
    "claude",
    "claude-code",
    "ai",
    "llm",
    "cad",
    "3d-modeling",
    "3d-printing",
    "parametric-design",
    "automation",
]
classifiers = [
    "Development Status :: 4 - Beta",
    "Environment :: Console",
    "Framework :: AsyncIO",
    "Intended Audience :: Developers",
    "Intended Audience :: Manufacturing",
    "Intended Audience :: Science/Research",
    "License :: OSI Approved :: MIT License",
    "Natural Language :: English",
    "Operating System :: OS Independent",
    "Operating System :: MacOS",
    "Operating System :: POSIX :: Linux",
    "Operating System :: Microsoft :: Windows",
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.11",
    "Programming Language :: Python :: 3.12",
    "Programming Language :: Python :: 3.13",
    "Programming Language :: Python :: 3 :: Only",
    "Topic :: Scientific/Engineering",
    "Topic :: Scientific/Engineering :: Visualization",
    "Topic :: Software Development :: Libraries :: Python Modules",
    "Typing :: Typed",
]

dependencies = [
    "mcp>=1.25.0",
    "pydantic>=2.10.0",
    "pydantic-settings>=2.7.0",
]

[project.optional-dependencies]
dev = [
    # Testing
    "pytest>=8.3.0",
    "pytest-asyncio>=0.25.0",
    "pytest-cov>=6.0.0",
    "pytest-timeout>=2.3.0",
    "pytest-watch>=4.2.0",
    # Linting and formatting
    "mypy>=1.14.0",
    "ruff>=0.8.0",
    # Security scanning
    "bandit[toml]>=1.8.0",
    "safety>=3.2.0",
    "detect-secrets>=1.5.0",
    # Pre-commit and commit conventions
    "pre-commit>=4.0.0",
    "commitizen>=4.1.0",
    # Documentation
    "mkdocs>=1.6.0",
    "mkdocs-material>=9.7.1",
    "mkdocstrings[python]>=0.27.0",
    "mkdocs-git-revision-date-localized-plugin>=1.2.0",
    "mkdocs-minify-plugin>=0.8.0",
    "mkdocs-redirects>=1.2.0",
    "mkdocs-glightbox>=0.4.0",
    "mkdocs-macros-plugin>=1.0.0",
    "md-toc>=9.0.0",
    "codespell>=2.3.0",
    # Publishing
    "twine>=6.0.0",
    "build>=1.2.0",
]

[project.scripts]
freecad-mcp = "freecad_mcp.server:main"

[project.urls]
Homepage = "https://github.com/spkane/freecad-robust-mcp-and-more"
Documentation = "https://github.com/spkane/freecad-robust-mcp-and-more#readme"
Repository = "https://github.com/spkane/freecad-robust-mcp-and-more.git"
Issues = "https://github.com/spkane/freecad-robust-mcp-and-more/issues"
Changelog = "https://github.com/spkane/freecad-robust-mcp-and-more/blob/main/CHANGELOG.md"
"Docker Hub" = "https://hub.docker.com/r/spkane/freecad-robust-mcp"

[tool.hatch.version]
source = "vcs"
# Use git tags for versioning: v0.1.0 -> 0.1.0
# For untagged commits: 0.1.0.dev5+g1a2b3c4

[tool.hatch.version.raw-options]
# Fall back version when not in a git repo (e.g., source tarball)
version_scheme = "guess-next-dev"
local_scheme = "node-and-date"

[tool.hatch.build.hooks.vcs]
version-file = "src/freecad_mcp/_version.py"

[tool.hatch.build.targets.wheel]
packages = ["src/freecad_mcp"]

[tool.ruff]
target-version = "py311"
line-length = 88
src = ["src", "tests", "macros"]
extend-include = ["*.FCMacro"]
# Exclude auto-generated files (hatch-vcs version file)
exclude = ["src/freecad_mcp/_version.py"]

[tool.ruff.lint]
select = [
    "E",      # pycodestyle errors
    "W",      # pycodestyle warnings
    "F",      # Pyflakes
    "I",      # isort
    "B",      # flake8-bugbear
    "C4",     # flake8-comprehensions
    "UP",     # pyupgrade
    "ARG",    # flake8-unused-arguments
    "SIM",    # flake8-simplify
    "TCH",    # flake8-type-checking
    "PTH",    # flake8-use-pathlib
    "ERA",    # eradicate (commented-out code)
    "PL",     # Pylint
    "RUF",    # Ruff-specific rules
    "S",      # flake8-bandit (security)
    "ANN",    # flake8-annotations
    "ASYNC",  # flake8-async
    "D",      # pydocstyle
]
ignore = [
    "D104",   # Missing docstring in public package - __init__.py often just imports
    "D107",   # Missing docstring in __init__ - class docstring covers this
    "ANN401", # Allow Any in type annotations - FreeCAD types are dynamic
    "ANN001", # Allow missing type annotations - FastMCP doesn't export types
    "PLR0913",# Too many arguments - CAD APIs have many params
    "PLR0915",# Too many statements - registration functions are long
    "PLC0415",# Allow imports in functions - FreeCAD must be imported lazily
    "PLR2004",# Allow magic numbers in comparisons - CAD dimensions
    "PLW0603",# Allow global statement - bridge lifecycle management
    "E501",   # Line too long - embedded Python code strings
    "RUF022", # Don't sort __all__ - prefer logical grouping
    "S110",   # Allow try-except-pass for optional operations
    "ARG002", # Allow unused arguments - needed for interface compliance
]

[tool.ruff.lint.pydocstyle]
convention = "google"

[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = [
    "S101",   # Allow asserts in tests
    "S108",   # Allow /tmp in tests
    "ANN",    # Don't require annotations in tests
    "D",      # Don't require docstrings in tests
    "PLR2004",# Allow magic numbers in tests
    "PTH",    # Allow os.path in tests - matches embedded FreeCAD code style
    "SIM105", # Allow try-except-pass in tests
    "SIM117", # Allow nested with statements
    "B017",   # Allow blind exception catch in tests
]
# FreeCAD macros have relaxed rules - they are standalone GUI scripts
# But still require essential docstrings (module, class, public function)
"**/*.FCMacro" = [
    "ANN",    # Don't require type annotations - FreeCAD types are complex
    "D104",   # Missing docstring in public package - N/A for macros
    "D105",   # Missing docstring in magic method - __init__ etc don't need them
    "D107",   # Missing docstring in __init__ - class docstring covers this
    "D203",   # Blank line before class docstring (conflicts with D211)
    "D212",   # Multi-line docstring start (conflicts with D213)
    "PLR0911",# Allow many return statements - GUI code paths vary
    "PLR0912",# Allow many branches - GUI code is complex
    "PLR0913",# Allow many arguments - CAD APIs have many params
    "PLR0915",# Allow many statements - macros are self-contained
    "PTH",    # Allow os.path - better FreeCAD compatibility than pathlib
    "SIM102", # Allow nested if statements - improves readability
    "SIM103", # Allow return after if-else for clarity
    "SIM108", # Allow if-else over ternary for readability
    "S112",   # Allow try-except-continue - used for geometry iteration
    "F841",   # Allow unused variables - Qt signal connections
    "UP008",  # Allow super(Class, self) - PySide compatibility
]

[tool.mypy]
python_version = "3.11"
# Not using strict mode due to FastMCP lacking type stubs
warn_return_any = false
warn_unused_configs = true
disallow_untyped_defs = false
disallow_incomplete_defs = false
check_untyped_defs = true
disallow_untyped_decorators = false
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = false
warn_no_return = true
follow_imports = "silent"
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false
disallow_incomplete_defs = false

[[tool.mypy.overrides]]
module = [
    "freecad_mcp.tools",
    "freecad_mcp.tools.*",
]
disallow_untyped_decorators = false
disallow_untyped_defs = false
warn_return_any = false

[[tool.mypy.overrides]]
module = [
    "freecad_mcp.resources",
    "freecad_mcp.resources.*",
]
disallow_untyped_decorators = false
disallow_untyped_defs = false
warn_return_any = false

[[tool.mypy.overrides]]
module = [
    "freecad_mcp.prompts",
    "freecad_mcp.prompts.*",
]
disallow_untyped_decorators = false
disallow_untyped_defs = false

[[tool.mypy.overrides]]
module = [
    "freecad_mcp.bridge",
    "freecad_mcp.bridge.*",
]
warn_return_any = false

[[tool.mypy.overrides]]
module = [
    "freecad_mcp.freecad_plugin",
    "freecad_mcp.freecad_plugin.*",
]
warn_return_any = false
disallow_untyped_defs = false

[[tool.mypy.overrides]]
module = "freecad_mcp.server"
disallow_untyped_decorators = false
disallow_untyped_defs = false

[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
addopts = [
    "-ra",
    "-q",
    "--strict-markers",
    "--strict-config",
]
markers = [
    "slow: marks tests as slow (deselect with '-m \"not slow\"')",
    "integration: marks integration tests requiring FreeCAD",
    "gui: marks tests requiring FreeCAD GUI mode (not headless)",
]

[tool.coverage.run]
source = ["src/freecad_mcp"]
branch = true
parallel = true
# Exclude bridge modules from unit test coverage - they require a running
# FreeCAD instance. These modules are exercised by integration tests which
# run separately (just testing::integration). To verify bridge coverage:
#   uv run pytest tests/integration --cov=freecad_mcp.bridge --cov-report=term-missing
omit = [
    "src/freecad_mcp/bridge/embedded.py",
    "src/freecad_mcp/bridge/socket.py",
    "src/freecad_mcp/bridge/xmlrpc.py",
]

[tool.coverage.report]
exclude_lines = [
    "pragma: no cover",
    "def __repr__",
    "raise AssertionError",
    "raise NotImplementedError",
    "if TYPE_CHECKING:",
    "if __name__ == .__main__.:",
    "@abstractmethod",
]
show_missing = true
fail_under = 80

[tool.bandit]
exclude_dirs = ["tests", "docs"]
skips = [
    "B101",  # Allow asserts
    "B102",  # exec is required for FreeCAD Python execution
    "B104",  # Binding to 0.0.0.0 is intentional for container/network access
    "B110",  # try-except-pass used for optional cleanup
    "B112",  # try-except-continue used for iterating over faces safely
    "B411",  # XML-RPC is required for FreeCAD compatibility
]

[tool.codespell]
skip = ".git,*.lock,*.toml"
ignore-words-list = "crate"

[tool.commitizen]
name = "cz_conventional_commits"
version_scheme = "semver2"
major_version_zero = true
# Note: This project uses component-specific tags for releases:
#   - robust-mcp-server-vX.Y.Z      (MCP server: PyPI, Docker, GitHub release)
#   - robust-mcp-workbench-vX.Y.Z   (FreeCAD workbench)
#   - macro-cut-object-for-magnets-vX.Y.Z
#   - macro-multi-export-vX.Y.Z
# Use `just release::tag-<component> <version>` to create release tags.
# Commitizen is used for commit message validation and changelog generation.
